@vibecheckai/cli 3.2.0 → 3.2.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/runners/lib/agent-firewall/change-packet/builder.js +214 -0
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
- package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
- package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
- package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
- package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/analysis-core.js +198 -180
- package/bin/runners/lib/analyzers.js +1119 -536
- package/bin/runners/lib/cli-output.js +236 -210
- package/bin/runners/lib/detectors-v2.js +547 -785
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/route-truth.js +1167 -322
- package/bin/runners/lib/scan-output.js +144 -738
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/terminal-ui.js +188 -770
- package/bin/runners/lib/truth.js +1004 -321
- package/bin/runners/lib/unified-output.js +162 -158
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runScan.js +113 -10
- package/bin/runners/runShip.js +7 -8
- package/bin/runners/runTruth.js +89 -0
- package/mcp-server/agent-firewall-interceptor.js +164 -0
- package/mcp-server/index.js +347 -313
- package/mcp-server/truth-context.js +131 -90
- package/mcp-server/truth-firewall-tools.js +1412 -1045
- package/package.json +1 -1
|
@@ -1,860 +1,622 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* - Entitlements (D_LOCAL_BYPASS_*)
|
|
15
|
-
* - Drift (D_CONTRACTS_*)
|
|
2
|
+
* Truth Context – MCP Tools for Evidence‑Backed AI
|
|
3
|
+
*
|
|
4
|
+
* Core context-engine tools that surface **truth-backed** context for AI agents.
|
|
5
|
+
* Every response is grounded in concrete evidence with file/line citations
|
|
6
|
+
* and explicit confidence scores.
|
|
7
|
+
*
|
|
8
|
+
* This is the "Truth Firewall", exposed to agents as an "Evidence Pack" / "Truth Pack". [web:3]
|
|
9
|
+
*
|
|
10
|
+
* Tools:
|
|
11
|
+
* - vibecheck.ctx – Build a repo-level Truth Pack (routes, auth, billing, env, schema)
|
|
12
|
+
* - vibecheck.verify_claim – Check whether a claim is backed by real evidence
|
|
13
|
+
* - vibecheck.evidence – Pull code-level evidence for a specific file/function
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
import fs from "fs/promises";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { execSync } from "child_process";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// TRUTH CONTEXT TOOLS
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
export const TRUTH_CONTEXT_TOOLS = [
|
|
25
|
+
{
|
|
26
|
+
name: "vibecheck.ctx",
|
|
27
|
+
description: `📋 Build a repo Truth Pack: routes, auth, billing, env vars, schema.
|
|
28
|
+
|
|
29
|
+
Generates an evidence-backed context bundle with file/line citations.
|
|
30
|
+
Use this before the agent makes any architectural or behavioral claims
|
|
31
|
+
about the codebase.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
- routes: All detected routes with handlers and middleware
|
|
35
|
+
- auth: Auth guards, protected routes, auth flow indicators
|
|
36
|
+
- billing: Payment gates, subscription checks, paid feature indicators
|
|
37
|
+
- env: Environment variables (declared vs used, mismatches)
|
|
38
|
+
- schema: Database schema and TypeScript contracts
|
|
39
|
+
- confidence: Aggregate confidence score (0–1) for the extracted view`,
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
scope: {
|
|
44
|
+
type: "string",
|
|
45
|
+
enum: ["all", "routes", "auth", "billing", "env", "schema"],
|
|
46
|
+
description: "Which slice of context to extract (default: all)",
|
|
47
|
+
default: "all",
|
|
48
|
+
},
|
|
49
|
+
path: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Project root path (default: current working directory)",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "vibecheck.verify_claim",
|
|
58
|
+
description: `🔍 Truth Firewall check – verify that a claim is backed by code.
|
|
59
|
+
|
|
60
|
+
Run this before asserting that something exists, is configured, or is enforced.
|
|
61
|
+
Returns concrete evidence (file/line) when the claim is supported,
|
|
62
|
+
or a structured rejection with an explanation when it is not.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
- "Route /api/users exists" → VERIFIED with handler at src/routes/users.ts:45
|
|
66
|
+
- "Auth is required for /admin" → VERIFIED via middleware at src/middleware/auth.ts:12
|
|
67
|
+
- "Stripe is configured" → REJECTED: No evidence of Stripe integration found`,
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
claim_type: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: [
|
|
74
|
+
"route",
|
|
75
|
+
"endpoint",
|
|
76
|
+
"env_var",
|
|
77
|
+
"middleware",
|
|
78
|
+
"auth_guard",
|
|
79
|
+
"billing_gate",
|
|
80
|
+
"file",
|
|
81
|
+
"function",
|
|
82
|
+
],
|
|
83
|
+
description: "Category of claim to verify",
|
|
84
|
+
},
|
|
85
|
+
claim: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description:
|
|
88
|
+
"The claim subject (e.g. '/api/users', 'AUTH_SECRET', 'authMiddleware')",
|
|
89
|
+
},
|
|
90
|
+
path: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Project root path (default: current working directory)",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
required: ["claim_type", "claim"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "vibecheck.evidence",
|
|
100
|
+
description: `📎 Retrieve code evidence for a file or symbol.
|
|
101
|
+
|
|
102
|
+
Returns an annotated code snippet with line numbers for precise citation.
|
|
103
|
+
Use this when the agent needs to quote or reason about specific code blocks
|
|
104
|
+
in its response.`,
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
file: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "File path relative to the project root",
|
|
111
|
+
},
|
|
112
|
+
function_name: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "Optional function/class name to locate within the file",
|
|
115
|
+
},
|
|
116
|
+
line: {
|
|
117
|
+
type: "number",
|
|
118
|
+
description: "Optional 1-based line number to center the snippet on",
|
|
119
|
+
},
|
|
120
|
+
context_lines: {
|
|
121
|
+
type: "number",
|
|
122
|
+
description:
|
|
123
|
+
"Number of lines of context before/after the target (default: 10)",
|
|
124
|
+
default: 10,
|
|
125
|
+
},
|
|
126
|
+
path: {
|
|
127
|
+
type: "string",
|
|
128
|
+
description: "Project root path (default: current working directory)",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ["file"],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// TOOL DISPATCH
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
export async function handleTruthContextTool(toolName, args) {
|
|
141
|
+
const projectPath = args.path || process.cwd();
|
|
142
|
+
|
|
143
|
+
switch (toolName) {
|
|
144
|
+
case "vibecheck.ctx":
|
|
145
|
+
return await getTruthPack(projectPath, args.scope || "all");
|
|
146
|
+
case "vibecheck.verify_claim":
|
|
147
|
+
return await verifyClaim(projectPath, args.claim_type, args.claim);
|
|
148
|
+
case "vibecheck.evidence":
|
|
149
|
+
return await getEvidence(projectPath, args.file, args);
|
|
150
|
+
default:
|
|
151
|
+
return { error: `Unknown tool: ${toolName}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
19
154
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const { createFindingV2, createEvidence, generateFingerprint } = require("./schema-validator");
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// CONTEXT EXTRACTION
|
|
157
|
+
// ============================================================================
|
|
24
158
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
159
|
+
async function getTruthPack(projectPath, scope) {
|
|
160
|
+
const truthPack = {
|
|
161
|
+
version: "1.0.0",
|
|
162
|
+
generatedAt: new Date().toISOString(),
|
|
163
|
+
projectPath,
|
|
164
|
+
scope,
|
|
165
|
+
confidence: 0,
|
|
166
|
+
sections: {},
|
|
167
|
+
};
|
|
28
168
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
function detectRouteMissing(truthpack) {
|
|
34
|
-
const findings = [];
|
|
35
|
-
const serverRoutes = truthpack.routes || [];
|
|
36
|
-
const clientCalls = truthpack.clientCalls || [];
|
|
37
|
-
|
|
38
|
-
for (const call of clientCalls) {
|
|
39
|
-
const resolved = call.resolvedPath || call.urlTemplate;
|
|
40
|
-
const method = call.method || "UNKNOWN";
|
|
41
|
-
|
|
42
|
-
const match = serverRoutes.find(r =>
|
|
43
|
-
routeMatches(r.path, resolved) &&
|
|
44
|
-
(r.methods.includes(method) || r.methods.includes("*"))
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
if (!match) {
|
|
48
|
-
findings.push(createFindingV2({
|
|
49
|
-
detectorId: "ROUTE_MISSING",
|
|
50
|
-
severity: "BLOCK",
|
|
51
|
-
category: "Routes",
|
|
52
|
-
scope: "client",
|
|
53
|
-
title: `Client calls ${method} ${resolved} but no server route exists`,
|
|
54
|
-
why: "AI frequently invents endpoints. This will cause 404 errors or silent failures in production.",
|
|
55
|
-
confidence: call.confidence || "medium",
|
|
56
|
-
evidence: call.evidence || [
|
|
57
|
-
createEvidence({
|
|
58
|
-
kind: "file",
|
|
59
|
-
reason: "Client call site",
|
|
60
|
-
file: call.evidence?.[0]?.file || "unknown",
|
|
61
|
-
lines: call.evidence?.[0]?.lines || "1-1",
|
|
62
|
-
})
|
|
63
|
-
],
|
|
64
|
-
fixHints: [
|
|
65
|
-
"Create the missing server route handler",
|
|
66
|
-
"Or update the client to call an existing route",
|
|
67
|
-
"Check truthpack.routes for available endpoints"
|
|
68
|
-
],
|
|
69
|
-
}));
|
|
169
|
+
try {
|
|
170
|
+
if (scope === "all" || scope === "routes") {
|
|
171
|
+
truthPack.sections.routes = await extractRoutes(projectPath);
|
|
70
172
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const serverRoutes = truthpack.routes || [];
|
|
83
|
-
const clientCalls = truthpack.clientCalls || [];
|
|
84
|
-
|
|
85
|
-
for (const call of clientCalls) {
|
|
86
|
-
const resolved = call.resolvedPath || call.urlTemplate;
|
|
87
|
-
const clientMethod = call.method || "UNKNOWN";
|
|
88
|
-
|
|
89
|
-
const pathMatch = serverRoutes.find(r => routeMatches(r.path, resolved));
|
|
90
|
-
|
|
91
|
-
if (pathMatch && !pathMatch.methods.includes(clientMethod) && !pathMatch.methods.includes("*")) {
|
|
92
|
-
const isCritical = /checkout|login|save|pay|submit|register/i.test(resolved);
|
|
93
|
-
|
|
94
|
-
findings.push(createFindingV2({
|
|
95
|
-
detectorId: "ROUTE_METHOD_MISMATCH",
|
|
96
|
-
severity: isCritical ? "BLOCK" : "WARN",
|
|
97
|
-
category: "Routes",
|
|
98
|
-
scope: "client",
|
|
99
|
-
title: `Method mismatch: client uses ${clientMethod} but server only handles ${pathMatch.methods.join("/")} for ${resolved}`,
|
|
100
|
-
why: "Method mismatch will cause 405 errors. Critical paths (checkout/login) must match exactly.",
|
|
101
|
-
confidence: "high",
|
|
102
|
-
evidence: [
|
|
103
|
-
createEvidence({
|
|
104
|
-
kind: "file",
|
|
105
|
-
reason: "Client call site",
|
|
106
|
-
file: call.evidence?.[0]?.file || "unknown",
|
|
107
|
-
lines: call.evidence?.[0]?.lines || "1-1",
|
|
108
|
-
}),
|
|
109
|
-
createEvidence({
|
|
110
|
-
kind: "file",
|
|
111
|
-
reason: "Server route handler",
|
|
112
|
-
file: pathMatch.handler?.file || "unknown",
|
|
113
|
-
lines: "1-1",
|
|
114
|
-
})
|
|
115
|
-
],
|
|
116
|
-
fixHints: [
|
|
117
|
-
`Update client to use ${pathMatch.methods[0]} method`,
|
|
118
|
-
`Or add ${clientMethod} handler to server route`
|
|
119
|
-
],
|
|
120
|
-
}));
|
|
173
|
+
if (scope === "all" || scope === "auth") {
|
|
174
|
+
truthPack.sections.auth = await extractAuth(projectPath);
|
|
175
|
+
}
|
|
176
|
+
if (scope === "all" || scope === "billing") {
|
|
177
|
+
truthPack.sections.billing = await extractBilling(projectPath);
|
|
178
|
+
}
|
|
179
|
+
if (scope === "all" || scope === "env") {
|
|
180
|
+
truthPack.sections.env = await extractEnvVars(projectPath);
|
|
181
|
+
}
|
|
182
|
+
if (scope === "all" || scope === "schema") {
|
|
183
|
+
truthPack.sections.schema = await extractSchema(projectPath);
|
|
121
184
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return findings;
|
|
125
|
-
}
|
|
126
185
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const findings = [];
|
|
133
|
-
const fastifyPrefixes = truthpack.stack?.fastify?.prefixes || [];
|
|
134
|
-
const clientCalls = truthpack.clientCalls || [];
|
|
135
|
-
|
|
136
|
-
if (fastifyPrefixes.length === 0) return findings;
|
|
137
|
-
|
|
138
|
-
const prefixSet = new Set(fastifyPrefixes);
|
|
139
|
-
let mismatchCount = 0;
|
|
140
|
-
|
|
141
|
-
for (const call of clientCalls) {
|
|
142
|
-
const resolved = call.resolvedPath || call.urlTemplate;
|
|
143
|
-
|
|
144
|
-
// Check if client path uses a known prefix
|
|
145
|
-
const usesKnownPrefix = fastifyPrefixes.some(prefix => resolved.startsWith(prefix));
|
|
146
|
-
|
|
147
|
-
if (!usesKnownPrefix && resolved.startsWith("/api/")) {
|
|
148
|
-
mismatchCount++;
|
|
186
|
+
const sections = Object.values(truthPack.sections);
|
|
187
|
+
if (sections.length > 0) {
|
|
188
|
+
truthPack.confidence =
|
|
189
|
+
sections.reduce((sum, section) => sum + (section.confidence || 0), 0) /
|
|
190
|
+
sections.length;
|
|
149
191
|
}
|
|
150
|
-
}
|
|
151
192
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
why: "Prefix drift causes silent failures. Clients must use the correct API prefix.",
|
|
160
|
-
confidence: "medium",
|
|
161
|
-
evidence: [
|
|
162
|
-
createEvidence({
|
|
163
|
-
kind: "file",
|
|
164
|
-
reason: "Fastify entry file with prefix registration",
|
|
165
|
-
file: truthpack.stack?.fastify?.entryFile || "unknown",
|
|
166
|
-
lines: "1-1",
|
|
167
|
-
})
|
|
168
|
-
],
|
|
169
|
-
fixHints: [
|
|
170
|
-
`Update client calls to use correct prefix (${fastifyPrefixes[0] || "/api"})`,
|
|
171
|
-
"Or update Fastify prefix registration to match client expectations"
|
|
172
|
-
],
|
|
173
|
-
}));
|
|
193
|
+
return truthPack;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return {
|
|
196
|
+
error: error.message,
|
|
197
|
+
projectPath,
|
|
198
|
+
suggestion: "Run `vibecheck init` to set up the project",
|
|
199
|
+
};
|
|
174
200
|
}
|
|
175
|
-
|
|
176
|
-
return findings;
|
|
177
201
|
}
|
|
178
202
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
203
|
+
async function extractRoutes(projectPath) {
|
|
204
|
+
const routes = [];
|
|
205
|
+
const routePatterns = [
|
|
206
|
+
/app\.(get|post|put|patch|delete|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
207
|
+
/router\.(get|post|put|patch|delete|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
208
|
+
/@(Get|Post|Put|Patch|Delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
209
|
+
];
|
|
182
210
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
severity: "BLOCK",
|
|
207
|
-
category: "AuthCoverage",
|
|
208
|
-
scope: "runtime",
|
|
209
|
-
title: `Protected route ${req.url} accessible to anonymous users`,
|
|
210
|
-
why: "Auth contract expects denial/redirect but got 2xx. This is a security bypass.",
|
|
211
|
-
confidence: "high",
|
|
212
|
-
evidence: [
|
|
213
|
-
createEvidence({
|
|
214
|
-
kind: "request",
|
|
215
|
-
reason: "Successful anonymous request to protected route",
|
|
216
|
-
url: req.url,
|
|
217
|
-
httpStatus: req.status,
|
|
218
|
-
requestId: req.id,
|
|
219
|
-
})
|
|
220
|
-
],
|
|
221
|
-
fixHints: [
|
|
222
|
-
"Add server-side auth middleware to this route",
|
|
223
|
-
"Ensure Next.js middleware matcher covers this path",
|
|
224
|
-
"Verify auth contract pattern is correct"
|
|
225
|
-
],
|
|
226
|
-
repro: {
|
|
227
|
-
steps: [
|
|
228
|
-
`Navigate to ${req.url} without authentication`,
|
|
229
|
-
"Observe that the page loads successfully (should be denied)"
|
|
230
|
-
],
|
|
231
|
-
url: req.url,
|
|
232
|
-
},
|
|
233
|
-
}));
|
|
211
|
+
const files = await findSourceFiles(projectPath, [".ts", ".js", ".tsx", ".jsx"]);
|
|
212
|
+
|
|
213
|
+
for (const file of files.slice(0, 50)) {
|
|
214
|
+
try {
|
|
215
|
+
const content = await fs.readFile(file, "utf8");
|
|
216
|
+
const relPath = path.relative(projectPath, file);
|
|
217
|
+
|
|
218
|
+
for (const pattern of routePatterns) {
|
|
219
|
+
let match;
|
|
220
|
+
pattern.lastIndex = 0;
|
|
221
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
222
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
223
|
+
routes.push({
|
|
224
|
+
method: match[1].toUpperCase(),
|
|
225
|
+
path: match[2],
|
|
226
|
+
file: relPath,
|
|
227
|
+
line,
|
|
228
|
+
evidence: {
|
|
229
|
+
snippet: content.split("\n")[line - 1]?.trim(),
|
|
230
|
+
verifiedAt: new Date().toISOString(),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
234
|
}
|
|
235
|
+
} catch {
|
|
236
|
+
// Skip unreadable files
|
|
235
237
|
}
|
|
236
238
|
}
|
|
237
239
|
|
|
238
|
-
return
|
|
240
|
+
return {
|
|
241
|
+
count: routes.length,
|
|
242
|
+
routes: routes.slice(0, 100),
|
|
243
|
+
confidence: routes.length > 0 ? 0.8 : 0.2,
|
|
244
|
+
};
|
|
239
245
|
}
|
|
240
246
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const protectedPatterns = authContract?.protectedRoutes || [];
|
|
250
|
-
const authedNetwork = realityReport.network || [];
|
|
251
|
-
|
|
252
|
-
for (const pattern of protectedPatterns) {
|
|
253
|
-
const blocked = authedNetwork.filter(req =>
|
|
254
|
-
matchPattern(pattern.pattern, req.url) &&
|
|
255
|
-
(req.status === 401 || req.status === 403 || req.status >= 300 && req.status < 400)
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
if (blocked.length > 2 && pattern.expect?.authed === "allow") {
|
|
259
|
-
findings.push(createFindingV2({
|
|
260
|
-
detectorId: "AUTH_PROTECTED_ROUTE_BLOCKED_WHEN_AUTHED",
|
|
261
|
-
severity: "BLOCK",
|
|
262
|
-
category: "AuthCoverage",
|
|
263
|
-
scope: "runtime",
|
|
264
|
-
title: `Protected route ${pattern.pattern} blocks authenticated users`,
|
|
265
|
-
why: "Auth contract expects allow but authenticated user is denied/redirected repeatedly.",
|
|
266
|
-
confidence: "high",
|
|
267
|
-
evidence: blocked.slice(0, 3).map(req => createEvidence({
|
|
268
|
-
kind: "request",
|
|
269
|
-
reason: "Request denied despite authentication",
|
|
270
|
-
url: req.url,
|
|
271
|
-
httpStatus: req.status,
|
|
272
|
-
requestId: req.id,
|
|
273
|
-
})),
|
|
274
|
-
fixHints: [
|
|
275
|
-
"Check if session/token is being passed correctly",
|
|
276
|
-
"Verify middleware is not over-blocking",
|
|
277
|
-
"Check for redirect loops"
|
|
278
|
-
],
|
|
279
|
-
}));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return findings;
|
|
284
|
-
}
|
|
247
|
+
async function extractAuth(projectPath) {
|
|
248
|
+
const authIndicators = [];
|
|
249
|
+
const authPatterns = [
|
|
250
|
+
/auth(enticate|orize|Middleware|Guard|Check)/gi,
|
|
251
|
+
/isAuthenticated|requireAuth|verifyToken|jwt\.verify/gi,
|
|
252
|
+
/passport\.(authenticate|use)/gi,
|
|
253
|
+
/session\.|cookie\./gi,
|
|
254
|
+
];
|
|
285
255
|
|
|
286
|
-
|
|
287
|
-
* D_AUTH_CONTRACT_DRIFT (WARN/BLOCK)
|
|
288
|
-
* Trigger: contracts/auth.json patterns don't match middleware matcher
|
|
289
|
-
*/
|
|
290
|
-
function detectAuthContractDrift(truthpack, authContract) {
|
|
291
|
-
const findings = [];
|
|
292
|
-
const middlewareMatchers = new Set(truthpack.auth?.middlewareMatchers || []);
|
|
293
|
-
const contractPatterns = new Set(authContract?.protectedRoutes?.map(r => r.pattern) || []);
|
|
294
|
-
|
|
295
|
-
// Patterns in contract but not in middleware
|
|
296
|
-
for (const pattern of contractPatterns) {
|
|
297
|
-
if (!middlewareMatchers.has(pattern)) {
|
|
298
|
-
findings.push(createFindingV2({
|
|
299
|
-
detectorId: "AUTH_CONTRACT_DRIFT",
|
|
300
|
-
severity: "BLOCK",
|
|
301
|
-
category: "Drift",
|
|
302
|
-
scope: "contracts",
|
|
303
|
-
title: `Auth pattern "${pattern}" in contract but not in middleware`,
|
|
304
|
-
why: "Contract expects protection but middleware doesn't enforce it. Security boundary may be exposed.",
|
|
305
|
-
confidence: "high",
|
|
306
|
-
evidence: [
|
|
307
|
-
createEvidence({
|
|
308
|
-
kind: "file",
|
|
309
|
-
reason: "Auth contract file",
|
|
310
|
-
file: ".vibecheck/contracts/auth.json",
|
|
311
|
-
lines: "1-1",
|
|
312
|
-
})
|
|
313
|
-
],
|
|
314
|
-
fixHints: [
|
|
315
|
-
"Add pattern to middleware matcher",
|
|
316
|
-
"Or run 'vibecheck ctx sync' to update contract"
|
|
317
|
-
],
|
|
318
|
-
}));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
256
|
+
const files = await findSourceFiles(projectPath, [".ts", ".js"]);
|
|
321
257
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"Run 'vibecheck ctx sync' to update contract"
|
|
343
|
-
],
|
|
344
|
-
}));
|
|
258
|
+
for (const file of files.slice(0, 50)) {
|
|
259
|
+
try {
|
|
260
|
+
const content = await fs.readFile(file, "utf8");
|
|
261
|
+
const relPath = path.relative(projectPath, file);
|
|
262
|
+
|
|
263
|
+
for (const pattern of authPatterns) {
|
|
264
|
+
let match;
|
|
265
|
+
pattern.lastIndex = 0;
|
|
266
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
267
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
268
|
+
authIndicators.push({
|
|
269
|
+
type: "auth_indicator",
|
|
270
|
+
match: match[0],
|
|
271
|
+
file: relPath,
|
|
272
|
+
line,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
// Skip
|
|
345
278
|
}
|
|
346
279
|
}
|
|
347
280
|
|
|
348
|
-
return
|
|
281
|
+
return {
|
|
282
|
+
count: authIndicators.length,
|
|
283
|
+
indicators: authIndicators.slice(0, 50),
|
|
284
|
+
confidence:
|
|
285
|
+
authIndicators.length > 5
|
|
286
|
+
? 0.8
|
|
287
|
+
: authIndicators.length > 0
|
|
288
|
+
? 0.5
|
|
289
|
+
: 0.1,
|
|
290
|
+
};
|
|
349
291
|
}
|
|
350
292
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
293
|
+
async function extractBilling(projectPath) {
|
|
294
|
+
const billingIndicators = [];
|
|
295
|
+
const billingPatterns = [
|
|
296
|
+
/stripe|paddle|lemonsqueezy|gumroad/gi,
|
|
297
|
+
/subscription|payment|checkout|invoice/gi,
|
|
298
|
+
/isPro|isPremium|isEnterprise|hasPaid/gi,
|
|
299
|
+
/price|tier|plan/gi,
|
|
300
|
+
];
|
|
354
301
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// Skip reporting for common optional env vars that are typically not documented
|
|
376
|
-
const commonOptionalVars = [
|
|
377
|
-
/^DEBUG$/i,
|
|
378
|
-
/^LOG_LEVEL$/i,
|
|
379
|
-
/^NODE_ENV$/i,
|
|
380
|
-
/^CI$/i,
|
|
381
|
-
/^VERCEL/i,
|
|
382
|
-
/^GITHUB_/i,
|
|
383
|
-
/^NEXT_PUBLIC_VERCEL/i,
|
|
384
|
-
];
|
|
385
|
-
|
|
386
|
-
if (commonOptionalVars.some(p => p.test(usage.name))) {
|
|
387
|
-
continue;
|
|
302
|
+
const files = await findSourceFiles(projectPath, [".ts", ".js"]);
|
|
303
|
+
|
|
304
|
+
for (const file of files.slice(0, 30)) {
|
|
305
|
+
try {
|
|
306
|
+
const content = await fs.readFile(file, "utf8");
|
|
307
|
+
const relPath = path.relative(projectPath, file);
|
|
308
|
+
|
|
309
|
+
for (const pattern of billingPatterns) {
|
|
310
|
+
let match;
|
|
311
|
+
pattern.lastIndex = 0;
|
|
312
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
313
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
314
|
+
billingIndicators.push({
|
|
315
|
+
type: "billing_indicator",
|
|
316
|
+
match: match[0],
|
|
317
|
+
file: relPath,
|
|
318
|
+
line,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
388
321
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
detectorId: "ENV_USED_BUT_UNDECLARED",
|
|
392
|
-
severity: isRequired ? "BLOCK" : "WARN",
|
|
393
|
-
category: "Env",
|
|
394
|
-
scope: "server",
|
|
395
|
-
title: `Env var ${usage.name} used but not declared in contract`,
|
|
396
|
-
why: isRequired
|
|
397
|
-
? "Required env var missing from contract. Deployment will fail if not set."
|
|
398
|
-
: "Env var used but not documented. AI won't know about this dependency.",
|
|
399
|
-
confidence: isRequired ? "high" : "medium",
|
|
400
|
-
evidence: usage.locations?.slice(0, 3).map(loc => createEvidence({
|
|
401
|
-
kind: "file",
|
|
402
|
-
reason: `Usage of ${usage.name}`,
|
|
403
|
-
file: loc.file,
|
|
404
|
-
lines: loc.lines,
|
|
405
|
-
snippet: loc.snippetHash,
|
|
406
|
-
})) || [],
|
|
407
|
-
fixHints: [
|
|
408
|
-
"Add to .env.example with appropriate default/placeholder",
|
|
409
|
-
"Run 'vibecheck ctx sync' to update env contract",
|
|
410
|
-
isRequired ? "Ensure this var is set in all environments" : null
|
|
411
|
-
].filter(Boolean),
|
|
412
|
-
}));
|
|
322
|
+
} catch {
|
|
323
|
+
// Skip
|
|
413
324
|
}
|
|
414
325
|
}
|
|
415
326
|
|
|
416
|
-
return
|
|
327
|
+
return {
|
|
328
|
+
count: billingIndicators.length,
|
|
329
|
+
indicators: billingIndicators.slice(0, 30),
|
|
330
|
+
confidence:
|
|
331
|
+
billingIndicators.length > 3
|
|
332
|
+
? 0.7
|
|
333
|
+
: billingIndicators.length > 0
|
|
334
|
+
? 0.4
|
|
335
|
+
: 0.1,
|
|
336
|
+
};
|
|
417
337
|
}
|
|
418
338
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
339
|
+
async function extractEnvVars(projectPath) {
|
|
340
|
+
const declared = [];
|
|
341
|
+
const used = [];
|
|
422
342
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
343
|
+
const envFiles = [".env.example", ".env.local.example", ".env.sample"];
|
|
344
|
+
for (const envFile of envFiles) {
|
|
345
|
+
try {
|
|
346
|
+
const content = await fs.readFile(path.join(projectPath, envFile), "utf8");
|
|
347
|
+
const lines = content.split("\n");
|
|
348
|
+
for (let i = 0; i < lines.length; i++) {
|
|
349
|
+
const match = lines[i].match(/^([A-Z][A-Z0-9_]*)=/);
|
|
350
|
+
if (match) {
|
|
351
|
+
declared.push({
|
|
352
|
+
name: match[1],
|
|
353
|
+
file: envFile,
|
|
354
|
+
line: i + 1,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
// File does not exist
|
|
360
|
+
}
|
|
361
|
+
}
|
|
433
362
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
363
|
+
const files = await findSourceFiles(projectPath, [".ts", ".js"]);
|
|
364
|
+
for (const file of files.slice(0, 30)) {
|
|
365
|
+
try {
|
|
366
|
+
const content = await fs.readFile(file, "utf8");
|
|
367
|
+
const relPath = path.relative(projectPath, file);
|
|
368
|
+
const pattern = /process\.env\.([A-Z][A-Z0-9_]*)/g;
|
|
369
|
+
let match;
|
|
370
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
371
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
372
|
+
used.push({
|
|
373
|
+
name: match[1],
|
|
374
|
+
file: relPath,
|
|
375
|
+
line,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
// Skip
|
|
380
|
+
}
|
|
381
|
+
}
|
|
443
382
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
383
|
+
const declaredNames = new Set(declared.map((d) => d.name));
|
|
384
|
+
const usedNames = new Set(used.map((u) => u.name));
|
|
385
|
+
const undeclared = [...usedNames].filter((name) => !declaredNames.has(name));
|
|
386
|
+
const unused = [...declaredNames].filter((name) => !usedNames.has(name));
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
declared: declared.slice(0, 50),
|
|
390
|
+
used: used.slice(0, 50),
|
|
391
|
+
mismatches: {
|
|
392
|
+
undeclared,
|
|
393
|
+
unused,
|
|
394
|
+
},
|
|
395
|
+
confidence: undeclared.length === 0 ? 0.9 : 0.5,
|
|
396
|
+
};
|
|
452
397
|
}
|
|
453
398
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// =============================================================================
|
|
399
|
+
async function extractSchema(projectPath) {
|
|
400
|
+
const schemas = [];
|
|
457
401
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const noNavigation = action.pageUrl === action.afterPageUrl;
|
|
470
|
-
const noDomChange = action.beforeDomHash === action.afterDomHash;
|
|
471
|
-
const noNetwork = !action.networkRequestIds || action.networkRequestIds.length === 0;
|
|
472
|
-
|
|
473
|
-
if (noNavigation && noDomChange && noNetwork) {
|
|
474
|
-
const isCritical = /save|submit|pay|login|continue|checkout/i.test(action.label || "");
|
|
475
|
-
|
|
476
|
-
findings.push(createFindingV2({
|
|
477
|
-
detectorId: "DEAD_CLICK_NO_EFFECT",
|
|
478
|
-
severity: isCritical ? "BLOCK" : "WARN",
|
|
479
|
-
category: "DeadUI",
|
|
480
|
-
scope: "runtime",
|
|
481
|
-
title: `Click on "${action.label || action.selector}" has no effect`,
|
|
482
|
-
why: "Button/link does nothing - no navigation, no DOM change, no network call. User will think app is broken.",
|
|
483
|
-
confidence: "high",
|
|
484
|
-
evidence: [
|
|
485
|
-
createEvidence({
|
|
486
|
-
kind: "screenshot",
|
|
487
|
-
reason: "Screenshot before click",
|
|
488
|
-
artifactPath: action.screenshotBefore,
|
|
489
|
-
}),
|
|
490
|
-
createEvidence({
|
|
491
|
-
kind: "screenshot",
|
|
492
|
-
reason: "Screenshot after click (unchanged)",
|
|
493
|
-
artifactPath: action.screenshotAfter,
|
|
494
|
-
})
|
|
495
|
-
].filter(e => e.artifactPath),
|
|
496
|
-
fixHints: [
|
|
497
|
-
"Wire click handler to actual functionality",
|
|
498
|
-
"If disabled, add aria-disabled and disabled styling",
|
|
499
|
-
"If feature not ready, remove or hide the element"
|
|
500
|
-
],
|
|
501
|
-
repro: {
|
|
502
|
-
steps: [
|
|
503
|
-
`Navigate to ${action.pageUrl}`,
|
|
504
|
-
`Click on element: ${action.selector || action.label}`,
|
|
505
|
-
"Observe: nothing happens"
|
|
506
|
-
],
|
|
507
|
-
url: action.pageUrl,
|
|
508
|
-
},
|
|
509
|
-
}));
|
|
402
|
+
try {
|
|
403
|
+
const prismaPath = path.join(projectPath, "prisma", "schema.prisma");
|
|
404
|
+
const content = await fs.readFile(prismaPath, "utf8");
|
|
405
|
+
const modelMatches = content.matchAll(/model\s+(\w+)\s*\{/g);
|
|
406
|
+
for (const match of modelMatches) {
|
|
407
|
+
schemas.push({
|
|
408
|
+
type: "prisma_model",
|
|
409
|
+
name: match[1],
|
|
410
|
+
file: "prisma/schema.prisma",
|
|
411
|
+
});
|
|
510
412
|
}
|
|
413
|
+
} catch {
|
|
414
|
+
// No Prisma schema
|
|
511
415
|
}
|
|
512
416
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
for (const reqId of action.networkRequestIds) {
|
|
529
|
-
const req = network.find(r => r.id === reqId);
|
|
530
|
-
if (req && (req.status >= 400 || req.failed)) {
|
|
531
|
-
findings.push(createFindingV2({
|
|
532
|
-
detectorId: "UI_ACTION_CAUSES_4XX_5XX",
|
|
533
|
-
severity: "BLOCK",
|
|
534
|
-
category: "DeadUI",
|
|
535
|
-
scope: "runtime",
|
|
536
|
-
title: `Click on "${action.label || action.selector}" causes ${req.status} error`,
|
|
537
|
-
why: `UI action triggers failed API call (${req.status}). User will see broken functionality.`,
|
|
538
|
-
confidence: "high",
|
|
539
|
-
evidence: [
|
|
540
|
-
createEvidence({
|
|
541
|
-
kind: "request",
|
|
542
|
-
reason: `Failed request triggered by UI action`,
|
|
543
|
-
url: req.url,
|
|
544
|
-
httpStatus: req.status,
|
|
545
|
-
requestId: req.id,
|
|
546
|
-
})
|
|
547
|
-
],
|
|
548
|
-
fixHints: [
|
|
549
|
-
"Fix the API endpoint to return success",
|
|
550
|
-
"Add proper error handling in UI",
|
|
551
|
-
"If endpoint doesn't exist, create it"
|
|
552
|
-
],
|
|
553
|
-
repro: {
|
|
554
|
-
steps: [
|
|
555
|
-
`Navigate to ${action.pageUrl}`,
|
|
556
|
-
`Click on element: ${action.selector || action.label}`,
|
|
557
|
-
`Observe: API returns ${req.status}`
|
|
558
|
-
],
|
|
559
|
-
url: action.pageUrl,
|
|
560
|
-
},
|
|
561
|
-
}));
|
|
417
|
+
const files = await findSourceFiles(projectPath, [".ts", ".tsx"]);
|
|
418
|
+
for (const file of files.slice(0, 20)) {
|
|
419
|
+
try {
|
|
420
|
+
const content = await fs.readFile(file, "utf8");
|
|
421
|
+
const relPath = path.relative(projectPath, file);
|
|
422
|
+
|
|
423
|
+
const typeMatches = content.matchAll(/(?:interface|type)\s+(\w+)/g);
|
|
424
|
+
for (const match of typeMatches) {
|
|
425
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
426
|
+
schemas.push({
|
|
427
|
+
type: "typescript_type",
|
|
428
|
+
name: match[1],
|
|
429
|
+
file: relPath,
|
|
430
|
+
line,
|
|
431
|
+
});
|
|
562
432
|
}
|
|
433
|
+
} catch {
|
|
434
|
+
// Skip
|
|
563
435
|
}
|
|
564
436
|
}
|
|
565
437
|
|
|
566
|
-
return
|
|
438
|
+
return {
|
|
439
|
+
count: schemas.length,
|
|
440
|
+
schemas: schemas.slice(0, 50),
|
|
441
|
+
confidence:
|
|
442
|
+
schemas.length > 5 ? 0.7 : schemas.length > 0 ? 0.4 : 0.2,
|
|
443
|
+
};
|
|
567
444
|
}
|
|
568
445
|
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// CLAIM VERIFICATION
|
|
448
|
+
// ============================================================================
|
|
572
449
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
// This requires checking if routes have proper verification
|
|
582
|
-
// Placeholder - actual implementation would scan the handler files
|
|
583
|
-
|
|
584
|
-
return findings;
|
|
585
|
-
}
|
|
450
|
+
async function verifyClaim(projectPath, claimType, claim) {
|
|
451
|
+
const result = {
|
|
452
|
+
claim: { type: claimType, value: claim },
|
|
453
|
+
verified: false,
|
|
454
|
+
evidence: null,
|
|
455
|
+
confidence: 0,
|
|
456
|
+
rejection: null,
|
|
457
|
+
};
|
|
586
458
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
459
|
+
try {
|
|
460
|
+
switch (claimType) {
|
|
461
|
+
case "file": {
|
|
462
|
+
const filePath = path.join(projectPath, claim);
|
|
463
|
+
try {
|
|
464
|
+
await fs.access(filePath);
|
|
465
|
+
const stats = await fs.stat(filePath);
|
|
466
|
+
result.verified = true;
|
|
467
|
+
result.confidence = 1.0;
|
|
468
|
+
result.evidence = {
|
|
469
|
+
file: claim,
|
|
470
|
+
exists: true,
|
|
471
|
+
size: stats.size,
|
|
472
|
+
verifiedAt: new Date().toISOString(),
|
|
473
|
+
};
|
|
474
|
+
} catch {
|
|
475
|
+
result.rejection = `File does not exist: ${claim}`;
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
590
479
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
480
|
+
case "route":
|
|
481
|
+
case "endpoint": {
|
|
482
|
+
const routes = await extractRoutes(projectPath);
|
|
483
|
+
const matchingRoute = routes.routes.find(
|
|
484
|
+
(route) => route.path === claim || route.path.includes(claim),
|
|
485
|
+
);
|
|
486
|
+
if (matchingRoute) {
|
|
487
|
+
result.verified = true;
|
|
488
|
+
result.confidence = 0.9;
|
|
489
|
+
result.evidence = matchingRoute;
|
|
490
|
+
} else {
|
|
491
|
+
result.rejection = `No route matching "${claim}" found in codebase`;
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
600
495
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
496
|
+
case "env_var": {
|
|
497
|
+
const envData = await extractEnvVars(projectPath);
|
|
498
|
+
const isDeclared = envData.declared.some((env) => env.name === claim);
|
|
499
|
+
const isUsed = envData.used.some((env) => env.name === claim);
|
|
500
|
+
if (isDeclared || isUsed) {
|
|
501
|
+
result.verified = true;
|
|
502
|
+
result.confidence = isDeclared && isUsed ? 1.0 : 0.7;
|
|
503
|
+
result.evidence = {
|
|
504
|
+
declared: isDeclared,
|
|
505
|
+
used: isUsed,
|
|
506
|
+
locations: [
|
|
507
|
+
...envData.declared.filter((env) => env.name === claim),
|
|
508
|
+
...envData.used.filter((env) => env.name === claim),
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
} else {
|
|
512
|
+
result.rejection = `Environment variable "${claim}" not found`;
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
604
516
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
* Trigger: truthpack fingerprint changed but contracts still reflect old fingerprint
|
|
608
|
-
*/
|
|
609
|
-
function detectContractsOutOfDate(truthpack, contracts) {
|
|
610
|
-
const findings = [];
|
|
611
|
-
const truthpackFingerprint = truthpack.fingerprint;
|
|
612
|
-
|
|
613
|
-
for (const [type, contract] of Object.entries(contracts)) {
|
|
614
|
-
if (contract.projectFingerprint && contract.projectFingerprint !== truthpackFingerprint) {
|
|
615
|
-
findings.push(createFindingV2({
|
|
616
|
-
detectorId: "CONTRACTS_OUT_OF_DATE",
|
|
617
|
-
severity: type === "routes" || type === "auth" ? "BLOCK" : "WARN",
|
|
618
|
-
category: "Drift",
|
|
619
|
-
scope: "contracts",
|
|
620
|
-
title: `${type} contract out of date (fingerprint mismatch)`,
|
|
621
|
-
why: `Contract was generated from old codebase. AI agents will use stale information.`,
|
|
622
|
-
confidence: "high",
|
|
623
|
-
evidence: [
|
|
624
|
-
createEvidence({
|
|
625
|
-
kind: "hash",
|
|
626
|
-
reason: "Contract fingerprint",
|
|
627
|
-
file: `.vibecheck/contracts/${type}.json`,
|
|
628
|
-
lines: "1-1",
|
|
629
|
-
})
|
|
630
|
-
],
|
|
631
|
-
fixHints: [
|
|
632
|
-
"Run 'vibecheck ctx sync' to regenerate contracts"
|
|
633
|
-
],
|
|
634
|
-
}));
|
|
517
|
+
default:
|
|
518
|
+
result.rejection = `Claim type "${claimType}" verification is not implemented yet`;
|
|
635
519
|
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
result.rejection = `Verification error: ${error.message}`;
|
|
636
522
|
}
|
|
637
523
|
|
|
638
|
-
return
|
|
524
|
+
return result;
|
|
639
525
|
}
|
|
640
526
|
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// EVIDENCE EXTRACTION
|
|
529
|
+
// ============================================================================
|
|
644
530
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
* - Dynamic segments (:id, [id], [slug], [...slug])
|
|
648
|
-
* - Optional catch-all routes [[...slug]]
|
|
649
|
-
* - Query string stripping
|
|
650
|
-
* - Trailing slash normalization
|
|
651
|
-
*/
|
|
652
|
-
function routeMatches(pattern, actual) {
|
|
653
|
-
// Normalize: strip query strings and trailing slashes
|
|
654
|
-
const normalizedPattern = pattern.split("?")[0].replace(/\/+$/, "") || "/";
|
|
655
|
-
const normalizedActual = actual.split("?")[0].replace(/\/+$/, "") || "/";
|
|
656
|
-
|
|
657
|
-
const patternParts = normalizedPattern.split("/").filter(Boolean);
|
|
658
|
-
const actualParts = normalizedActual.split("/").filter(Boolean);
|
|
659
|
-
|
|
660
|
-
// Handle catch-all routes: [...slug] or [[...slug]]
|
|
661
|
-
const hasCatchAll = patternParts.some(p =>
|
|
662
|
-
p.startsWith("[...") || p.startsWith("[[...")
|
|
663
|
-
);
|
|
664
|
-
|
|
665
|
-
if (hasCatchAll) {
|
|
666
|
-
const catchAllIndex = patternParts.findIndex(p =>
|
|
667
|
-
p.startsWith("[...") || p.startsWith("[[...")
|
|
668
|
-
);
|
|
669
|
-
|
|
670
|
-
// For catch-all, pattern up to catch-all must match
|
|
671
|
-
for (let i = 0; i < catchAllIndex; i++) {
|
|
672
|
-
const p = patternParts[i];
|
|
673
|
-
if (isDynamicSegment(p)) continue;
|
|
674
|
-
if (p !== actualParts[i]) return false;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Catch-all matches any remaining segments (including none for [[...]])
|
|
678
|
-
const isOptional = patternParts[catchAllIndex].startsWith("[[...");
|
|
679
|
-
if (!isOptional && actualParts.length <= catchAllIndex) {
|
|
680
|
-
return false;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
return true;
|
|
684
|
-
}
|
|
531
|
+
async function getEvidence(projectPath, file, options) {
|
|
532
|
+
const filePath = path.join(projectPath, file);
|
|
685
533
|
|
|
686
|
-
|
|
687
|
-
|
|
534
|
+
try {
|
|
535
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
536
|
+
const lines = content.split("\n");
|
|
537
|
+
|
|
538
|
+
let targetLine = options.line || 1;
|
|
539
|
+
const contextLines = options.context_lines || 10;
|
|
540
|
+
|
|
541
|
+
if (options.function_name) {
|
|
542
|
+
const pattern = new RegExp(
|
|
543
|
+
`(function|const|let|var|class)\\s+${options.function_name}`,
|
|
544
|
+
"i",
|
|
545
|
+
);
|
|
546
|
+
for (let i = 0; i < lines.length; i++) {
|
|
547
|
+
if (pattern.test(lines[i])) {
|
|
548
|
+
targetLine = i + 1;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
688
553
|
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
554
|
+
const startLine = Math.max(1, targetLine - contextLines);
|
|
555
|
+
const endLine = Math.min(lines.length, targetLine + contextLines);
|
|
556
|
+
|
|
557
|
+
const snippet = lines
|
|
558
|
+
.slice(startLine - 1, endLine)
|
|
559
|
+
.map(
|
|
560
|
+
(line, index) =>
|
|
561
|
+
`${String(startLine + index).padStart(4, " ")} | ${line}`,
|
|
562
|
+
)
|
|
563
|
+
.join("\n");
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
file,
|
|
567
|
+
targetLine,
|
|
568
|
+
startLine,
|
|
569
|
+
endLine,
|
|
570
|
+
totalLines: lines.length,
|
|
571
|
+
snippet,
|
|
572
|
+
verifiedAt: new Date().toISOString(),
|
|
573
|
+
};
|
|
574
|
+
} catch (error) {
|
|
575
|
+
return {
|
|
576
|
+
error: `Cannot read file: ${error.message}`,
|
|
577
|
+
file,
|
|
578
|
+
};
|
|
693
579
|
}
|
|
694
|
-
return true;
|
|
695
580
|
}
|
|
696
581
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
function isDynamicSegment(segment) {
|
|
701
|
-
return segment.startsWith(":") || // Express-style :id
|
|
702
|
-
segment.startsWith("[") || // Next.js-style [id]
|
|
703
|
-
segment === "*" || // Wildcard
|
|
704
|
-
segment.startsWith("$"); // Remix-style $id
|
|
705
|
-
}
|
|
582
|
+
// ============================================================================
|
|
583
|
+
// UTILITIES
|
|
584
|
+
// ============================================================================
|
|
706
585
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
function
|
|
711
|
-
try {
|
|
712
|
-
// Normalize URL: extract pathname
|
|
713
|
-
let pathname;
|
|
586
|
+
async function findSourceFiles(projectPath, extensions) {
|
|
587
|
+
const files = [];
|
|
588
|
+
|
|
589
|
+
async function walk(dir) {
|
|
714
590
|
try {
|
|
715
|
-
|
|
591
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
592
|
+
for (const entry of entries) {
|
|
593
|
+
const fullPath = path.join(dir, entry.name);
|
|
594
|
+
if (entry.isDirectory()) {
|
|
595
|
+
if (
|
|
596
|
+
!entry.name.startsWith(".") &&
|
|
597
|
+
entry.name !== "node_modules" &&
|
|
598
|
+
entry.name !== "dist" &&
|
|
599
|
+
entry.name !== "build"
|
|
600
|
+
) {
|
|
601
|
+
await walk(fullPath);
|
|
602
|
+
}
|
|
603
|
+
} else if (entry.isFile()) {
|
|
604
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
605
|
+
if (extensions.includes(ext)) {
|
|
606
|
+
files.push(fullPath);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
716
610
|
} catch {
|
|
717
|
-
|
|
611
|
+
// Skip inaccessible directories
|
|
718
612
|
}
|
|
719
|
-
|
|
720
|
-
// Escape special regex chars except * and ?
|
|
721
|
-
const escaped = pattern
|
|
722
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
723
|
-
.replace(/\*\*/g, "{{GLOBSTAR}}")
|
|
724
|
-
.replace(/\*/g, "[^/]*")
|
|
725
|
-
.replace(/\?/g, ".")
|
|
726
|
-
.replace(/{{GLOBSTAR}}/g, ".*");
|
|
727
|
-
|
|
728
|
-
const regex = new RegExp("^" + escaped + "$");
|
|
729
|
-
return regex.test(url) || regex.test(pathname);
|
|
730
|
-
} catch {
|
|
731
|
-
// Fallback to simple comparison
|
|
732
|
-
return pattern === url;
|
|
733
613
|
}
|
|
734
|
-
}
|
|
735
614
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
* Returns { required: boolean, confidence: 'high' | 'medium' | 'low', reason: string }
|
|
739
|
-
*/
|
|
740
|
-
function isLikelyRequired(name) {
|
|
741
|
-
// High-confidence required patterns
|
|
742
|
-
const highConfidenceRequired = [
|
|
743
|
-
/^DATABASE_URL$/i,
|
|
744
|
-
/^NEXTAUTH_SECRET$/i,
|
|
745
|
-
/^NEXTAUTH_URL$/i,
|
|
746
|
-
/^JWT_SECRET$/i,
|
|
747
|
-
/^AUTH_SECRET$/i,
|
|
748
|
-
/^SESSION_SECRET$/i,
|
|
749
|
-
/^ENCRYPTION_KEY$/i,
|
|
750
|
-
/^STRIPE_SECRET_KEY$/i,
|
|
751
|
-
/^STRIPE_WEBHOOK_SECRET$/i,
|
|
752
|
-
/^OPENAI_API_KEY$/i,
|
|
753
|
-
/^ANTHROPIC_API_KEY$/i,
|
|
754
|
-
];
|
|
755
|
-
|
|
756
|
-
// Medium-confidence required patterns
|
|
757
|
-
const mediumConfidenceRequired = [
|
|
758
|
-
/SECRET$/i,
|
|
759
|
-
/TOKEN$/i,
|
|
760
|
-
/API_KEY$/i,
|
|
761
|
-
/PRIVATE_KEY$/i,
|
|
762
|
-
/PASSWORD$/i,
|
|
763
|
-
/CREDENTIALS$/i,
|
|
764
|
-
];
|
|
765
|
-
|
|
766
|
-
// Patterns that indicate optional env vars
|
|
767
|
-
const optionalPatterns = [
|
|
768
|
-
/^DEBUG/i,
|
|
769
|
-
/^LOG_/i,
|
|
770
|
-
/^ENABLE_/i,
|
|
771
|
-
/^DISABLE_/i,
|
|
772
|
-
/^FEATURE_/i,
|
|
773
|
-
/^FLAG_/i,
|
|
774
|
-
/^ANALYTICS/i,
|
|
775
|
-
/^TELEMETRY/i,
|
|
776
|
-
/^SENTRY/i,
|
|
777
|
-
/^PORT$/i,
|
|
778
|
-
/^HOST$/i,
|
|
779
|
-
/^NODE_ENV$/i,
|
|
780
|
-
];
|
|
781
|
-
|
|
782
|
-
// Check optional first (overrides required)
|
|
783
|
-
if (optionalPatterns.some(p => p.test(name))) {
|
|
784
|
-
return false;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Check high-confidence required
|
|
788
|
-
if (highConfidenceRequired.some(p => p.test(name))) {
|
|
789
|
-
return true;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Check medium-confidence required
|
|
793
|
-
if (mediumConfidenceRequired.some(p => p.test(name))) {
|
|
794
|
-
return true;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Check if an env var usage suggests it's optional
|
|
802
|
-
*/
|
|
803
|
-
function hasOptionalUsagePattern(code) {
|
|
804
|
-
const optionalPatterns = [
|
|
805
|
-
/\|\|\s*['"]?undefined['"]?/, // || undefined
|
|
806
|
-
/\?\?\s*['"]?undefined['"]?/, // ?? undefined
|
|
807
|
-
/\|\|\s*null/, // || null
|
|
808
|
-
/\?\?\s*null/, // ?? null
|
|
809
|
-
/\|\|\s*false/, // || false
|
|
810
|
-
/\?\?\s*false/, // ?? false
|
|
811
|
-
/if\s*\(\s*process\.env\./, // Conditional usage
|
|
812
|
-
/process\.env\.\w+\s*\?\s*\./, // Optional chaining
|
|
813
|
-
];
|
|
814
|
-
|
|
815
|
-
return optionalPatterns.some(p => p.test(code));
|
|
615
|
+
await walk(projectPath);
|
|
616
|
+
return files;
|
|
816
617
|
}
|
|
817
618
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
module.exports = {
|
|
823
|
-
// Routes
|
|
824
|
-
detectRouteMissing,
|
|
825
|
-
detectRouteMethodMismatch,
|
|
826
|
-
detectRoutePrefixDrift,
|
|
827
|
-
|
|
828
|
-
// Auth
|
|
829
|
-
detectAuthProtectedAccessibleAnon,
|
|
830
|
-
detectAuthProtectedBlockedWhenAuthed,
|
|
831
|
-
detectAuthContractDrift,
|
|
832
|
-
|
|
833
|
-
// Env
|
|
834
|
-
detectEnvUsedButUndeclared,
|
|
835
|
-
|
|
836
|
-
// Fake Success
|
|
837
|
-
detectFakeSuccessToastBeforeAwait,
|
|
838
|
-
detectFakeSuccessResponseIgnored,
|
|
839
|
-
detectSilentCatch,
|
|
840
|
-
|
|
841
|
-
// Dead UI
|
|
842
|
-
detectDeadClickNoEffect,
|
|
843
|
-
detectUIActionCauses4xx5xx,
|
|
844
|
-
|
|
845
|
-
// Billing
|
|
846
|
-
detectStripeWebhookNoSigVerify,
|
|
847
|
-
|
|
848
|
-
// Entitlements
|
|
849
|
-
detectLocalBypassPaidFeature,
|
|
850
|
-
|
|
851
|
-
// Drift
|
|
852
|
-
detectContractsOutOfDate,
|
|
853
|
-
|
|
854
|
-
// Helpers
|
|
855
|
-
routeMatches,
|
|
856
|
-
matchPattern,
|
|
857
|
-
isLikelyRequired,
|
|
858
|
-
isDynamicSegment,
|
|
859
|
-
hasOptionalUsagePattern,
|
|
619
|
+
export default {
|
|
620
|
+
TRUTH_CONTEXT_TOOLS,
|
|
621
|
+
handleTruthContextTool,
|
|
860
622
|
};
|