vibepro 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- package/src/workspace.js +126 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const ROUTE_FILE_PATTERN = /^(?:src\/)?app\/api\/(.+)\/route\.(js|jsx|ts|tsx)$/;
|
|
5
|
+
const MIDDLEWARE_FILES = ['middleware.ts', 'middleware.js', 'src/middleware.ts', 'src/middleware.js'];
|
|
6
|
+
|
|
7
|
+
export async function scanApiBoundary(repoRoot, architectureProfile) {
|
|
8
|
+
const root = path.resolve(repoRoot);
|
|
9
|
+
const entrypoints = architectureProfile?.views?.runtime?.entrypoints ?? [];
|
|
10
|
+
const apiRouteFiles = entrypoints.filter((file) => ROUTE_FILE_PATTERN.test(file));
|
|
11
|
+
const middleware = await readMiddlewareMatchers(root, architectureProfile);
|
|
12
|
+
const routes = [];
|
|
13
|
+
|
|
14
|
+
for (const file of apiRouteFiles) {
|
|
15
|
+
const routePath = routePathFromFile(file);
|
|
16
|
+
const content = await readTextIfExists(path.join(root, file));
|
|
17
|
+
const classification = classifyRoute(routePath);
|
|
18
|
+
const protection = await classifyProtection({ repoRoot: root, file, routePath, classification, middleware, content });
|
|
19
|
+
const riskHints = collectRiskHints({ routePath, classification, protection, content });
|
|
20
|
+
routes.push({
|
|
21
|
+
file,
|
|
22
|
+
route_path: routePath,
|
|
23
|
+
classification,
|
|
24
|
+
methods: detectMethods(content),
|
|
25
|
+
protection,
|
|
26
|
+
risk_hints: riskHints
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
route_count: routes.length,
|
|
32
|
+
middleware,
|
|
33
|
+
routes,
|
|
34
|
+
summary: summarizeRoutes(routes),
|
|
35
|
+
protection_summary: summarizeProtection(routes)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function routePathFromFile(file) {
|
|
40
|
+
const match = ROUTE_FILE_PATTERN.exec(file);
|
|
41
|
+
if (!match) return null;
|
|
42
|
+
return `/api/${match[1]}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function classifyRoute(routePath) {
|
|
46
|
+
const normalized = routePath.toLowerCase();
|
|
47
|
+
if (normalized.includes('/admin')) return 'admin';
|
|
48
|
+
if (normalized.includes('/internal')) return 'internal';
|
|
49
|
+
if (normalized.includes('/webhook')) return 'webhook';
|
|
50
|
+
if (normalized.includes('/debug') || normalized.includes('/test')) return 'debug';
|
|
51
|
+
if (normalized.includes('/cron') || normalized.includes('/batch') || normalized.includes('/queue')) {
|
|
52
|
+
return 'cron_batch_queue';
|
|
53
|
+
}
|
|
54
|
+
if (normalized.includes('/auth')) return 'auth';
|
|
55
|
+
return 'public';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function classifyProtection({ repoRoot, file, routePath, classification, middleware, content }) {
|
|
59
|
+
const evidence = [];
|
|
60
|
+
const code = stripComments(content);
|
|
61
|
+
if (middleware.matchers.some((matcher) => routeMatchesMatcher(routePath, matcher))) {
|
|
62
|
+
evidence.push('middleware_matcher');
|
|
63
|
+
}
|
|
64
|
+
if (hasRouteAuthReference(code)) {
|
|
65
|
+
evidence.push('route_auth_reference');
|
|
66
|
+
}
|
|
67
|
+
if (await hasImportedAuthHelperReference({ repoRoot, file, code })) {
|
|
68
|
+
evidence.push('route_auth_reference');
|
|
69
|
+
evidence.push('imported_auth_helper');
|
|
70
|
+
}
|
|
71
|
+
if (classification === 'debug' && hasDebugAccessGate(code)) {
|
|
72
|
+
evidence.push('debug_access_gate');
|
|
73
|
+
}
|
|
74
|
+
if (classification === 'debug' && await hasImportedDebugAccessGateHelperReference({ repoRoot, file, code })) {
|
|
75
|
+
evidence.push('debug_access_gate');
|
|
76
|
+
evidence.push('imported_debug_gate_helper');
|
|
77
|
+
}
|
|
78
|
+
if (classification === 'webhook' && hasWebhookSignatureCheck(code)) {
|
|
79
|
+
evidence.push('webhook_signature_check');
|
|
80
|
+
}
|
|
81
|
+
if (classification === 'webhook' && await hasImportedWebhookSignatureHelperReference({ repoRoot, file, code })) {
|
|
82
|
+
evidence.push('webhook_signature_check');
|
|
83
|
+
evidence.push('imported_signature_helper');
|
|
84
|
+
evidence.push('imported_webhook_signature_helper');
|
|
85
|
+
}
|
|
86
|
+
if (middleware.matchers.some((matcher) => matcherExcludesApi(matcher))) {
|
|
87
|
+
evidence.push('middleware_excludes_api');
|
|
88
|
+
}
|
|
89
|
+
const uniqueEvidence = [...new Set(evidence)];
|
|
90
|
+
return {
|
|
91
|
+
status: resolveProtectionStatus(uniqueEvidence, middleware),
|
|
92
|
+
evidence: uniqueEvidence
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function collectRiskHints({ classification, protection, content }) {
|
|
97
|
+
const hints = [];
|
|
98
|
+
if (['admin', 'internal', 'cron_batch_queue'].includes(classification) && !isProtectedStatus(protection.status)) {
|
|
99
|
+
hints.push('privileged_route_unprotected');
|
|
100
|
+
}
|
|
101
|
+
if (classification === 'debug' && !isProtectedStatus(protection.status)) {
|
|
102
|
+
hints.push('debug_route_exposed');
|
|
103
|
+
}
|
|
104
|
+
if (classification === 'webhook' && !protection.evidence.includes('webhook_signature_check')) {
|
|
105
|
+
hints.push('webhook_signature_not_detected');
|
|
106
|
+
}
|
|
107
|
+
return hints;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveProtectionStatus(evidence, middleware) {
|
|
111
|
+
if (evidence.includes('webhook_signature_check')) return 'protected_by_route';
|
|
112
|
+
if (evidence.includes('debug_access_gate')) return 'protected_by_route';
|
|
113
|
+
if (evidence.includes('route_auth_reference')) return 'protected_by_route';
|
|
114
|
+
if (evidence.includes('middleware_matcher')) return 'protected_by_middleware';
|
|
115
|
+
if (evidence.includes('middleware_excludes_api')) return 'excluded_by_middleware';
|
|
116
|
+
if (middleware.matchers.some((matcher) => isComplexMatcher(matcher))) return 'unknown';
|
|
117
|
+
return 'unprotected';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isProtectedStatus(status) {
|
|
121
|
+
return status === 'protected'
|
|
122
|
+
|| status === 'protected_by_route'
|
|
123
|
+
|| status === 'protected_by_middleware';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function detectMethods(content) {
|
|
127
|
+
const methods = [];
|
|
128
|
+
for (const method of ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) {
|
|
129
|
+
if (new RegExp(`export\\s+async\\s+function\\s+${method}\\b`).test(content)
|
|
130
|
+
|| new RegExp(`export\\s+function\\s+${method}\\b`).test(content)) {
|
|
131
|
+
methods.push(method);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return methods;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function readMiddlewareMatchers(root, architectureProfile) {
|
|
138
|
+
const middlewareFiles = architectureProfile?.views?.security?.auth_boundaries
|
|
139
|
+
?.filter((boundary) => boundary.type === 'middleware')
|
|
140
|
+
.map((boundary) => boundary.file)
|
|
141
|
+
?? [];
|
|
142
|
+
const files = middlewareFiles.length > 0 ? middlewareFiles : MIDDLEWARE_FILES;
|
|
143
|
+
const matchers = [];
|
|
144
|
+
const evidence = [];
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const content = await readTextIfExists(path.join(root, file));
|
|
147
|
+
if (!content) continue;
|
|
148
|
+
const fileMatchers = extractMatchers(content);
|
|
149
|
+
matchers.push(...fileMatchers);
|
|
150
|
+
evidence.push({ file, matchers: fileMatchers });
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
matchers: [...new Set(matchers)],
|
|
154
|
+
evidence
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractMatchers(content) {
|
|
159
|
+
const matchers = [];
|
|
160
|
+
const matcherBlock = /matcher\s*:\s*\[([\s\S]*?)\]/m.exec(content);
|
|
161
|
+
if (matcherBlock) {
|
|
162
|
+
for (const match of matcherBlock[1].matchAll(/['"`]([^'"`]+)['"`]/g)) {
|
|
163
|
+
matchers.push(match[1]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const singleMatcher = /matcher\s*:\s*['"`]([^'"`]+)['"`]/m.exec(content);
|
|
167
|
+
if (singleMatcher) matchers.push(singleMatcher[1]);
|
|
168
|
+
return matchers;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function routeMatchesMatcher(routePath, matcher) {
|
|
172
|
+
if (isComplexMatcher(matcher)) return false;
|
|
173
|
+
const normalized = matcher
|
|
174
|
+
.replace(/\/:path\*$/, '')
|
|
175
|
+
.replace(/\/$/, '');
|
|
176
|
+
if (!normalized) return false;
|
|
177
|
+
return routePath === normalized || routePath.startsWith(`${normalized}/`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasWebhookSignatureCheck(content) {
|
|
181
|
+
const readsSignatureHeader = /headers\s*\(\s*\)\.get\s*\(\s*['"`][^'"`]*signature[^'"`]*['"`]\s*\)/i.test(content)
|
|
182
|
+
|| /\.headers\.get\s*\(\s*['"`][^'"`]*signature[^'"`]*['"`]\s*\)/i.test(content)
|
|
183
|
+
|| /\bstripe-signature\b/i.test(content);
|
|
184
|
+
const verifiesSignature = /\b(constructEvent|webhooks\.verify|verifyWebhook|verifySignature|verify)\s*\(/i.test(content);
|
|
185
|
+
const readsWebhookTokenHeader = /\.headers\.get\s*\([^)]*(webhookHeaderName|authorization|token|signature)[^)]*\)/i.test(content)
|
|
186
|
+
|| /\bheaders\s*\(\s*\)\.get\s*\([^)]*(webhookHeaderName|authorization|token|signature)[^)]*\)/i.test(content);
|
|
187
|
+
const hasWebhookTokenSecret = /\b(expectedWebhookToken|webhookAuthToken|webhookSecret|WEBHOOK[A-Z0-9_]*(TOKEN|SECRET|AUTH)|resolve[A-Za-z0-9_]*Webhook[A-Za-z0-9_]*Token)\b/.test(content);
|
|
188
|
+
const verifiesWebhookToken = /\b(verify[A-Za-z0-9_]*(Webhook|Signature|Token)[A-Za-z0-9_]*|timingSafeEqual|safeSignatureEquals)\s*\(/.test(content);
|
|
189
|
+
const hasProviderWebhookVerification = /\b(validateRequest|validateRequestWithBody|constructEvent|unwrap|verifySignature|verifyWebhook|verifyWebhookSignature|verify[A-Za-z0-9_]*Webhook[A-Za-z0-9_]*)\s*\(/i.test(content)
|
|
190
|
+
|| /\bwebhooks\.(constructEvent|unwrap|verifySignature|verify)\s*\(/i.test(content);
|
|
191
|
+
const hasSignatureCrypto = /\b(createHmac|timingSafeEqual)\s*\(/i.test(content);
|
|
192
|
+
const hasSignatureSecret = /\bprocess\.env\.[A-Z0-9_]*(WEBHOOK|SIGNATURE|SECRET|TOKEN|AUTH)[A-Z0-9_]*\b/i.test(content)
|
|
193
|
+
|| /\b[A-Za-z0-9_]*(webhook|signature)[A-Za-z0-9_]*(Secret|Token|Key)\b/i.test(content);
|
|
194
|
+
return (readsSignatureHeader && verifiesSignature)
|
|
195
|
+
|| (readsWebhookTokenHeader && hasWebhookTokenSecret && verifiesWebhookToken)
|
|
196
|
+
|| (hasSignatureSecret && (hasProviderWebhookVerification || hasSignatureCrypto));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function hasRouteAuthReference(content) {
|
|
200
|
+
return /\b(getServerSession|requireAuth|currentUser|getSession|auth\.api\.getSession|validateSession)\s*\(/i.test(content)
|
|
201
|
+
|| /\bcookies\s*\(\s*\)\.get\s*\(\s*['"`][^'"`]*(session|token)[^'"`]*['"`]\s*\)/i.test(content)
|
|
202
|
+
|| /\.cookies\.get\s*\(\s*['"`][^'"`]*(session|token)[^'"`]*['"`]\s*\)/i.test(content)
|
|
203
|
+
|| hasAuthorizationHeaderGuard(content);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasDebugAccessGate(content) {
|
|
207
|
+
const hasDebugEnvGate = /\bprocess\.env\.[A-Z0-9_]*(DEBUG|TEST|INTERNAL)[A-Z0-9_]*(ENABLED|TOKEN|SECRET|KEY)?\b/i.test(content)
|
|
208
|
+
|| /\b(NODE_ENV|VERCEL_ENV)\b[\s\S]{0,80}\bproduction\b/i.test(content);
|
|
209
|
+
if (!hasDebugEnvGate) return false;
|
|
210
|
+
|
|
211
|
+
const hasDenyPath = /\b(status\s*:\s*(401|403|404)|notFound\s*\(|Unauthorized|Forbidden|disabled|forbidden|unauthorized)\b/i.test(content);
|
|
212
|
+
const hasCallerBoundary = /\b(auth|getServerSession|currentUser|getSession|validateSession)\s*\(/i.test(content)
|
|
213
|
+
|| /\bsession\?\.user\b|\bsession\.user\b|\buserType\b|\brole\b|\badmin\b/i.test(content)
|
|
214
|
+
|| /\bNODE_ENV\b|\bVERCEL_ENV\b/i.test(content);
|
|
215
|
+
return hasDenyPath && hasCallerBoundary;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function hasAuthorizationHeaderGuard(content) {
|
|
219
|
+
const readsAuthorizationHeader = /\.headers\.get\s*\(\s*['"`]authorization['"`]\s*\)/i.test(content)
|
|
220
|
+
|| /\bheaders\s*\(\s*\)\.get\s*\(\s*['"`]authorization['"`]\s*\)/i.test(content);
|
|
221
|
+
if (!readsAuthorizationHeader) return false;
|
|
222
|
+
const hasSecretSource = /\bprocess\.env\.[A-Z0-9_]*(API_KEY|TOKEN|SECRET|AUTH)[A-Z0-9_]*\b/.test(content);
|
|
223
|
+
const checksBearer = /\bBearer\b/i.test(content);
|
|
224
|
+
return hasSecretSource || checksBearer;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function hasImportedAuthHelperReference({ repoRoot, file, code }) {
|
|
228
|
+
return hasImportedAuthHelperReferenceRecursive({
|
|
229
|
+
repoRoot,
|
|
230
|
+
file,
|
|
231
|
+
code,
|
|
232
|
+
visited: new Set([normalizeRelativeFile(file)]),
|
|
233
|
+
depth: 0
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function hasImportedWebhookSignatureHelperReference({ repoRoot, file, code }) {
|
|
238
|
+
return hasImportedWebhookSignatureHelperReferenceRecursive({
|
|
239
|
+
repoRoot,
|
|
240
|
+
file,
|
|
241
|
+
code,
|
|
242
|
+
visited: new Set([normalizeRelativeFile(file)]),
|
|
243
|
+
depth: 0
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function hasImportedDebugAccessGateHelperReference({ repoRoot, file, code }) {
|
|
248
|
+
return hasImportedDebugAccessGateHelperReferenceRecursive({
|
|
249
|
+
repoRoot,
|
|
250
|
+
file,
|
|
251
|
+
code,
|
|
252
|
+
visited: new Set([normalizeRelativeFile(file)]),
|
|
253
|
+
depth: 0
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function hasImportedDebugAccessGateHelperReferenceRecursive({ repoRoot, file, code, visited, depth }) {
|
|
258
|
+
if (depth >= 4) return false;
|
|
259
|
+
const imports = extractLocalImports(code);
|
|
260
|
+
for (const item of imports) {
|
|
261
|
+
if (!item.specifiers.some((name) => new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`).test(code))) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const importedModule = await readImportedModule(repoRoot, file, item.source);
|
|
265
|
+
if (!importedModule.content) continue;
|
|
266
|
+
if (visited.has(importedModule.file)) continue;
|
|
267
|
+
visited.add(importedModule.file);
|
|
268
|
+
|
|
269
|
+
const importedCode = stripComments(importedModule.content);
|
|
270
|
+
if (hasDebugAccessGate(importedCode)) return true;
|
|
271
|
+
if (await hasImportedDebugAccessGateHelperReferenceRecursive({
|
|
272
|
+
repoRoot,
|
|
273
|
+
file: importedModule.file,
|
|
274
|
+
code: importedCode,
|
|
275
|
+
visited,
|
|
276
|
+
depth: depth + 1
|
|
277
|
+
})) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function hasImportedWebhookSignatureHelperReferenceRecursive({ repoRoot, file, code, visited, depth }) {
|
|
285
|
+
if (depth >= 4) return false;
|
|
286
|
+
const imports = extractLocalImports(code);
|
|
287
|
+
for (const item of imports) {
|
|
288
|
+
const calledSpecifiers = item.specifiers.filter((name) => new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`).test(code));
|
|
289
|
+
if (calledSpecifiers.length === 0) continue;
|
|
290
|
+
|
|
291
|
+
const importedModule = await readImportedModule(repoRoot, file, item.source);
|
|
292
|
+
if (!importedModule.content) continue;
|
|
293
|
+
if (visited.has(importedModule.file)) continue;
|
|
294
|
+
visited.add(importedModule.file);
|
|
295
|
+
|
|
296
|
+
const importedCode = stripComments(importedModule.content);
|
|
297
|
+
if (calledSpecifiers.some((name) => isLikelyWebhookSignatureHelperName(name))
|
|
298
|
+
&& hasWebhookSignatureVerificationSignal(importedCode)) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
if (hasWebhookSignatureCheck(importedCode)) return true;
|
|
302
|
+
if (await hasImportedWebhookSignatureHelperReferenceRecursive({
|
|
303
|
+
repoRoot,
|
|
304
|
+
file: importedModule.file,
|
|
305
|
+
code: importedCode,
|
|
306
|
+
visited,
|
|
307
|
+
depth: depth + 1
|
|
308
|
+
})) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function hasImportedAuthHelperReferenceRecursive({ repoRoot, file, code, visited, depth }) {
|
|
316
|
+
if (depth >= 4) return false;
|
|
317
|
+
const imports = extractLocalImports(code);
|
|
318
|
+
for (const item of imports) {
|
|
319
|
+
if (!item.specifiers.some((name) => new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`).test(code))) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const importedModule = await readImportedModule(repoRoot, file, item.source);
|
|
323
|
+
if (!importedModule.content) continue;
|
|
324
|
+
if (visited.has(importedModule.file)) continue;
|
|
325
|
+
visited.add(importedModule.file);
|
|
326
|
+
|
|
327
|
+
const importedCode = stripComments(importedModule.content);
|
|
328
|
+
if (hasRouteAuthReference(importedCode)) return true;
|
|
329
|
+
if (await hasImportedAuthHelperReferenceRecursive({
|
|
330
|
+
repoRoot,
|
|
331
|
+
file: importedModule.file,
|
|
332
|
+
code: importedCode,
|
|
333
|
+
visited,
|
|
334
|
+
depth: depth + 1
|
|
335
|
+
})) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function isLikelyWebhookSignatureHelperName(name) {
|
|
343
|
+
return /\b(verify|validate|authenticate|authorize)[A-Za-z0-9_$]*(Webhook|Signature)[A-Za-z0-9_$]*\b/i.test(name)
|
|
344
|
+
|| /\b(Webhook|Signature)[A-Za-z0-9_$]*(verify|validate|authenticate|authorize)[A-Za-z0-9_$]*\b/i.test(name);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function hasWebhookSignatureVerificationSignal(content) {
|
|
348
|
+
return /\b(validateRequest|validateRequestWithBody|verifySignature|webhooks\.(verify|unwrap)|constructEvent|Webhook\s*\(|timingSafeEqual|safeSignatureEquals)\b/i.test(content)
|
|
349
|
+
|| /\b(x-twilio-signature|stripe-signature|svix-signature|OPENAI_WEBHOOK_SECRET|TWILIO_AUTH_TOKEN|WEBHOOK[A-Z0-9_]*(TOKEN|SECRET|AUTH))\b/i.test(content);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function extractLocalImports(content) {
|
|
353
|
+
const imports = [];
|
|
354
|
+
const namedImportPattern = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
355
|
+
for (const match of content.matchAll(namedImportPattern)) {
|
|
356
|
+
imports.push({
|
|
357
|
+
specifiers: match[1]
|
|
358
|
+
.split(',')
|
|
359
|
+
.map((item) => item.trim().split(/\s+as\s+/i).pop())
|
|
360
|
+
.filter(Boolean),
|
|
361
|
+
source: match[2]
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return imports.filter((item) => item.source.startsWith('@/') || item.source.startsWith('.'));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function readImportedModule(repoRoot, importerFile, source) {
|
|
368
|
+
const candidates = resolveImportCandidates(repoRoot, importerFile, source);
|
|
369
|
+
return readModuleFromCandidates(repoRoot, candidates);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function resolveImportCandidates(repoRoot, importerFile, source) {
|
|
373
|
+
const base = source.startsWith('@/')
|
|
374
|
+
? path.join(repoRoot, 'src', source.slice(2))
|
|
375
|
+
: path.resolve(path.dirname(path.join(repoRoot, importerFile)), source);
|
|
376
|
+
return [
|
|
377
|
+
base,
|
|
378
|
+
`${base}.ts`,
|
|
379
|
+
`${base}.tsx`,
|
|
380
|
+
`${base}.js`,
|
|
381
|
+
`${base}.jsx`,
|
|
382
|
+
path.join(base, 'index.ts'),
|
|
383
|
+
path.join(base, 'index.tsx'),
|
|
384
|
+
path.join(base, 'index.js'),
|
|
385
|
+
path.join(base, 'index.jsx')
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function readModuleFromCandidates(repoRoot, candidates) {
|
|
390
|
+
for (const candidate of candidates) {
|
|
391
|
+
const content = await readTextIfExists(candidate);
|
|
392
|
+
if (content) {
|
|
393
|
+
return {
|
|
394
|
+
file: normalizeRelativeFile(path.relative(repoRoot, candidate)),
|
|
395
|
+
content
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { file: '', content: '' };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function normalizeRelativeFile(file) {
|
|
403
|
+
return file.split(path.sep).join('/');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function escapeRegExp(value) {
|
|
407
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function stripComments(content) {
|
|
411
|
+
return content
|
|
412
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
413
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isComplexMatcher(matcher) {
|
|
417
|
+
return matcher.includes('(?')
|
|
418
|
+
|| matcher.includes('.*')
|
|
419
|
+
|| matcher.includes('[')
|
|
420
|
+
|| matcher.includes(']')
|
|
421
|
+
|| matcher.includes('|');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function matcherExcludesApi(matcher) {
|
|
425
|
+
return matcher.includes('(?!api') || matcher.includes('(?!/api');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function summarizeRoutes(routes) {
|
|
429
|
+
const summary = {};
|
|
430
|
+
for (const route of routes) {
|
|
431
|
+
summary[route.classification] = (summary[route.classification] ?? 0) + 1;
|
|
432
|
+
}
|
|
433
|
+
return summary;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function summarizeProtection(routes) {
|
|
437
|
+
const summary = {};
|
|
438
|
+
for (const route of routes) {
|
|
439
|
+
const status = route.protection?.status ?? 'unknown';
|
|
440
|
+
summary[status] = (summary[status] ?? 0) + 1;
|
|
441
|
+
}
|
|
442
|
+
return summary;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function readTextIfExists(filePath) {
|
|
446
|
+
try {
|
|
447
|
+
return await readFile(filePath, 'utf8');
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error.code === 'ENOENT' || error.code === 'EISDIR') return '';
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
}
|