@vibecheckai/cli 3.4.0 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/registry.js +154 -338
- package/bin/runners/context/generators/mcp.js +13 -15
- package/bin/runners/context/proof-context.js +1 -248
- package/bin/runners/lib/analysis-core.js +180 -198
- package/bin/runners/lib/analyzers.js +223 -1669
- package/bin/runners/lib/cli-output.js +210 -242
- package/bin/runners/lib/detectors-v2.js +785 -547
- package/bin/runners/lib/entitlements-v2.js +458 -96
- package/bin/runners/lib/error-handler.js +9 -16
- package/bin/runners/lib/global-flags.js +0 -37
- package/bin/runners/lib/route-truth.js +322 -1167
- package/bin/runners/lib/scan-output.js +469 -448
- package/bin/runners/lib/ship-output.js +27 -280
- package/bin/runners/lib/terminal-ui.js +733 -231
- package/bin/runners/lib/truth.js +321 -1004
- package/bin/runners/lib/unified-output.js +158 -162
- package/bin/runners/lib/upsell.js +204 -104
- package/bin/runners/runAllowlist.js +324 -0
- package/bin/runners/runAuth.js +95 -324
- package/bin/runners/runCheckpoint.js +21 -39
- package/bin/runners/runContext.js +24 -136
- package/bin/runners/runDoctor.js +67 -115
- package/bin/runners/runEvidencePack.js +219 -0
- package/bin/runners/runFix.js +5 -6
- package/bin/runners/runGuard.js +118 -212
- package/bin/runners/runInit.js +2 -14
- package/bin/runners/runInstall.js +281 -0
- package/bin/runners/runLabs.js +341 -0
- package/bin/runners/runMcp.js +52 -130
- package/bin/runners/runPolish.js +20 -43
- package/bin/runners/runProve.js +3 -13
- package/bin/runners/runReality.js +0 -14
- package/bin/runners/runReport.js +2 -3
- package/bin/runners/runScan.js +44 -511
- package/bin/runners/runShip.js +14 -28
- package/bin/runners/runValidate.js +2 -19
- package/bin/runners/runWatch.js +54 -118
- package/bin/vibecheck.js +41 -148
- package/mcp-server/ARCHITECTURE.md +339 -0
- package/mcp-server/__tests__/cache.test.ts +313 -0
- package/mcp-server/__tests__/executor.test.ts +239 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +1 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +3 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/package.json +5 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +5 -0
- package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +4 -0
- package/mcp-server/__tests__/ids.test.ts +345 -0
- package/mcp-server/__tests__/integration/tools.test.ts +410 -0
- package/mcp-server/__tests__/registry.test.ts +365 -0
- package/mcp-server/__tests__/sandbox.test.ts +323 -0
- package/mcp-server/__tests__/schemas.test.ts +372 -0
- package/mcp-server/benchmarks/run-benchmarks.ts +304 -0
- package/mcp-server/examples/doctor.request.json +14 -0
- package/mcp-server/examples/doctor.response.json +53 -0
- package/mcp-server/examples/error.response.json +15 -0
- package/mcp-server/examples/scan.request.json +14 -0
- package/mcp-server/examples/scan.response.json +108 -0
- package/mcp-server/handlers/tool-handler.ts +671 -0
- package/mcp-server/index-v3.ts +293 -0
- package/mcp-server/index.js +1072 -1573
- package/mcp-server/index.old.js +4137 -0
- package/mcp-server/lib/cache.ts +341 -0
- package/mcp-server/lib/errors.ts +346 -0
- package/mcp-server/lib/executor.ts +792 -0
- package/mcp-server/lib/ids.ts +238 -0
- package/mcp-server/lib/logger.ts +368 -0
- package/mcp-server/lib/metrics.ts +365 -0
- package/mcp-server/lib/sandbox.ts +337 -0
- package/mcp-server/lib/validator.ts +229 -0
- package/mcp-server/package-lock.json +165 -0
- package/mcp-server/package.json +32 -7
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/registry/tools.json +476 -0
- package/mcp-server/schemas/error-envelope.schema.json +125 -0
- package/mcp-server/schemas/finding.schema.json +167 -0
- package/mcp-server/schemas/report-artifact.schema.json +88 -0
- package/mcp-server/schemas/run-request.schema.json +75 -0
- package/mcp-server/schemas/verdict.schema.json +168 -0
- package/mcp-server/tier-auth.d.ts +71 -0
- package/mcp-server/tier-auth.js +371 -183
- package/mcp-server/truth-context.js +90 -131
- package/mcp-server/truth-firewall-tools.js +1000 -1611
- package/mcp-server/tsconfig.json +34 -0
- package/mcp-server/vibecheck-tools.js +2 -2
- package/mcp-server/vitest.config.ts +16 -0
- package/package.json +3 -4
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +0 -474
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +0 -488
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +0 -228
- package/bin/runners/lib/agent-firewall/change-packet/store.js +0 -200
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +0 -21
- package/bin/runners/lib/agent-firewall/claims/extractor.js +0 -303
- package/bin/runners/lib/agent-firewall/claims/patterns.js +0 -24
- package/bin/runners/lib/agent-firewall/critic/index.js +0 -151
- package/bin/runners/lib/agent-firewall/critic/judge.js +0 -432
- package/bin/runners/lib/agent-firewall/critic/prompts.js +0 -305
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +0 -88
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +0 -75
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +0 -127
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +0 -102
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +0 -213
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +0 -145
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +0 -19
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +0 -87
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +0 -184
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +0 -163
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +0 -107
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +0 -68
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +0 -66
- package/bin/runners/lib/agent-firewall/interceptor/base.js +0 -304
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +0 -35
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +0 -35
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +0 -34
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +0 -465
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +0 -604
- package/bin/runners/lib/agent-firewall/lawbook/index.js +0 -304
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +0 -514
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +0 -420
- package/bin/runners/lib/agent-firewall/logger.js +0 -141
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +0 -90
- package/bin/runners/lib/agent-firewall/policy/engine.js +0 -103
- package/bin/runners/lib/agent-firewall/policy/loader.js +0 -451
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +0 -50
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +0 -50
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +0 -86
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +0 -162
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +0 -189
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +0 -93
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +0 -57
- package/bin/runners/lib/agent-firewall/policy/schema.json +0 -183
- package/bin/runners/lib/agent-firewall/policy/verdict.js +0 -54
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +0 -394
- package/bin/runners/lib/agent-firewall/proposal/index.js +0 -212
- package/bin/runners/lib/agent-firewall/proposal/schema.js +0 -251
- package/bin/runners/lib/agent-firewall/proposal/validator.js +0 -386
- package/bin/runners/lib/agent-firewall/reality/index.js +0 -332
- package/bin/runners/lib/agent-firewall/reality/state.js +0 -625
- package/bin/runners/lib/agent-firewall/reality/watcher.js +0 -322
- package/bin/runners/lib/agent-firewall/risk/index.js +0 -173
- package/bin/runners/lib/agent-firewall/risk/scorer.js +0 -328
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +0 -321
- package/bin/runners/lib/agent-firewall/risk/vectors.js +0 -421
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +0 -472
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +0 -346
- package/bin/runners/lib/agent-firewall/simulator/index.js +0 -181
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +0 -380
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +0 -661
- package/bin/runners/lib/agent-firewall/time-machine/index.js +0 -267
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +0 -436
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +0 -490
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +0 -530
- package/bin/runners/lib/agent-firewall/truthpack/index.js +0 -67
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +0 -137
- package/bin/runners/lib/agent-firewall/unblock/planner.js +0 -337
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +0 -118
- package/bin/runners/lib/api-client.js +0 -269
- package/bin/runners/lib/authority-badge.js +0 -425
- package/bin/runners/lib/engines/accessibility-engine.js +0 -190
- package/bin/runners/lib/engines/api-consistency-engine.js +0 -162
- package/bin/runners/lib/engines/ast-cache.js +0 -99
- package/bin/runners/lib/engines/code-quality-engine.js +0 -255
- package/bin/runners/lib/engines/console-logs-engine.js +0 -115
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +0 -268
- package/bin/runners/lib/engines/dead-code-engine.js +0 -198
- package/bin/runners/lib/engines/deprecated-api-engine.js +0 -226
- package/bin/runners/lib/engines/empty-catch-engine.js +0 -150
- package/bin/runners/lib/engines/file-filter.js +0 -131
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +0 -251
- package/bin/runners/lib/engines/mock-data-engine.js +0 -272
- package/bin/runners/lib/engines/parallel-processor.js +0 -71
- package/bin/runners/lib/engines/performance-issues-engine.js +0 -265
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +0 -243
- package/bin/runners/lib/engines/todo-fixme-engine.js +0 -115
- package/bin/runners/lib/engines/type-aware-engine.js +0 -152
- package/bin/runners/lib/engines/unsafe-regex-engine.js +0 -225
- package/bin/runners/lib/engines/vibecheck-engines/README.md +0 -53
- package/bin/runners/lib/engines/vibecheck-engines/index.js +0 -15
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +0 -139
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
- package/bin/runners/lib/engines/vibecheck-engines/package.json +0 -13
- package/bin/runners/lib/exit-codes.js +0 -275
- package/bin/runners/lib/fingerprint.js +0 -377
- package/bin/runners/lib/help-formatter.js +0 -413
- package/bin/runners/lib/logger.js +0 -38
- package/bin/runners/lib/ship-output-enterprise.js +0 -239
- package/bin/runners/lib/unified-cli-output.js +0 -604
- package/bin/runners/runAgent.d.ts +0 -5
- package/bin/runners/runAgent.js +0 -161
- package/bin/runners/runApprove.js +0 -1200
- package/bin/runners/runClassify.js +0 -859
- package/bin/runners/runContext.d.ts +0 -4
- package/bin/runners/runFirewall.d.ts +0 -5
- package/bin/runners/runFirewall.js +0 -134
- package/bin/runners/runFirewallHook.d.ts +0 -5
- package/bin/runners/runFirewallHook.js +0 -56
- package/bin/runners/runPolish.d.ts +0 -4
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runTruth.d.ts +0 -5
- package/bin/runners/runTruth.js +0 -101
- package/mcp-server/HARDENING_SUMMARY.md +0 -299
- package/mcp-server/agent-firewall-interceptor.js +0 -500
- package/mcp-server/authority-tools.js +0 -569
- package/mcp-server/conductor/conflict-resolver.js +0 -588
- package/mcp-server/conductor/execution-planner.js +0 -544
- package/mcp-server/conductor/index.js +0 -377
- package/mcp-server/conductor/lock-manager.js +0 -615
- package/mcp-server/conductor/request-queue.js +0 -550
- package/mcp-server/conductor/session-manager.js +0 -500
- package/mcp-server/conductor/tools.js +0 -510
- package/mcp-server/lib/api-client.cjs +0 -13
- package/mcp-server/lib/logger.cjs +0 -30
- package/mcp-server/logger.js +0 -173
- package/mcp-server/tools-v3.js +0 -706
- package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
|
@@ -1,622 +1,860 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
2
|
+
* Canonical Detectors v2
|
|
3
|
+
*
|
|
4
|
+
* Implementation of all spec-defined detectors that produce v2-compliant findings.
|
|
5
|
+
* Each detector has a unique ID and produces evidence-backed findings.
|
|
6
|
+
*
|
|
7
|
+
* Detector Categories:
|
|
8
|
+
* - Routes (D_ROUTE_*)
|
|
9
|
+
* - Auth Coverage (D_AUTH_*)
|
|
10
|
+
* - Env (D_ENV_*)
|
|
11
|
+
* - Fake Success / Truth (D_FAKE_SUCCESS_*, D_SILENT_CATCH)
|
|
12
|
+
* - Dead UI (D_DEAD_*, D_UI_*)
|
|
13
|
+
* - Billing/Stripe (D_STRIPE_*)
|
|
14
|
+
* - Entitlements (D_LOCAL_BYPASS_*)
|
|
15
|
+
* - Drift (D_CONTRACTS_*)
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
|
|
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
|
-
}
|
|
18
|
+
"use strict";
|
|
154
19
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
const crypto = require("crypto");
|
|
23
|
+
const { createFindingV2, createEvidence, generateFingerprint } = require("./schema-validator");
|
|
158
24
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
generatedAt: new Date().toISOString(),
|
|
163
|
-
projectPath,
|
|
164
|
-
scope,
|
|
165
|
-
confidence: 0,
|
|
166
|
-
sections: {},
|
|
167
|
-
};
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// B1) Routes Detectors
|
|
27
|
+
// =============================================================================
|
|
168
28
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
29
|
+
/**
|
|
30
|
+
* D_ROUTE_MISSING (BLOCK)
|
|
31
|
+
* Trigger: clientCalls contains /api/x but truthpack routes has no matching endpoint
|
|
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
|
+
}));
|
|
181
70
|
}
|
|
182
|
-
|
|
183
|
-
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return findings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* D_ROUTE_METHOD_MISMATCH (BLOCK/WARN)
|
|
78
|
+
* Trigger: client uses POST but server exposes GET (or vice versa)
|
|
79
|
+
*/
|
|
80
|
+
function detectRouteMethodMismatch(truthpack) {
|
|
81
|
+
const findings = [];
|
|
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
|
+
}));
|
|
184
121
|
}
|
|
122
|
+
}
|
|
185
123
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
124
|
+
return findings;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* D_ROUTE_PREFIX_DRIFT (WARN→BLOCK)
|
|
129
|
+
* Trigger: Fastify registered under /api/v1 but client hits /api/
|
|
130
|
+
*/
|
|
131
|
+
function detectRoutePrefixDrift(truthpack) {
|
|
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++;
|
|
191
149
|
}
|
|
150
|
+
}
|
|
192
151
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
152
|
+
if (mismatchCount > 0) {
|
|
153
|
+
findings.push(createFindingV2({
|
|
154
|
+
detectorId: "ROUTE_PREFIX_DRIFT",
|
|
155
|
+
severity: mismatchCount > 5 ? "BLOCK" : "WARN",
|
|
156
|
+
category: "Routes",
|
|
157
|
+
scope: "client",
|
|
158
|
+
title: `${mismatchCount} client calls don't match Fastify prefixes: ${fastifyPrefixes.join(", ")}`,
|
|
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
|
+
}));
|
|
200
174
|
}
|
|
201
|
-
}
|
|
202
175
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
];
|
|
176
|
+
return findings;
|
|
177
|
+
}
|
|
210
178
|
|
|
211
|
-
|
|
179
|
+
// =============================================================================
|
|
180
|
+
// B2) Auth Coverage Detectors
|
|
181
|
+
// =============================================================================
|
|
212
182
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
183
|
+
/**
|
|
184
|
+
* D_AUTH_PROTECTED_ROUTE_ACCESSIBLE_ANON (BLOCK) - runtime
|
|
185
|
+
* Trigger: --verify-auth ANON pass can access protected route
|
|
186
|
+
*/
|
|
187
|
+
function detectAuthProtectedAccessibleAnon(realityReport, authContract) {
|
|
188
|
+
const findings = [];
|
|
189
|
+
if (!realityReport?.run?.pass === "anon") return findings;
|
|
190
|
+
|
|
191
|
+
const protectedPatterns = authContract?.protectedRoutes || [];
|
|
192
|
+
const anonPages = realityReport.pages || [];
|
|
193
|
+
const anonNetwork = realityReport.network || [];
|
|
194
|
+
|
|
195
|
+
for (const pattern of protectedPatterns) {
|
|
196
|
+
// Check if anon pass successfully accessed a protected route
|
|
197
|
+
const accessed = anonNetwork.filter(req =>
|
|
198
|
+
matchPattern(pattern.pattern, req.url) &&
|
|
199
|
+
req.status >= 200 && req.status < 300
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
for (const req of accessed) {
|
|
203
|
+
if (pattern.expect?.anon === "deny" || pattern.expect?.anon === "redirect") {
|
|
204
|
+
findings.push(createFindingV2({
|
|
205
|
+
detectorId: "AUTH_PROTECTED_ROUTE_ACCESSIBLE_ANON",
|
|
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
|
+
}));
|
|
234
234
|
}
|
|
235
|
-
} catch {
|
|
236
|
-
// Skip unreadable files
|
|
237
235
|
}
|
|
238
236
|
}
|
|
239
237
|
|
|
240
|
-
return
|
|
241
|
-
count: routes.length,
|
|
242
|
-
routes: routes.slice(0, 100),
|
|
243
|
-
confidence: routes.length > 0 ? 0.8 : 0.2,
|
|
244
|
-
};
|
|
238
|
+
return findings;
|
|
245
239
|
}
|
|
246
240
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
for (const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
241
|
+
/**
|
|
242
|
+
* D_AUTH_PROTECTED_ROUTE_BLOCKED_WHEN_AUTHED (BLOCK) - runtime
|
|
243
|
+
* Trigger: AUTH pass still denied/redirected repeatedly
|
|
244
|
+
*/
|
|
245
|
+
function detectAuthProtectedBlockedWhenAuthed(realityReport, authContract) {
|
|
246
|
+
const findings = [];
|
|
247
|
+
if (!realityReport?.run?.pass === "authed") return findings;
|
|
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
|
+
}));
|
|
278
280
|
}
|
|
279
281
|
}
|
|
280
282
|
|
|
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
|
-
};
|
|
283
|
+
return findings;
|
|
291
284
|
}
|
|
292
285
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
];
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
}
|
|
303
321
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
// Patterns in middleware but not in contract (WARN - might be intentional)
|
|
323
|
+
for (const pattern of middlewareMatchers) {
|
|
324
|
+
if (!contractPatterns.has(pattern)) {
|
|
325
|
+
findings.push(createFindingV2({
|
|
326
|
+
detectorId: "AUTH_CONTRACT_DRIFT",
|
|
327
|
+
severity: "WARN",
|
|
328
|
+
category: "Drift",
|
|
329
|
+
scope: "contracts",
|
|
330
|
+
title: `Middleware pattern "${pattern}" not declared in auth contract`,
|
|
331
|
+
why: "Middleware protects a pattern not in contract. AI agents won't know about this protection.",
|
|
332
|
+
confidence: "medium",
|
|
333
|
+
evidence: [
|
|
334
|
+
createEvidence({
|
|
335
|
+
kind: "file",
|
|
336
|
+
reason: "Middleware file",
|
|
337
|
+
file: truthpack.stack?.next?.middlewareFile || "middleware.ts",
|
|
338
|
+
lines: "1-1",
|
|
339
|
+
})
|
|
340
|
+
],
|
|
341
|
+
fixHints: [
|
|
342
|
+
"Run 'vibecheck ctx sync' to update contract"
|
|
343
|
+
],
|
|
344
|
+
}));
|
|
324
345
|
}
|
|
325
346
|
}
|
|
326
347
|
|
|
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
|
-
};
|
|
348
|
+
return findings;
|
|
337
349
|
}
|
|
338
350
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
351
|
+
// =============================================================================
|
|
352
|
+
// B3) Env Detectors
|
|
353
|
+
// =============================================================================
|
|
342
354
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
/**
|
|
356
|
+
* D_ENV_USED_BUT_UNDECLARED (WARN→BLOCK if required)
|
|
357
|
+
* Trigger: truthpack envUsage includes FOO but contracts/env.json does not
|
|
358
|
+
* Enhanced with better required/optional detection
|
|
359
|
+
*/
|
|
360
|
+
function detectEnvUsedButUndeclared(truthpack, envContract) {
|
|
361
|
+
const findings = [];
|
|
362
|
+
const envUsage = truthpack.envUsage || [];
|
|
363
|
+
const declaredVars = new Set(envContract?.vars?.map(v => v.name) || []);
|
|
364
|
+
|
|
365
|
+
for (const usage of envUsage) {
|
|
366
|
+
if (!declaredVars.has(usage.name)) {
|
|
367
|
+
// Check if usage pattern suggests optional
|
|
368
|
+
const usageCode = usage.locations?.[0]?.snippet || "";
|
|
369
|
+
const hasOptionalPattern = hasOptionalUsagePattern(usageCode);
|
|
370
|
+
|
|
371
|
+
// Determine if required based on name patterns and usage
|
|
372
|
+
const isRequired = !hasOptionalPattern &&
|
|
373
|
+
(usage.inferredRequiredness === "required" || isLikelyRequired(usage.name));
|
|
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;
|
|
357
388
|
}
|
|
358
|
-
|
|
359
|
-
|
|
389
|
+
|
|
390
|
+
findings.push(createFindingV2({
|
|
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
|
+
}));
|
|
360
413
|
}
|
|
361
414
|
}
|
|
362
415
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
}
|
|
416
|
+
return findings;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// =============================================================================
|
|
420
|
+
// B4) Fake Success / Truth Detectors
|
|
421
|
+
// =============================================================================
|
|
382
422
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
undeclared,
|
|
393
|
-
unused,
|
|
394
|
-
},
|
|
395
|
-
confidence: undeclared.length === 0 ? 0.9 : 0.5,
|
|
396
|
-
};
|
|
423
|
+
/**
|
|
424
|
+
* D_FAKE_SUCCESS_TOAST_BEFORE_AWAIT (BLOCK/WARN)
|
|
425
|
+
* Trigger: toast.success before awaited network call
|
|
426
|
+
*/
|
|
427
|
+
function detectFakeSuccessToastBeforeAwait(projectPath) {
|
|
428
|
+
const findings = [];
|
|
429
|
+
// This requires AST analysis - see existing findFakeSuccess in analyzers.js
|
|
430
|
+
// Placeholder for integration
|
|
431
|
+
return findings;
|
|
397
432
|
}
|
|
398
433
|
|
|
399
|
-
|
|
400
|
-
|
|
434
|
+
/**
|
|
435
|
+
* D_FAKE_SUCCESS_RESPONSE_OK_IGNORED (BLOCK)
|
|
436
|
+
* Trigger: fetch result not checked before success UI
|
|
437
|
+
*/
|
|
438
|
+
function detectFakeSuccessResponseIgnored(projectPath) {
|
|
439
|
+
const findings = [];
|
|
440
|
+
// This requires AST analysis
|
|
441
|
+
return findings;
|
|
442
|
+
}
|
|
401
443
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
444
|
+
/**
|
|
445
|
+
* D_SILENT_CATCH (WARN→BLOCK)
|
|
446
|
+
* Trigger: catch (e) {} OR catch { return null } + UI success continues
|
|
447
|
+
*/
|
|
448
|
+
function detectSilentCatch(projectPath) {
|
|
449
|
+
const findings = [];
|
|
450
|
+
// This requires AST analysis
|
|
451
|
+
return findings;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// =============================================================================
|
|
455
|
+
// B5) Dead UI Detectors (runtime-first)
|
|
456
|
+
// =============================================================================
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* D_DEAD_CLICK_NO_EFFECT (BLOCK/WARN)
|
|
460
|
+
* Trigger: action click yields no navigation, no DOM change, no network, no console
|
|
461
|
+
*/
|
|
462
|
+
function detectDeadClickNoEffect(realityReport) {
|
|
463
|
+
const findings = [];
|
|
464
|
+
const actions = realityReport?.actions || [];
|
|
465
|
+
|
|
466
|
+
for (const action of actions) {
|
|
467
|
+
if (action.type !== "click") continue;
|
|
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
|
+
}));
|
|
412
510
|
}
|
|
413
|
-
} catch {
|
|
414
|
-
// No Prisma schema
|
|
415
511
|
}
|
|
416
512
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
513
|
+
return findings;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* D_UI_ACTION_CAUSES_4XX_5XX (BLOCK)
|
|
518
|
+
* Trigger: click leads to request with 4xx/5xx
|
|
519
|
+
*/
|
|
520
|
+
function detectUIActionCauses4xx5xx(realityReport) {
|
|
521
|
+
const findings = [];
|
|
522
|
+
const actions = realityReport?.actions || [];
|
|
523
|
+
const network = realityReport?.network || [];
|
|
524
|
+
|
|
525
|
+
for (const action of actions) {
|
|
526
|
+
if (!action.networkRequestIds) continue;
|
|
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
|
+
}));
|
|
432
562
|
}
|
|
433
|
-
} catch {
|
|
434
|
-
// Skip
|
|
435
563
|
}
|
|
436
564
|
}
|
|
437
565
|
|
|
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
|
-
};
|
|
566
|
+
return findings;
|
|
444
567
|
}
|
|
445
568
|
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
569
|
+
// =============================================================================
|
|
570
|
+
// B6) Billing/Stripe Detectors
|
|
571
|
+
// =============================================================================
|
|
449
572
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
573
|
+
/**
|
|
574
|
+
* D_STRIPE_WEBHOOK_NO_SIGNATURE_VERIFY (BLOCK)
|
|
575
|
+
* Trigger: webhook route handler lacks signature verification
|
|
576
|
+
*/
|
|
577
|
+
function detectStripeWebhookNoSigVerify(truthpack) {
|
|
578
|
+
const findings = [];
|
|
579
|
+
const webhookRoutes = truthpack.externals?.stripe?.webhookRoutes || [];
|
|
580
|
+
|
|
581
|
+
// This requires checking if routes have proper verification
|
|
582
|
+
// Placeholder - actual implementation would scan the handler files
|
|
583
|
+
|
|
584
|
+
return findings;
|
|
585
|
+
}
|
|
458
586
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
}
|
|
587
|
+
// =============================================================================
|
|
588
|
+
// B7) Entitlements Detectors
|
|
589
|
+
// =============================================================================
|
|
479
590
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
result.evidence = matchingRoute;
|
|
490
|
-
} else {
|
|
491
|
-
result.rejection = `No route matching "${claim}" found in codebase`;
|
|
492
|
-
}
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
591
|
+
/**
|
|
592
|
+
* D_LOCAL_BYPASS_PAID_FEATURE (BLOCK)
|
|
593
|
+
* Trigger: code checks env var like OWNER_MODE=true to unlock features
|
|
594
|
+
*/
|
|
595
|
+
function detectLocalBypassPaidFeature(projectPath) {
|
|
596
|
+
const findings = [];
|
|
597
|
+
// This requires scanning for patterns like OWNER_MODE, SKIP_AUTH, etc.
|
|
598
|
+
return findings;
|
|
599
|
+
}
|
|
495
600
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
}
|
|
601
|
+
// =============================================================================
|
|
602
|
+
// B8) Drift Detectors
|
|
603
|
+
// =============================================================================
|
|
516
604
|
|
|
517
|
-
|
|
518
|
-
|
|
605
|
+
/**
|
|
606
|
+
* D_CONTRACTS_OUT_OF_DATE (WARN/BLOCK)
|
|
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
|
+
}));
|
|
519
635
|
}
|
|
520
|
-
} catch (error) {
|
|
521
|
-
result.rejection = `Verification error: ${error.message}`;
|
|
522
636
|
}
|
|
523
637
|
|
|
524
|
-
return
|
|
638
|
+
return findings;
|
|
525
639
|
}
|
|
526
640
|
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
641
|
+
// =============================================================================
|
|
642
|
+
// Helpers
|
|
643
|
+
// =============================================================================
|
|
530
644
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
645
|
+
/**
|
|
646
|
+
* Enhanced route matching with support for:
|
|
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;
|
|
552
681
|
}
|
|
682
|
+
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Standard matching: lengths must match
|
|
687
|
+
if (patternParts.length !== actualParts.length) return false;
|
|
553
688
|
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
};
|
|
689
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
690
|
+
const p = patternParts[i];
|
|
691
|
+
if (isDynamicSegment(p)) continue;
|
|
692
|
+
if (p !== actualParts[i]) return false;
|
|
579
693
|
}
|
|
694
|
+
return true;
|
|
580
695
|
}
|
|
581
696
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
697
|
+
/**
|
|
698
|
+
* Check if a route segment is dynamic
|
|
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
|
+
}
|
|
588
706
|
|
|
589
|
-
|
|
707
|
+
/**
|
|
708
|
+
* Enhanced glob pattern matching with proper escaping
|
|
709
|
+
*/
|
|
710
|
+
function matchPattern(pattern, url) {
|
|
711
|
+
try {
|
|
712
|
+
// Normalize URL: extract pathname
|
|
713
|
+
let pathname;
|
|
590
714
|
try {
|
|
591
|
-
|
|
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
|
-
}
|
|
715
|
+
pathname = new URL(url, "http://localhost").pathname;
|
|
610
716
|
} catch {
|
|
611
|
-
|
|
717
|
+
pathname = url.split("?")[0];
|
|
612
718
|
}
|
|
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;
|
|
613
733
|
}
|
|
734
|
+
}
|
|
614
735
|
|
|
615
|
-
|
|
616
|
-
|
|
736
|
+
/**
|
|
737
|
+
* Enhanced env var requirement detection
|
|
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));
|
|
617
816
|
}
|
|
618
817
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
818
|
+
// =============================================================================
|
|
819
|
+
// Exports
|
|
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,
|
|
622
860
|
};
|