prodlint 0.2.0 โ 0.2.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/action.yml +35 -21
- package/dist/cli.js +10 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +124 -13875
- package/dist/mcp.js.map +1 -1
- package/package.json +3 -2
package/action.yml
CHANGED
|
@@ -41,38 +41,47 @@ runs:
|
|
|
41
41
|
- name: Run prodlint
|
|
42
42
|
id: scan
|
|
43
43
|
shell: bash
|
|
44
|
+
env:
|
|
45
|
+
INPUT_PATH: ${{ inputs.path }}
|
|
46
|
+
INPUT_IGNORE: ${{ inputs.ignore }}
|
|
44
47
|
run: |
|
|
45
|
-
|
|
46
|
-
if [ -n "$
|
|
47
|
-
IFS=',' read -ra PATTERNS <<< "$
|
|
48
|
+
CMD_ARGS=("$INPUT_PATH" "--json")
|
|
49
|
+
if [ -n "$INPUT_IGNORE" ]; then
|
|
50
|
+
IFS=',' read -ra PATTERNS <<< "$INPUT_IGNORE"
|
|
48
51
|
for p in "${PATTERNS[@]}"; do
|
|
49
|
-
|
|
52
|
+
trimmed=$(echo "$p" | xargs)
|
|
53
|
+
CMD_ARGS+=("--ignore" "$trimmed")
|
|
50
54
|
done
|
|
51
55
|
fi
|
|
52
56
|
|
|
53
|
-
OUTPUT=$(npx -y prodlint@latest $
|
|
57
|
+
OUTPUT=$(npx -y prodlint@latest "${CMD_ARGS[@]}" 2>&1) || true
|
|
54
58
|
|
|
55
59
|
SCORE=$(echo "$OUTPUT" | node -e "
|
|
56
60
|
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
57
|
-
console.log(d.overallScore);
|
|
61
|
+
console.log(Number(d.overallScore) || 0);
|
|
58
62
|
" 2>/dev/null || echo "0")
|
|
63
|
+
SCORE=$(echo "$SCORE" | tr -cd '0-9')
|
|
59
64
|
|
|
60
65
|
CRITICAL=$(echo "$OUTPUT" | node -e "
|
|
61
66
|
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
62
|
-
console.log(d.summary.critical);
|
|
67
|
+
console.log(Number(d.summary.critical) || 0);
|
|
63
68
|
" 2>/dev/null || echo "0")
|
|
69
|
+
CRITICAL=$(echo "$CRITICAL" | tr -cd '0-9')
|
|
64
70
|
|
|
65
|
-
echo "score=$SCORE" >> $GITHUB_OUTPUT
|
|
66
|
-
echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
|
|
71
|
+
echo "score=${SCORE:-0}" >> "$GITHUB_OUTPUT"
|
|
72
|
+
echo "critical=${CRITICAL:-0}" >> "$GITHUB_OUTPUT"
|
|
67
73
|
echo "$OUTPUT" > /tmp/prodlint-result.json
|
|
68
74
|
|
|
69
75
|
- name: Generate comment body
|
|
70
76
|
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
71
77
|
id: comment
|
|
72
78
|
shell: bash
|
|
79
|
+
env:
|
|
80
|
+
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
81
|
+
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
73
82
|
run: |
|
|
74
|
-
SCORE="$
|
|
75
|
-
THRESHOLD="$
|
|
83
|
+
SCORE="$SCAN_SCORE"
|
|
84
|
+
THRESHOLD="$INPUT_THRESHOLD"
|
|
76
85
|
|
|
77
86
|
if [ "$SCORE" -ge 80 ]; then
|
|
78
87
|
EMOJI="โ
"
|
|
@@ -85,10 +94,12 @@ runs:
|
|
|
85
94
|
COLOR="red"
|
|
86
95
|
fi
|
|
87
96
|
|
|
88
|
-
# Build comment from JSON
|
|
89
|
-
|
|
97
|
+
# Build comment from JSON with markdown sanitization
|
|
98
|
+
TMPFILE=$(mktemp "${RUNNER_TEMP:-/tmp}/prodlint-comment-XXXXXX.md")
|
|
99
|
+
node -e "
|
|
90
100
|
const fs = require('fs');
|
|
91
101
|
const d = JSON.parse(fs.readFileSync('/tmp/prodlint-result.json', 'utf8'));
|
|
102
|
+
const esc = s => String(s).replace(/[[\]()\\*_\`<>]/g, c => '\\\\' + c);
|
|
92
103
|
const lines = [];
|
|
93
104
|
lines.push('## ' + '$EMOJI' + ' Prodlint Score: **' + d.overallScore + '/100**');
|
|
94
105
|
lines.push('');
|
|
@@ -96,7 +107,7 @@ runs:
|
|
|
96
107
|
lines.push('|----------|-------|--------|');
|
|
97
108
|
for (const c of d.categoryScores) {
|
|
98
109
|
const icon = c.score >= 80 ? '๐ข' : c.score >= 60 ? '๐ก' : '๐ด';
|
|
99
|
-
lines.push('| ' + icon + ' ' + c.category + ' | ' + c.score + '/100 | ' + c.findingCount + ' |');
|
|
110
|
+
lines.push('| ' + icon + ' ' + esc(c.category) + ' | ' + c.score + '/100 | ' + c.findingCount + ' |');
|
|
100
111
|
}
|
|
101
112
|
if (d.summary.critical > 0) {
|
|
102
113
|
lines.push('');
|
|
@@ -104,7 +115,7 @@ runs:
|
|
|
104
115
|
lines.push('');
|
|
105
116
|
const crits = d.findings.filter(f => f.severity === 'critical');
|
|
106
117
|
for (const f of crits.slice(0, 10)) {
|
|
107
|
-
lines.push('-
|
|
118
|
+
lines.push('- \`' + esc(f.ruleId) + '\` \`' + esc(f.file) + ':' + f.line + '\` โ ' + esc(f.message));
|
|
108
119
|
}
|
|
109
120
|
if (crits.length > 10) {
|
|
110
121
|
lines.push('- ...and ' + (crits.length - 10) + ' more');
|
|
@@ -113,11 +124,11 @@ runs:
|
|
|
113
124
|
lines.push('');
|
|
114
125
|
lines.push('---');
|
|
115
126
|
lines.push('*Scanned ' + d.filesScanned + ' files in ' + d.scanDurationMs + 'ms ยท [prodlint](https://prodlint.com)*');
|
|
116
|
-
|
|
117
|
-
"
|
|
127
|
+
fs.writeFileSync('$TMPFILE', lines.join('\n'));
|
|
128
|
+
"
|
|
118
129
|
|
|
119
|
-
#
|
|
120
|
-
|
|
130
|
+
# Use the generated file for the comment step
|
|
131
|
+
cp "$TMPFILE" /tmp/prodlint-comment.md
|
|
121
132
|
|
|
122
133
|
- name: Post PR comment
|
|
123
134
|
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
@@ -129,9 +140,12 @@ runs:
|
|
|
129
140
|
- name: Check threshold
|
|
130
141
|
if: inputs.threshold != '0'
|
|
131
142
|
shell: bash
|
|
143
|
+
env:
|
|
144
|
+
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
145
|
+
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
132
146
|
run: |
|
|
133
|
-
SCORE="$
|
|
134
|
-
THRESHOLD="$
|
|
147
|
+
SCORE="$SCAN_SCORE"
|
|
148
|
+
THRESHOLD="$INPUT_THRESHOLD"
|
|
135
149
|
if [ "$SCORE" -lt "$THRESHOLD" ]; then
|
|
136
150
|
echo "::error::Prodlint score ($SCORE) is below threshold ($THRESHOLD)"
|
|
137
151
|
exit 1
|
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,8 @@ import { parseArgs } from "util";
|
|
|
5
5
|
|
|
6
6
|
// src/utils/file-walker.ts
|
|
7
7
|
import fg from "fast-glob";
|
|
8
|
-
import { readFile, stat } from "fs/promises";
|
|
9
|
-
import { resolve, extname } from "path";
|
|
8
|
+
import { readFile, stat, realpath } from "fs/promises";
|
|
9
|
+
import { resolve, extname, sep } from "path";
|
|
10
10
|
|
|
11
11
|
// src/utils/patterns.ts
|
|
12
12
|
function isApiRoute(relativePath) {
|
|
@@ -153,17 +153,21 @@ async function walkFiles(root, extraIgnores = []) {
|
|
|
153
153
|
cwd: root,
|
|
154
154
|
ignore: [...DEFAULT_IGNORES, ...extraIgnores],
|
|
155
155
|
absolute: false,
|
|
156
|
-
dot: true
|
|
156
|
+
dot: true,
|
|
157
|
+
followSymbolicLinks: false
|
|
157
158
|
});
|
|
158
159
|
return files.sort();
|
|
159
160
|
}
|
|
160
161
|
async function readFileContext(root, relativePath) {
|
|
161
162
|
try {
|
|
162
163
|
const absolutePath = resolve(root, relativePath);
|
|
164
|
+
const realRoot = await realpath(root);
|
|
165
|
+
const realFile = await realpath(absolutePath);
|
|
166
|
+
if (!realFile.startsWith(realRoot + sep) && realFile !== realRoot) return null;
|
|
163
167
|
const fileStats = await stat(absolutePath);
|
|
164
168
|
if (fileStats.size > MAX_FILE_SIZE) return null;
|
|
165
169
|
const content = await readFile(absolutePath, "utf-8");
|
|
166
|
-
const lines = content.split(
|
|
170
|
+
const lines = content.split(/\r?\n|\r/);
|
|
167
171
|
return {
|
|
168
172
|
absolutePath,
|
|
169
173
|
relativePath,
|
|
@@ -1023,7 +1027,7 @@ async function scan(options) {
|
|
|
1023
1027
|
const summary = summarizeFindings(findings);
|
|
1024
1028
|
return {
|
|
1025
1029
|
version: getVersion(),
|
|
1026
|
-
scannedPath:
|
|
1030
|
+
scannedPath: options.path,
|
|
1027
1031
|
filesScanned: filePaths.length,
|
|
1028
1032
|
scanDurationMs: Math.round(performance.now() - start),
|
|
1029
1033
|
findings,
|
|
@@ -1172,7 +1176,7 @@ function printHelp() {
|
|
|
1172
1176
|
`);
|
|
1173
1177
|
}
|
|
1174
1178
|
main().catch((err) => {
|
|
1175
|
-
console.error(err);
|
|
1179
|
+
console.error("prodlint error:", err instanceof Error ? err.message : "Unknown error");
|
|
1176
1180
|
process.exit(2);
|
|
1177
1181
|
});
|
|
1178
1182
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/utils/file-walker.ts","../src/utils/patterns.ts","../src/utils/version.ts","../src/scorer.ts","../src/rules/secrets.ts","../src/rules/hallucinated-imports.ts","../src/rules/auth-checks.ts","../src/rules/env-exposure.ts","../src/rules/error-handling.ts","../src/rules/input-validation.ts","../src/rules/rate-limiting.ts","../src/rules/cors-config.ts","../src/rules/ai-smells.ts","../src/rules/unsafe-html.ts","../src/rules/sql-injection.ts","../src/rules/index.ts","../src/scanner.ts","../src/reporter.ts"],"sourcesContent":["import { parseArgs } from 'node:util'\r\nimport { scan } from './scanner.js'\r\nimport { reportPretty, reportJson } from './reporter.js'\r\nimport { getVersion } from './utils/version.js'\r\n\r\nasync function main() {\r\n const { values, positionals } = parseArgs({\r\n allowPositionals: true,\r\n options: {\r\n json: { type: 'boolean', default: false },\r\n ignore: { type: 'string', multiple: true, default: [] },\r\n help: { type: 'boolean', short: 'h', default: false },\r\n version: { type: 'boolean', short: 'v', default: false },\r\n },\r\n })\r\n\r\n if (values.help) {\r\n printHelp()\r\n process.exit(0)\r\n }\r\n\r\n if (values.version) {\r\n console.log(getVersion())\r\n process.exit(0)\r\n }\r\n\r\n const targetPath = positionals[0] ?? '.'\r\n\r\n const result = await scan({\r\n path: targetPath,\r\n ignore: values.ignore as string[],\r\n })\r\n\r\n if (values.json) {\r\n console.log(reportJson(result))\r\n } else {\r\n console.log(reportPretty(result))\r\n }\r\n\r\n if (result.summary.critical > 0) {\r\n process.exit(1)\r\n }\r\n}\r\n\r\nfunction printHelp() {\r\n console.log(`\r\n prodlint - Scan AI-generated projects for production readiness issues\r\n\r\n Usage:\r\n npx prodlint [path] [options]\r\n\r\n Options:\r\n --json Output results as JSON\r\n --ignore <pattern> Glob patterns to ignore (can be repeated)\r\n -h, --help Show this help message\r\n -v, --version Show version\r\n\r\n Examples:\r\n npx prodlint Scan current directory\r\n npx prodlint ./my-app Scan specific path\r\n npx prodlint --json JSON output\r\n npx prodlint --ignore \"*.test\" Ignore test files\r\n`)\r\n}\r\n\r\nmain().catch((err) => {\r\n console.error(err)\r\n process.exit(2)\r\n})\r\n","import fg from 'fast-glob'\r\nimport { readFile, stat } from 'node:fs/promises'\r\nimport { resolve, extname } from 'node:path'\r\nimport { buildCommentMap } from './patterns.js'\r\nimport type { FileContext, ProjectContext } from '../types.js'\r\n\r\nconst DEFAULT_IGNORES = [\r\n '**/node_modules/**',\r\n '**/dist/**',\r\n '**/build/**',\r\n '**/.next/**',\r\n '**/.git/**',\r\n '**/coverage/**',\r\n '**/*.min.js',\r\n '**/*.min.css',\r\n '**/package-lock.json',\r\n '**/yarn.lock',\r\n '**/pnpm-lock.yaml',\r\n '**/bun.lockb',\r\n '**/*.map',\r\n '**/*.d.ts',\r\n]\r\n\r\nconst SCAN_EXTENSIONS = [\r\n 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',\r\n 'json',\r\n]\r\n\r\nconst MAX_FILE_SIZE = 1024 * 1024 // 1MB\r\n\r\nexport async function walkFiles(\r\n root: string,\r\n extraIgnores: string[] = [],\r\n): Promise<string[]> {\r\n const patterns = SCAN_EXTENSIONS.map(ext => `**/*.${ext}`)\r\n // Also grab .env files (dotfiles)\r\n patterns.push('**/.env', '**/.env.*')\r\n // Also grab .gitignore\r\n patterns.push('**/.gitignore')\r\n\r\n const files = await fg(patterns, {\r\n cwd: root,\r\n ignore: [...DEFAULT_IGNORES, ...extraIgnores],\r\n absolute: false,\r\n dot: true,\r\n })\r\n\r\n return files.sort()\r\n}\r\n\r\nexport async function readFileContext(\r\n root: string,\r\n relativePath: string,\r\n): Promise<FileContext | null> {\r\n try {\r\n const absolutePath = resolve(root, relativePath)\r\n\r\n // Skip files over 1MB\r\n const fileStats = await stat(absolutePath)\r\n if (fileStats.size > MAX_FILE_SIZE) return null\r\n\r\n const content = await readFile(absolutePath, 'utf-8')\r\n const lines = content.split('\\n')\r\n return {\r\n absolutePath,\r\n relativePath,\r\n content,\r\n lines,\r\n ext: extname(relativePath).slice(1), // remove leading dot\r\n commentMap: buildCommentMap(lines),\r\n }\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\nexport async function buildProjectContext(\r\n root: string,\r\n files: string[],\r\n): Promise<ProjectContext> {\r\n let packageJson: Record<string, unknown> | null = null\r\n let declaredDependencies = new Set<string>()\r\n let tsconfigPaths = new Set<string>()\r\n let hasAuthMiddleware = false\r\n let gitignoreContent: string | null = null\r\n let envInGitignore = false\r\n\r\n // Read package.json\r\n try {\r\n const raw = await readFile(resolve(root, 'package.json'), 'utf-8')\r\n packageJson = JSON.parse(raw)\r\n const deps = {\r\n ...(packageJson?.dependencies as Record<string, string> ?? {}),\r\n ...(packageJson?.devDependencies as Record<string, string> ?? {}),\r\n ...(packageJson?.peerDependencies as Record<string, string> ?? {}),\r\n }\r\n declaredDependencies = new Set(Object.keys(deps))\r\n } catch {\r\n // No package.json or invalid JSON\r\n }\r\n\r\n // Read tsconfig.json to extract path aliases\r\n try {\r\n const raw = await readFile(resolve(root, 'tsconfig.json'), 'utf-8')\r\n // Strip single-line comments (tsconfig allows them)\r\n const stripped = raw.replace(/\\/\\/.*$/gm, '')\r\n const tsconfig = JSON.parse(stripped)\r\n const paths = tsconfig?.compilerOptions?.paths as Record<string, string[]> | undefined\r\n if (paths) {\r\n for (const alias of Object.keys(paths)) {\r\n // @components/* โ @components\r\n // @/* โ @/\r\n const prefix = alias.replace(/\\/?\\*$/, '')\r\n if (prefix) tsconfigPaths.add(prefix)\r\n }\r\n }\r\n } catch {\r\n // No tsconfig.json\r\n }\r\n\r\n // Check for auth middleware (middleware.ts or middleware.js in root)\r\n try {\r\n for (const name of ['middleware.ts', 'middleware.js', 'src/middleware.ts', 'src/middleware.js']) {\r\n try {\r\n const content = await readFile(resolve(root, name), 'utf-8')\r\n const authPatterns = [\r\n /getSession/i, /getUser/i, /auth\\(\\)/, /withAuth/i,\r\n /clerkMiddleware/i, /authMiddleware/i, /NextAuth/i,\r\n /supabase.*auth/i, /createMiddlewareClient/i,\r\n /getToken/i, /verifyToken/i, /jwt/i, /updateSession/i,\r\n ]\r\n if (authPatterns.some(p => p.test(content))) {\r\n hasAuthMiddleware = true\r\n break\r\n }\r\n } catch {\r\n // File doesn't exist\r\n }\r\n }\r\n } catch {\r\n // No middleware\r\n }\r\n\r\n // Read .gitignore\r\n try {\r\n gitignoreContent = await readFile(resolve(root, '.gitignore'), 'utf-8')\r\n envInGitignore = /^\\.env$/m.test(gitignoreContent) ||\r\n /^\\.env\\*$/m.test(gitignoreContent) ||\r\n /^\\.env\\.\\*$/m.test(gitignoreContent) ||\r\n /^\\.env\\.local$/m.test(gitignoreContent)\r\n } catch {\r\n // No .gitignore\r\n }\r\n\r\n return {\r\n root,\r\n packageJson,\r\n declaredDependencies,\r\n tsconfigPaths,\r\n hasAuthMiddleware,\r\n gitignoreContent,\r\n envInGitignore,\r\n allFiles: files,\r\n }\r\n}\r\n","/**\r\n * Check if a file path looks like a Next.js API route\r\n */\r\nexport function isApiRoute(relativePath: string): boolean {\r\n // Next.js App Router: app/**/route.ts\r\n if (/app\\/.*route\\.(ts|js|tsx|jsx)$/.test(relativePath)) return true\r\n // Next.js Pages Router: pages/api/**\r\n if (/pages\\/api\\//.test(relativePath)) return true\r\n return false\r\n}\r\n\r\n/**\r\n * Check if a file is a React client component (has \"use client\" directive)\r\n */\r\nexport function isClientComponent(content: string): boolean {\r\n // Must be at the very top (possibly after comments/whitespace)\r\n const firstLines = content.slice(0, 500)\r\n return /^(['\"])use client\\1/m.test(firstLines)\r\n}\r\n\r\n/**\r\n * Check if a file is a React server component (default in App Router)\r\n */\r\nexport function isServerComponent(content: string): boolean {\r\n return !isClientComponent(content)\r\n}\r\n\r\n/**\r\n * Pre-compute which lines are inside block comments.\r\n * Returns a boolean array where true = line is inside a block comment.\r\n */\r\nexport function buildCommentMap(lines: string[]): boolean[] {\r\n const map = new Array<boolean>(lines.length).fill(false)\r\n let inBlock = false\r\n\r\n for (let i = 0; i < lines.length; i++) {\r\n const line = lines[i]\r\n\r\n if (inBlock) {\r\n map[i] = true\r\n if (line.includes('*/')) {\r\n inBlock = false\r\n }\r\n continue\r\n }\r\n\r\n // Check for block comment start (not preceded by code on the same line that matters)\r\n const trimmed = line.trim()\r\n if (trimmed.startsWith('/*')) {\r\n map[i] = true\r\n if (!trimmed.includes('*/')) {\r\n inBlock = true\r\n }\r\n continue\r\n }\r\n\r\n // Inline /* ... */ on a line with code โ don't mark the whole line\r\n // But a line that's ONLY a mid-block comment continuation (starts with *)\r\n if (trimmed.startsWith('*')) {\r\n // Could be JSDoc or block comment continuation\r\n map[i] = true\r\n }\r\n }\r\n\r\n return map\r\n}\r\n\r\n/**\r\n * Check if a line is a comment (single-line or inside block comment)\r\n */\r\nexport function isCommentLine(lines: string[], lineIndex: number, commentMap: boolean[]): boolean {\r\n if (commentMap[lineIndex]) return true\r\n const trimmed = lines[lineIndex]?.trim() ?? ''\r\n return trimmed.startsWith('//')\r\n}\r\n\r\n/**\r\n * Check if line is suppressed by a prodlint-disable comment\r\n */\r\nexport function isLineSuppressed(\r\n lines: string[],\r\n lineIndex: number,\r\n ruleId: string,\r\n): boolean {\r\n // Check file-level disable\r\n for (const line of lines) {\r\n const trimmed = line.trim()\r\n if (trimmed === '' || trimmed.startsWith('//') || trimmed.startsWith('/*')) {\r\n const match = trimmed.match(/prodlint-disable\\s+(.+)/)\r\n if (match) {\r\n const ids = match[1].split(/[\\s,]+/).filter(Boolean)\r\n if (ids.includes(ruleId)) return true\r\n }\r\n continue\r\n }\r\n break // Stop at first non-comment, non-empty line\r\n }\r\n\r\n // Check line-level disable (previous line)\r\n if (lineIndex > 0) {\r\n const prevLine = lines[lineIndex - 1].trim()\r\n const match = prevLine.match(/prodlint-disable-next-line\\s+(.+)/)\r\n if (match) {\r\n const ids = match[1].split(/[\\s,]+/).filter(Boolean)\r\n if (ids.includes(ruleId)) return true\r\n }\r\n }\r\n\r\n return false\r\n}\r\n\r\n/**\r\n * Known Node.js built-in modules\r\n */\r\nexport const NODE_BUILTINS = new Set([\r\n 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster',\r\n 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel',\r\n 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https',\r\n 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks',\r\n 'process', 'punycode', 'querystring', 'readline', 'repl',\r\n 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'trace_events',\r\n 'tty', 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib',\r\n // node: prefixed are handled separately\r\n])\r\n","import { readFileSync } from 'node:fs'\r\nimport { fileURLToPath } from 'node:url'\r\nimport { dirname, resolve } from 'node:path'\r\n\r\nexport function getVersion(): string {\r\n try {\r\n // Try relative to the built file location\r\n const dir = dirname(fileURLToPath(import.meta.url))\r\n const pkg = JSON.parse(\r\n readFileSync(resolve(dir, '..', 'package.json'), 'utf-8'),\r\n )\r\n return pkg.version ?? '0.0.0'\r\n } catch {\r\n return '0.0.0'\r\n }\r\n}\r\n","import type { Category, CategoryScore, Finding } from './types.js'\r\n\r\nconst CATEGORIES: Category[] = ['security', 'reliability', 'performance', 'ai-quality']\r\n\r\nconst DEDUCTIONS: Record<string, number> = {\r\n critical: 10,\r\n warning: 3,\r\n info: 1,\r\n}\r\n\r\nexport function calculateScores(findings: Finding[]): {\r\n overallScore: number\r\n categoryScores: CategoryScore[]\r\n} {\r\n const categoryScores: CategoryScore[] = CATEGORIES.map(category => {\r\n const categoryFindings = findings.filter(f => f.category === category)\r\n let score = 100\r\n for (const f of categoryFindings) {\r\n score -= DEDUCTIONS[f.severity] ?? 0\r\n }\r\n return {\r\n category,\r\n score: Math.max(0, score),\r\n findingCount: categoryFindings.length,\r\n }\r\n })\r\n\r\n const overallScore = Math.round(\r\n categoryScores.reduce((sum, c) => sum + c.score, 0) / CATEGORIES.length,\r\n )\r\n\r\n return { overallScore, categoryScores }\r\n}\r\n\r\nexport function summarizeFindings(findings: Finding[]) {\r\n return {\r\n critical: findings.filter(f => f.severity === 'critical').length,\r\n warning: findings.filter(f => f.severity === 'warning').length,\r\n info: findings.filter(f => f.severity === 'info').length,\r\n }\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\nconst SECRET_PATTERNS: { name: string; pattern: RegExp }[] = [\r\n { name: 'Stripe secret key', pattern: /sk_live_[a-zA-Z0-9]{20,}/ },\r\n { name: 'Stripe test key', pattern: /sk_test_[a-zA-Z0-9]{20,}/ },\r\n { name: 'AWS access key', pattern: /AKIA[0-9A-Z]{16}/ },\r\n { name: 'AWS secret key', pattern: /(?:aws_secret_access_key|AWS_SECRET)\\s*[=:]\\s*['\"]?[A-Za-z0-9/+=]{40}['\"]?/ },\r\n { name: 'Supabase service role key', pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\.[A-Za-z0-9_-]{50,}\\.[A-Za-z0-9_-]{20,}/ },\r\n { name: 'OpenAI API key', pattern: /sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}/ },\r\n { name: 'GitHub token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },\r\n { name: 'GitHub fine-grained token', pattern: /github_pat_[A-Za-z0-9_]{22,}/ },\r\n { name: 'Generic API key assignment', pattern: /(?:api_key|apikey|api_secret|secret_key|private_key)\\s*[=:]\\s*['\"][a-zA-Z0-9_\\-]{20,}['\"]/ },\r\n { name: 'SendGrid API key', pattern: /SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}/ },\r\n]\r\n\r\nexport const secretsRule: Rule = {\r\n id: 'secrets',\r\n name: 'Hardcoded Secrets',\r\n description: 'Detects hardcoded API keys, tokens, and credentials in source code',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n const line = file.lines[i]\r\n\r\n // Skip comments (single-line and block)\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n for (const { name, pattern } of SECRET_PATTERNS) {\r\n const match = pattern.exec(line)\r\n if (match) {\r\n findings.push({\r\n ruleId: 'secrets',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: match.index + 1,\r\n message: `Hardcoded ${name} detected`,\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n }\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine, NODE_BUILTINS } from '../utils/patterns.js'\r\n\r\n// Packages that are commonly available without being in package.json\r\nconst IMPLICIT_PACKAGES = new Set([\r\n 'react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime',\r\n 'next', 'next/server', 'next/image', 'next/link', 'next/font',\r\n 'next/navigation', 'next/headers', 'next/dynamic', 'next/script',\r\n 'next/router', 'next/head', 'next/app', 'next/document',\r\n])\r\n\r\n// Line-level patterns to capture import sources\r\nconst LINE_IMPORT_PATTERNS = [\r\n /from\\s+['\"]([^'\"./][^'\"]*)['\"]/,\r\n /require\\s*\\(\\s*['\"]([^'\"./][^'\"]*)['\"]\\s*\\)/,\r\n /import\\s*\\(\\s*['\"]([^'\"./][^'\"]*)['\"]\\s*\\)/,\r\n]\r\n\r\nfunction getPackageName(importPath: string): string {\r\n if (importPath.startsWith('@')) {\r\n const parts = importPath.split('/')\r\n return parts.slice(0, 2).join('/')\r\n }\r\n return importPath.split('/')[0]\r\n}\r\n\r\nfunction isNodeBuiltin(name: string): boolean {\r\n if (name.startsWith('node:')) return true\r\n return NODE_BUILTINS.has(name)\r\n}\r\n\r\nfunction isPathAlias(importPath: string, tsconfigPaths: Set<string>): boolean {\r\n // Common convention aliases\r\n if (importPath.startsWith('@/') || importPath === '@') return true\r\n if (importPath.startsWith('~/') || importPath.startsWith('#/')) return true\r\n\r\n // Check against tsconfig.json paths\r\n for (const prefix of tsconfigPaths) {\r\n if (importPath === prefix || importPath.startsWith(prefix + '/')) return true\r\n }\r\n\r\n return false\r\n}\r\n\r\nexport const hallucinatedImportsRule: Rule = {\r\n id: 'hallucinated-imports',\r\n name: 'Hallucinated Imports',\r\n description: 'Detects imports of packages not declared in package.json and not Node.js built-ins',\r\n category: 'reliability',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'],\r\n\r\n check(file: FileContext, project: ProjectContext): Finding[] {\r\n if (!project.packageJson) return []\r\n\r\n const findings: Finding[] = []\r\n const seen = new Set<string>()\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n // Skip comments\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n for (const pattern of LINE_IMPORT_PATTERNS) {\r\n const match = pattern.exec(line)\r\n if (!match) continue\r\n\r\n const importPath = match[1]\r\n const pkgName = getPackageName(importPath)\r\n\r\n if (seen.has(pkgName)) continue\r\n seen.add(pkgName)\r\n\r\n if (isPathAlias(importPath, project.tsconfigPaths)) continue\r\n if (isNodeBuiltin(pkgName)) continue\r\n if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue\r\n if (project.declaredDependencies.has(pkgName)) continue\r\n\r\n findings.push({\r\n ruleId: 'hallucinated-imports',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: match.index + 1,\r\n message: `Package \"${pkgName}\" is imported but not in package.json`,\r\n severity: 'critical',\r\n category: 'reliability',\r\n })\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isApiRoute } from '../utils/patterns.js'\r\n\r\n// Routes that typically don't need auth\r\nconst AUTH_EXEMPT_PATTERNS = [\r\n /auth/i,\r\n /login/i,\r\n /signup/i,\r\n /register/i,\r\n /callback/i,\r\n /webhook/i,\r\n /health/i,\r\n /ping/i,\r\n /cron/i,\r\n /inngest/i,\r\n /stripe/i,\r\n /public/i,\r\n]\r\n\r\n// Patterns that indicate auth is being checked\r\nconst AUTH_PATTERNS = [\r\n /getServerSession\\s*\\(/,\r\n /getSession\\s*\\(/,\r\n /\\.auth\\.getUser\\s*\\(/,\r\n /auth\\(\\)/,\r\n /authenticate\\s*\\(/,\r\n /isAuthenticated/,\r\n /requireAuth/,\r\n /withAuth/,\r\n /NextAuth/,\r\n /getToken\\s*\\(/,\r\n /verifyToken\\s*\\(/,\r\n /jwt\\.verify\\s*\\(/,\r\n /createRouteHandlerClient/,\r\n /createServerComponentClient/,\r\n /authorization/i,\r\n /bearer/i,\r\n]\r\n\r\nexport const authChecksRule: Rule = {\r\n id: 'auth-checks',\r\n name: 'Missing Auth Checks',\r\n description: 'Detects API routes that lack authentication checks',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, project: ProjectContext): Finding[] {\r\n if (!isApiRoute(file.relativePath)) return []\r\n\r\n // Check if route is exempt\r\n for (const pattern of AUTH_EXEMPT_PATTERNS) {\r\n if (pattern.test(file.relativePath)) return []\r\n }\r\n\r\n // If project uses middleware-based auth, downgrade to info\r\n const severity = project.hasAuthMiddleware ? 'info' as const : 'critical' as const\r\n\r\n // Check if any auth pattern exists in the file\r\n for (const pattern of AUTH_PATTERNS) {\r\n if (pattern.test(file.content)) return []\r\n }\r\n\r\n // Find the line where the handler is exported\r\n let handlerLine = 1\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (/export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE|handler)/i.test(file.lines[i])) {\r\n handlerLine = i + 1\r\n break\r\n }\r\n }\r\n\r\n const message = project.hasAuthMiddleware\r\n ? 'API route has no inline auth check (middleware auth detected โ verify coverage)'\r\n : 'API route has no authentication check'\r\n\r\n return [{\r\n ruleId: 'auth-checks',\r\n file: file.relativePath,\r\n line: handlerLine,\r\n column: 1,\r\n message,\r\n severity,\r\n category: 'security',\r\n }]\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isClientComponent, isCommentLine } from '../utils/patterns.js'\r\n\r\n// Server-only env vars that should never appear in client code\r\nconst SERVER_ENV_PATTERN = /process\\.env\\.(?!NEXT_PUBLIC_)([A-Z][A-Z0-9_]*)/g\r\n\r\n// Env var names that are definitely server-only sensitive\r\nconst SENSITIVE_ENV_NAMES = new Set([\r\n 'DATABASE_URL',\r\n 'SUPABASE_SERVICE_ROLE_KEY',\r\n 'STRIPE_SECRET_KEY',\r\n 'STRIPE_WEBHOOK_SECRET',\r\n 'OPENAI_API_KEY',\r\n 'ANTHROPIC_API_KEY',\r\n 'AWS_SECRET_ACCESS_KEY',\r\n 'JWT_SECRET',\r\n 'SESSION_SECRET',\r\n 'REDIS_URL',\r\n 'SMTP_PASSWORD',\r\n 'SENDGRID_API_KEY',\r\n])\r\n\r\nexport const envExposureRule: Rule = {\r\n id: 'env-exposure',\r\n name: 'Environment Variable Exposure',\r\n description: 'Detects server environment variables used in client components and .env files not in .gitignore',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: [],\r\n\r\n check(file: FileContext, project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n // Check 1: .env not in .gitignore\r\n if (file.relativePath === '.gitignore') {\r\n if (!project.envInGitignore) {\r\n findings.push({\r\n ruleId: 'env-exposure',\r\n file: file.relativePath,\r\n line: 1,\r\n column: 1,\r\n message: '.env is not listed in .gitignore โ secrets may be committed',\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n }\r\n return findings\r\n }\r\n\r\n // Check 2: Server env vars in client components\r\n if (!['ts', 'tsx', 'js', 'jsx'].includes(file.ext)) return findings\r\n if (!isClientComponent(file.content)) return findings\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n const regex = new RegExp(SERVER_ENV_PATTERN.source, SERVER_ENV_PATTERN.flags)\r\n let match: RegExpExecArray | null\r\n while ((match = regex.exec(line)) !== null) {\r\n const envName = match[1]\r\n const isSensitive = SENSITIVE_ENV_NAMES.has(envName)\r\n findings.push({\r\n ruleId: 'env-exposure',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: match.index + 1,\r\n message: isSensitive\r\n ? `Sensitive server env var \"${envName}\" used in client component`\r\n : `Server env var \"${envName}\" used in client component (will be undefined at runtime)`,\r\n severity: isSensitive ? 'critical' : 'warning',\r\n category: 'security',\r\n })\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isApiRoute, isCommentLine } from '../utils/patterns.js'\r\n\r\nexport const errorHandlingRule: Rule = {\r\n id: 'error-handling',\r\n name: 'Missing Error Handling',\r\n description: 'Detects API routes without try/catch and empty catch blocks',\r\n category: 'reliability',\r\n severity: 'warning',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n // Check 1: API routes without try/catch\r\n if (isApiRoute(file.relativePath)) {\r\n const hasTryCatch = /try\\s*\\{/.test(file.content)\r\n if (!hasTryCatch) {\r\n let handlerLine = 1\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (/export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE|handler)/i.test(file.lines[i])) {\r\n handlerLine = i + 1\r\n break\r\n }\r\n }\r\n findings.push({\r\n ruleId: 'error-handling',\r\n file: file.relativePath,\r\n line: handlerLine,\r\n column: 1,\r\n message: 'API route handler has no try/catch block',\r\n severity: 'warning',\r\n category: 'reliability',\r\n })\r\n }\r\n }\r\n\r\n // Check 2: Empty catch blocks (all files)\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n // Single-line empty catch: catch (...) {} or catch {}\r\n if (/catch\\s*(\\([^)]*\\))?\\s*\\{\\s*\\}/.test(line)) {\r\n findings.push({\r\n ruleId: 'error-handling',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('catch') + 1,\r\n message: 'Empty catch block silently swallows errors',\r\n severity: 'warning',\r\n category: 'reliability',\r\n })\r\n continue\r\n }\r\n\r\n // Multi-line empty catch: catch (...) {\\n}\r\n if (/catch\\s*(\\([^)]*\\))?\\s*\\{\\s*$/.test(line)) {\r\n const nextLine = file.lines[i + 1]?.trim()\r\n if (nextLine === '}') {\r\n findings.push({\r\n ruleId: 'error-handling',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('catch') + 1,\r\n message: 'Empty catch block silently swallows errors',\r\n severity: 'warning',\r\n category: 'reliability',\r\n })\r\n }\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isApiRoute } from '../utils/patterns.js'\r\n\r\nconst VALIDATION_PATTERNS = [\r\n /\\.parse\\s*\\(/,\r\n /\\.safeParse\\s*\\(/,\r\n /\\.validate\\s*\\(/,\r\n /\\.validateSync\\s*\\(/,\r\n /Joi\\.object/,\r\n /z\\.object/,\r\n /z\\.string/,\r\n /z\\.number/,\r\n /z\\.array/,\r\n /yup\\.object/,\r\n /ajv/i,\r\n /typebox/i,\r\n /valibot/i,\r\n /typeof\\s+.*body/,\r\n]\r\n\r\nconst BODY_ACCESS_PATTERNS = [\r\n /req\\.body/,\r\n /request\\.json\\s*\\(\\)/,\r\n /req\\.json\\s*\\(\\)/,\r\n]\r\n\r\nexport const inputValidationRule: Rule = {\r\n id: 'input-validation',\r\n name: 'Missing Input Validation',\r\n description: 'Detects API routes that access request body without validation',\r\n category: 'security',\r\n severity: 'warning',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n if (!isApiRoute(file.relativePath)) return []\r\n\r\n const accessesBody = BODY_ACCESS_PATTERNS.some(p => p.test(file.content))\r\n if (!accessesBody) return []\r\n\r\n const hasValidation = VALIDATION_PATTERNS.some(p => p.test(file.content))\r\n if (hasValidation) return []\r\n\r\n let bodyLine = 1\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (BODY_ACCESS_PATTERNS.some(p => p.test(file.lines[i]))) {\r\n bodyLine = i + 1\r\n break\r\n }\r\n }\r\n\r\n return [{\r\n ruleId: 'input-validation',\r\n file: file.relativePath,\r\n line: bodyLine,\r\n column: 1,\r\n message: 'Request body accessed without validation (consider using Zod, Yup, or similar)',\r\n severity: 'warning',\r\n category: 'security',\r\n }]\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isApiRoute } from '../utils/patterns.js'\r\n\r\n// Patterns indicating rate limiting is present\r\nconst RATE_LIMIT_PATTERNS = [\r\n /rateLimit/i,\r\n /rateLimiter/i,\r\n /rate-limit/i,\r\n /upstash.*ratelimit/i,\r\n /Ratelimit/,\r\n /@upstash\\/ratelimit/,\r\n /express-rate-limit/,\r\n /limiter/i,\r\n /throttle/i,\r\n /slidingWindow/,\r\n /fixedWindow/,\r\n /tokenBucket/,\r\n]\r\n\r\n// Routes that typically don't need rate limiting\r\nconst EXEMPT_PATTERNS = [\r\n /health/i,\r\n /ping/i,\r\n /webhook/i,\r\n /cron/i,\r\n /inngest/i,\r\n]\r\n\r\nexport const rateLimitingRule: Rule = {\r\n id: 'rate-limiting',\r\n name: 'Missing Rate Limiting',\r\n description: 'Detects API routes without rate limiting',\r\n category: 'security',\r\n severity: 'warning',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n if (!isApiRoute(file.relativePath)) return []\r\n\r\n // Check if route is exempt\r\n for (const pattern of EXEMPT_PATTERNS) {\r\n if (pattern.test(file.relativePath)) return []\r\n }\r\n\r\n // Check if rate limiting exists in the file\r\n for (const pattern of RATE_LIMIT_PATTERNS) {\r\n if (pattern.test(file.content)) return []\r\n }\r\n\r\n // Find the handler line\r\n let handlerLine = 1\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (/export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE|handler)/i.test(file.lines[i])) {\r\n handlerLine = i + 1\r\n break\r\n }\r\n }\r\n\r\n return [{\r\n ruleId: 'rate-limiting',\r\n file: file.relativePath,\r\n line: handlerLine,\r\n column: 1,\r\n message: 'API route has no rate limiting',\r\n severity: 'warning',\r\n category: 'security',\r\n }]\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\nexport const corsConfigRule: Rule = {\r\n id: 'cors-config',\r\n name: 'Permissive CORS',\r\n description: 'Detects overly permissive CORS configuration',\r\n category: 'security',\r\n severity: 'warning',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n if (/['\"]Access-Control-Allow-Origin['\"]\\s*[,:]\\s*['\"]\\*['\"]/.test(line)) {\r\n findings.push({\r\n ruleId: 'cors-config',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('Access-Control') + 1,\r\n message: 'Access-Control-Allow-Origin set to \"*\" allows any domain',\r\n severity: 'warning',\r\n category: 'security',\r\n })\r\n }\r\n\r\n if (/cors\\(\\s*\\)/.test(line)) {\r\n findings.push({\r\n ruleId: 'cors-config',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('cors(') + 1,\r\n message: 'cors() called without config allows all origins',\r\n severity: 'warning',\r\n category: 'security',\r\n })\r\n }\r\n\r\n if (/origin\\s*:\\s*['\"]\\*['\"]/.test(line)) {\r\n findings.push({\r\n ruleId: 'cors-config',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('origin') + 1,\r\n message: 'CORS origin set to \"*\" allows any domain',\r\n severity: 'warning',\r\n category: 'security',\r\n })\r\n }\r\n\r\n if (/origin\\s*:\\s*true/.test(line)) {\r\n findings.push({\r\n ruleId: 'cors-config',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('origin') + 1,\r\n message: 'CORS origin set to true mirrors any requesting origin',\r\n severity: 'warning',\r\n category: 'security',\r\n })\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\nconst CONSOLE_LOG_THRESHOLD = 5\r\nconst ANY_TYPE_THRESHOLD = 5\r\nconst COMMENTED_CODE_THRESHOLD = 3\r\n\r\nexport const aiSmellsRule: Rule = {\r\n id: 'ai-smells',\r\n name: 'AI Code Smells',\r\n description: 'Detects TODOs, placeholder functions, excessive console.log, any types, and commented-out code',\r\n category: 'ai-quality',\r\n severity: 'info',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n let consoleLogCount = 0\r\n let anyTypeCount = 0\r\n let commentedCodeRun = 0\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n const line = file.lines[i]\r\n const trimmed = line.trim()\r\n\r\n // Inside block comments โ only check for commented code detection\r\n if (file.commentMap[i]) {\r\n commentedCodeRun = 0\r\n continue\r\n }\r\n\r\n // TODO / FIXME / HACK / XXX comments (single-line only)\r\n if (trimmed.startsWith('//')) {\r\n const todoMatch = trimmed.match(/\\/\\/\\s*(TODO|FIXME|HACK|XXX)\\b(.*)/)\r\n if (todoMatch) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf(todoMatch[1]) + 1,\r\n message: `${todoMatch[1]} comment: ${todoMatch[2].trim() || '(no description)'}`,\r\n severity: 'info',\r\n category: 'ai-quality',\r\n })\r\n }\r\n\r\n // Commented-out code detection\r\n const commentContent = trimmed.slice(2).trim()\r\n const looksLikeCode = /^(import |export |const |let |var |function |if |for |while |return |await |async |class |switch )/.test(commentContent) ||\r\n /[{};=]$/.test(commentContent) ||\r\n /^\\w+\\(/.test(commentContent)\r\n\r\n if (looksLikeCode) {\r\n commentedCodeRun++\r\n if (commentedCodeRun === COMMENTED_CODE_THRESHOLD) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: i + 1 - COMMENTED_CODE_THRESHOLD + 1,\r\n column: 1,\r\n message: `${COMMENTED_CODE_THRESHOLD}+ consecutive lines of commented-out code`,\r\n severity: 'info',\r\n category: 'ai-quality',\r\n })\r\n }\r\n } else {\r\n commentedCodeRun = 0\r\n }\r\n continue\r\n }\r\n\r\n commentedCodeRun = 0\r\n\r\n // Placeholder functions\r\n if (/(?:throw new Error|throw Error)\\s*\\(\\s*['\"]not implemented['\"]/i.test(line)) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: 1,\r\n message: 'Placeholder \"not implemented\" function',\r\n severity: 'warning',\r\n category: 'ai-quality',\r\n })\r\n }\r\n\r\n // console.log\r\n if (/console\\.log\\s*\\(/.test(line)) {\r\n consoleLogCount++\r\n }\r\n\r\n // Excessive `any` types (only in actual type positions)\r\n if (/:\\s*any\\b/.test(line) || /\\bas\\s+any\\b/.test(line) || /<any>/.test(line)) {\r\n anyTypeCount++\r\n }\r\n }\r\n\r\n if (consoleLogCount > CONSOLE_LOG_THRESHOLD) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: 1,\r\n column: 1,\r\n message: `${consoleLogCount} console.log statements (consider a proper logger)`,\r\n severity: 'warning',\r\n category: 'ai-quality',\r\n })\r\n }\r\n\r\n if (anyTypeCount > ANY_TYPE_THRESHOLD) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: 1,\r\n column: 1,\r\n message: `${anyTypeCount} uses of \"any\" type (consider proper typing)`,\r\n severity: 'warning',\r\n category: 'ai-quality',\r\n })\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\nexport const unsafeHtmlRule: Rule = {\r\n id: 'unsafe-html',\r\n name: 'Unsafe HTML Rendering',\r\n description: 'Detects dangerouslySetInnerHTML and other XSS vectors in JSX/DOM code',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n // Only match dangerouslySetInnerHTML in JSX attribute position (after = or with {)\r\n // Avoids matching inside string literals, variable names, or regex patterns\r\n if (/dangerouslySetInnerHTML\\s*=/.test(line) || /dangerouslySetInnerHTML\\s*:/.test(line)) {\r\n findings.push({\r\n ruleId: 'unsafe-html',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('dangerouslySetInnerHTML') + 1,\r\n message: 'dangerouslySetInnerHTML is an XSS risk โ sanitize with DOMPurify or similar',\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n }\r\n\r\n // innerHTML assignment (actual DOM mutation, not inside a string)\r\n if (/\\w\\.innerHTML\\s*=/.test(line)) {\r\n findings.push({\r\n ruleId: 'unsafe-html',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('.innerHTML') + 1,\r\n message: 'Direct innerHTML assignment is an XSS risk',\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\n// Patterns for unsafe SQL construction\r\nconst SQL_INJECTION_PATTERNS = [\r\n // Template literals with SQL keywords and interpolation\r\n { pattern: /`\\s*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\\b[^`]*\\$\\{/, message: 'SQL query built with template literal interpolation โ use parameterized queries' },\r\n // String concatenation with SQL\r\n { pattern: /(?:SELECT|INSERT|UPDATE|DELETE|DROP)\\b.*['\"]?\\s*\\+\\s*\\w/, message: 'SQL query built with string concatenation โ use parameterized queries' },\r\n // .query() or .execute() with template literal\r\n { pattern: /\\.(?:query|execute|raw)\\s*\\(\\s*`[^`]*\\$\\{/, message: 'Database query with template literal interpolation โ use parameterized queries' },\r\n]\r\n\r\nexport const sqlInjectionRule: Rule = {\r\n id: 'sql-injection',\r\n name: 'SQL Injection Risk',\r\n description: 'Detects SQL queries built with string interpolation or concatenation',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n for (const { pattern, message } of SQL_INJECTION_PATTERNS) {\r\n if (pattern.test(line)) {\r\n findings.push({\r\n ruleId: 'sql-injection',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: 1,\r\n message,\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n break // One finding per line is enough\r\n }\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule } from '../types.js'\r\nimport { secretsRule } from './secrets.js'\r\nimport { hallucinatedImportsRule } from './hallucinated-imports.js'\r\nimport { authChecksRule } from './auth-checks.js'\r\nimport { envExposureRule } from './env-exposure.js'\r\nimport { errorHandlingRule } from './error-handling.js'\r\nimport { inputValidationRule } from './input-validation.js'\r\nimport { rateLimitingRule } from './rate-limiting.js'\r\nimport { corsConfigRule } from './cors-config.js'\r\nimport { aiSmellsRule } from './ai-smells.js'\r\nimport { unsafeHtmlRule } from './unsafe-html.js'\r\nimport { sqlInjectionRule } from './sql-injection.js'\r\n\r\nexport const rules: Rule[] = [\r\n secretsRule,\r\n hallucinatedImportsRule,\r\n authChecksRule,\r\n envExposureRule,\r\n errorHandlingRule,\r\n inputValidationRule,\r\n rateLimitingRule,\r\n corsConfigRule,\r\n aiSmellsRule,\r\n unsafeHtmlRule,\r\n sqlInjectionRule,\r\n]\r\n","import { walkFiles, readFileContext, buildProjectContext } from './utils/file-walker.js'\r\nimport { isLineSuppressed } from './utils/patterns.js'\r\nimport { getVersion } from './utils/version.js'\r\nimport { calculateScores, summarizeFindings } from './scorer.js'\r\nimport { rules } from './rules/index.js'\r\nimport type { Finding, ScanOptions, ScanResult } from './types.js'\r\nimport { resolve } from 'node:path'\r\n\r\nexport async function scan(options: ScanOptions): Promise<ScanResult> {\r\n const start = performance.now()\r\n const root = resolve(options.path)\r\n\r\n const filePaths = await walkFiles(root, options.ignore)\r\n const project = await buildProjectContext(root, filePaths)\r\n\r\n const findings: Finding[] = []\r\n\r\n for (const relativePath of filePaths) {\r\n const file = await readFileContext(root, relativePath)\r\n if (!file) continue\r\n\r\n for (const rule of rules) {\r\n if (\r\n rule.fileExtensions.length > 0 &&\r\n !rule.fileExtensions.includes(file.ext)\r\n ) {\r\n continue\r\n }\r\n\r\n const ruleFindings = rule.check(file, project)\r\n\r\n for (const finding of ruleFindings) {\r\n if (!isLineSuppressed(file.lines, finding.line - 1, finding.ruleId)) {\r\n findings.push(finding)\r\n }\r\n }\r\n }\r\n }\r\n\r\n const { overallScore, categoryScores } = calculateScores(findings)\r\n const summary = summarizeFindings(findings)\r\n\r\n return {\r\n version: getVersion(),\r\n scannedPath: root,\r\n filesScanned: filePaths.length,\r\n scanDurationMs: Math.round(performance.now() - start),\r\n findings,\r\n overallScore,\r\n categoryScores,\r\n summary,\r\n }\r\n}\r\n","import pc from 'picocolors'\r\nimport type { ScanResult, Finding } from './types.js'\r\n\r\nconst SEVERITY_COLORS = {\r\n critical: pc.red,\r\n warning: pc.yellow,\r\n info: pc.blue,\r\n} as const\r\n\r\nconst SEVERITY_LABELS = {\r\n critical: 'CRIT',\r\n warning: 'WARN',\r\n info: 'INFO',\r\n} as const\r\n\r\nfunction scoreColor(score: number): (s: string) => string {\r\n if (score >= 80) return pc.green\r\n if (score >= 50) return pc.yellow\r\n return pc.red\r\n}\r\n\r\nfunction groupByFile(findings: Finding[]): Map<string, Finding[]> {\r\n const map = new Map<string, Finding[]>()\r\n for (const f of findings) {\r\n const group = map.get(f.file) ?? []\r\n group.push(f)\r\n map.set(f.file, group)\r\n }\r\n return map\r\n}\r\n\r\nexport function reportPretty(result: ScanResult): string {\r\n const lines: string[] = []\r\n\r\n lines.push('')\r\n lines.push(pc.bold(' prodlint') + pc.dim(` v${result.version}`))\r\n lines.push(pc.dim(` Scanned ${result.filesScanned} files in ${result.scanDurationMs}ms`))\r\n lines.push('')\r\n\r\n // Findings grouped by file\r\n if (result.findings.length > 0) {\r\n const grouped = groupByFile(result.findings)\r\n for (const [file, findings] of grouped) {\r\n lines.push(pc.underline(file))\r\n for (const f of findings) {\r\n const color = SEVERITY_COLORS[f.severity]\r\n const label = SEVERITY_LABELS[f.severity]\r\n lines.push(\r\n ` ${pc.dim(`${f.line}:${f.column}`)} ${color(label)} ${f.message} ${pc.dim(f.ruleId)}`,\r\n )\r\n }\r\n lines.push('')\r\n }\r\n }\r\n\r\n // Category scores\r\n lines.push(pc.bold(' Scores'))\r\n for (const cs of result.categoryScores) {\r\n const color = scoreColor(cs.score)\r\n const bar = renderBar(cs.score)\r\n lines.push(` ${cs.category.padEnd(14)} ${color(String(cs.score).padStart(3))} ${bar} ${pc.dim(`(${cs.findingCount} issues)`)}`)\r\n }\r\n lines.push('')\r\n\r\n // Overall\r\n const overallColor = scoreColor(result.overallScore)\r\n lines.push(pc.bold(` Overall: ${overallColor(String(result.overallScore))}/100`))\r\n lines.push('')\r\n\r\n // Summary line\r\n const { critical, warning, info } = result.summary\r\n const parts: string[] = []\r\n if (critical > 0) parts.push(pc.red(`${critical} critical`))\r\n if (warning > 0) parts.push(pc.yellow(`${warning} warnings`))\r\n if (info > 0) parts.push(pc.blue(`${info} info`))\r\n if (parts.length === 0) {\r\n lines.push(pc.green(' No issues found!'))\r\n } else {\r\n lines.push(` ${parts.join(pc.dim(' ยท '))}`)\r\n }\r\n lines.push('')\r\n\r\n // Badge\r\n const badgeColor = result.overallScore >= 80 ? 'brightgreen' : result.overallScore >= 60 ? 'yellow' : 'red'\r\n const badgeUrl = `https://img.shields.io/badge/prodlint-${result.overallScore}%2F100-${badgeColor}`\r\n lines.push(pc.dim(' Add to your README:'))\r\n lines.push(pc.dim(` [](https://prodlint.com)`))\r\n lines.push('')\r\n\r\n return lines.join('\\n')\r\n}\r\n\r\nexport function reportJson(result: ScanResult): string {\r\n return JSON.stringify(result, null, 2)\r\n}\r\n\r\nfunction renderBar(score: number): string {\r\n const width = 20\r\n const filled = Math.round((score / 100) * width)\r\n const empty = width - filled\r\n const color = scoreColor(score)\r\n return color('โ'.repeat(filled)) + pc.dim('โ'.repeat(empty))\r\n}\r\n"],"mappings":"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","names":["resolve","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/utils/file-walker.ts","../src/utils/patterns.ts","../src/utils/version.ts","../src/scorer.ts","../src/rules/secrets.ts","../src/rules/hallucinated-imports.ts","../src/rules/auth-checks.ts","../src/rules/env-exposure.ts","../src/rules/error-handling.ts","../src/rules/input-validation.ts","../src/rules/rate-limiting.ts","../src/rules/cors-config.ts","../src/rules/ai-smells.ts","../src/rules/unsafe-html.ts","../src/rules/sql-injection.ts","../src/rules/index.ts","../src/scanner.ts","../src/reporter.ts"],"sourcesContent":["import { parseArgs } from 'node:util'\r\nimport { scan } from './scanner.js'\r\nimport { reportPretty, reportJson } from './reporter.js'\r\nimport { getVersion } from './utils/version.js'\r\n\r\nasync function main() {\r\n const { values, positionals } = parseArgs({\r\n allowPositionals: true,\r\n options: {\r\n json: { type: 'boolean', default: false },\r\n ignore: { type: 'string', multiple: true, default: [] },\r\n help: { type: 'boolean', short: 'h', default: false },\r\n version: { type: 'boolean', short: 'v', default: false },\r\n },\r\n })\r\n\r\n if (values.help) {\r\n printHelp()\r\n process.exit(0)\r\n }\r\n\r\n if (values.version) {\r\n console.log(getVersion())\r\n process.exit(0)\r\n }\r\n\r\n const targetPath = positionals[0] ?? '.'\r\n\r\n const result = await scan({\r\n path: targetPath,\r\n ignore: values.ignore as string[],\r\n })\r\n\r\n if (values.json) {\r\n console.log(reportJson(result))\r\n } else {\r\n console.log(reportPretty(result))\r\n }\r\n\r\n if (result.summary.critical > 0) {\r\n process.exit(1)\r\n }\r\n}\r\n\r\nfunction printHelp() {\r\n console.log(`\r\n prodlint - Scan AI-generated projects for production readiness issues\r\n\r\n Usage:\r\n npx prodlint [path] [options]\r\n\r\n Options:\r\n --json Output results as JSON\r\n --ignore <pattern> Glob patterns to ignore (can be repeated)\r\n -h, --help Show this help message\r\n -v, --version Show version\r\n\r\n Examples:\r\n npx prodlint Scan current directory\r\n npx prodlint ./my-app Scan specific path\r\n npx prodlint --json JSON output\r\n npx prodlint --ignore \"*.test\" Ignore test files\r\n`)\r\n}\r\n\r\nmain().catch((err) => {\r\n console.error('prodlint error:', err instanceof Error ? err.message : 'Unknown error')\r\n process.exit(2)\r\n})\r\n","import fg from 'fast-glob'\r\nimport { readFile, stat, realpath } from 'node:fs/promises'\r\nimport { resolve, extname, sep } from 'node:path'\r\nimport { buildCommentMap } from './patterns.js'\r\nimport type { FileContext, ProjectContext } from '../types.js'\r\n\r\nconst DEFAULT_IGNORES = [\r\n '**/node_modules/**',\r\n '**/dist/**',\r\n '**/build/**',\r\n '**/.next/**',\r\n '**/.git/**',\r\n '**/coverage/**',\r\n '**/*.min.js',\r\n '**/*.min.css',\r\n '**/package-lock.json',\r\n '**/yarn.lock',\r\n '**/pnpm-lock.yaml',\r\n '**/bun.lockb',\r\n '**/*.map',\r\n '**/*.d.ts',\r\n]\r\n\r\nconst SCAN_EXTENSIONS = [\r\n 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',\r\n 'json',\r\n]\r\n\r\nconst MAX_FILE_SIZE = 1024 * 1024 // 1MB\r\n\r\nexport async function walkFiles(\r\n root: string,\r\n extraIgnores: string[] = [],\r\n): Promise<string[]> {\r\n const patterns = SCAN_EXTENSIONS.map(ext => `**/*.${ext}`)\r\n // Also grab .env files (dotfiles)\r\n patterns.push('**/.env', '**/.env.*')\r\n // Also grab .gitignore\r\n patterns.push('**/.gitignore')\r\n\r\n const files = await fg(patterns, {\r\n cwd: root,\r\n ignore: [...DEFAULT_IGNORES, ...extraIgnores],\r\n absolute: false,\r\n dot: true,\r\n followSymbolicLinks: false,\r\n })\r\n\r\n return files.sort()\r\n}\r\n\r\nexport async function readFileContext(\r\n root: string,\r\n relativePath: string,\r\n): Promise<FileContext | null> {\r\n try {\r\n const absolutePath = resolve(root, relativePath)\r\n\r\n // Verify the resolved path stays within the project root\r\n const realRoot = await realpath(root)\r\n const realFile = await realpath(absolutePath)\r\n if (!realFile.startsWith(realRoot + sep) && realFile !== realRoot) return null\r\n\r\n // Skip files over 1MB\r\n const fileStats = await stat(absolutePath)\r\n if (fileStats.size > MAX_FILE_SIZE) return null\r\n\r\n const content = await readFile(absolutePath, 'utf-8')\r\n const lines = content.split(/\\r?\\n|\\r/)\r\n return {\r\n absolutePath,\r\n relativePath,\r\n content,\r\n lines,\r\n ext: extname(relativePath).slice(1), // remove leading dot\r\n commentMap: buildCommentMap(lines),\r\n }\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\nexport async function buildProjectContext(\r\n root: string,\r\n files: string[],\r\n): Promise<ProjectContext> {\r\n let packageJson: Record<string, unknown> | null = null\r\n let declaredDependencies = new Set<string>()\r\n let tsconfigPaths = new Set<string>()\r\n let hasAuthMiddleware = false\r\n let gitignoreContent: string | null = null\r\n let envInGitignore = false\r\n\r\n // Read package.json\r\n try {\r\n const raw = await readFile(resolve(root, 'package.json'), 'utf-8')\r\n packageJson = JSON.parse(raw)\r\n const deps = {\r\n ...(packageJson?.dependencies as Record<string, string> ?? {}),\r\n ...(packageJson?.devDependencies as Record<string, string> ?? {}),\r\n ...(packageJson?.peerDependencies as Record<string, string> ?? {}),\r\n }\r\n declaredDependencies = new Set(Object.keys(deps))\r\n } catch {\r\n // No package.json or invalid JSON\r\n }\r\n\r\n // Read tsconfig.json to extract path aliases\r\n try {\r\n const raw = await readFile(resolve(root, 'tsconfig.json'), 'utf-8')\r\n // Strip single-line comments (tsconfig allows them)\r\n const stripped = raw.replace(/\\/\\/.*$/gm, '')\r\n const tsconfig = JSON.parse(stripped)\r\n const paths = tsconfig?.compilerOptions?.paths as Record<string, string[]> | undefined\r\n if (paths) {\r\n for (const alias of Object.keys(paths)) {\r\n // @components/* โ @components\r\n // @/* โ @/\r\n const prefix = alias.replace(/\\/?\\*$/, '')\r\n if (prefix) tsconfigPaths.add(prefix)\r\n }\r\n }\r\n } catch {\r\n // No tsconfig.json\r\n }\r\n\r\n // Check for auth middleware (middleware.ts or middleware.js in root)\r\n try {\r\n for (const name of ['middleware.ts', 'middleware.js', 'src/middleware.ts', 'src/middleware.js']) {\r\n try {\r\n const content = await readFile(resolve(root, name), 'utf-8')\r\n const authPatterns = [\r\n /getSession/i, /getUser/i, /auth\\(\\)/, /withAuth/i,\r\n /clerkMiddleware/i, /authMiddleware/i, /NextAuth/i,\r\n /supabase.*auth/i, /createMiddlewareClient/i,\r\n /getToken/i, /verifyToken/i, /jwt/i, /updateSession/i,\r\n ]\r\n if (authPatterns.some(p => p.test(content))) {\r\n hasAuthMiddleware = true\r\n break\r\n }\r\n } catch {\r\n // File doesn't exist\r\n }\r\n }\r\n } catch {\r\n // No middleware\r\n }\r\n\r\n // Read .gitignore\r\n try {\r\n gitignoreContent = await readFile(resolve(root, '.gitignore'), 'utf-8')\r\n envInGitignore = /^\\.env$/m.test(gitignoreContent) ||\r\n /^\\.env\\*$/m.test(gitignoreContent) ||\r\n /^\\.env\\.\\*$/m.test(gitignoreContent) ||\r\n /^\\.env\\.local$/m.test(gitignoreContent)\r\n } catch {\r\n // No .gitignore\r\n }\r\n\r\n return {\r\n root,\r\n packageJson,\r\n declaredDependencies,\r\n tsconfigPaths,\r\n hasAuthMiddleware,\r\n gitignoreContent,\r\n envInGitignore,\r\n allFiles: files,\r\n }\r\n}\r\n","/**\r\n * Check if a file path looks like a Next.js API route\r\n */\r\nexport function isApiRoute(relativePath: string): boolean {\r\n // Next.js App Router: app/**/route.ts\r\n if (/app\\/.*route\\.(ts|js|tsx|jsx)$/.test(relativePath)) return true\r\n // Next.js Pages Router: pages/api/**\r\n if (/pages\\/api\\//.test(relativePath)) return true\r\n return false\r\n}\r\n\r\n/**\r\n * Check if a file is a React client component (has \"use client\" directive)\r\n */\r\nexport function isClientComponent(content: string): boolean {\r\n // Must be at the very top (possibly after comments/whitespace)\r\n const firstLines = content.slice(0, 500)\r\n return /^(['\"])use client\\1/m.test(firstLines)\r\n}\r\n\r\n/**\r\n * Check if a file is a React server component (default in App Router)\r\n */\r\nexport function isServerComponent(content: string): boolean {\r\n return !isClientComponent(content)\r\n}\r\n\r\n/**\r\n * Pre-compute which lines are inside block comments.\r\n * Returns a boolean array where true = line is inside a block comment.\r\n */\r\nexport function buildCommentMap(lines: string[]): boolean[] {\r\n const map = new Array<boolean>(lines.length).fill(false)\r\n let inBlock = false\r\n\r\n for (let i = 0; i < lines.length; i++) {\r\n const line = lines[i]\r\n\r\n if (inBlock) {\r\n map[i] = true\r\n if (line.includes('*/')) {\r\n inBlock = false\r\n }\r\n continue\r\n }\r\n\r\n // Check for block comment start (not preceded by code on the same line that matters)\r\n const trimmed = line.trim()\r\n if (trimmed.startsWith('/*')) {\r\n map[i] = true\r\n if (!trimmed.includes('*/')) {\r\n inBlock = true\r\n }\r\n continue\r\n }\r\n\r\n // Inline /* ... */ on a line with code โ don't mark the whole line\r\n // But a line that's ONLY a mid-block comment continuation (starts with *)\r\n if (trimmed.startsWith('*')) {\r\n // Could be JSDoc or block comment continuation\r\n map[i] = true\r\n }\r\n }\r\n\r\n return map\r\n}\r\n\r\n/**\r\n * Check if a line is a comment (single-line or inside block comment)\r\n */\r\nexport function isCommentLine(lines: string[], lineIndex: number, commentMap: boolean[]): boolean {\r\n if (commentMap[lineIndex]) return true\r\n const trimmed = lines[lineIndex]?.trim() ?? ''\r\n return trimmed.startsWith('//')\r\n}\r\n\r\n/**\r\n * Check if line is suppressed by a prodlint-disable comment\r\n */\r\nexport function isLineSuppressed(\r\n lines: string[],\r\n lineIndex: number,\r\n ruleId: string,\r\n): boolean {\r\n // Check file-level disable\r\n for (const line of lines) {\r\n const trimmed = line.trim()\r\n if (trimmed === '' || trimmed.startsWith('//') || trimmed.startsWith('/*')) {\r\n const match = trimmed.match(/prodlint-disable\\s+(.+)/)\r\n if (match) {\r\n const ids = match[1].split(/[\\s,]+/).filter(Boolean)\r\n if (ids.includes(ruleId)) return true\r\n }\r\n continue\r\n }\r\n break // Stop at first non-comment, non-empty line\r\n }\r\n\r\n // Check line-level disable (previous line)\r\n if (lineIndex > 0) {\r\n const prevLine = lines[lineIndex - 1].trim()\r\n const match = prevLine.match(/prodlint-disable-next-line\\s+(.+)/)\r\n if (match) {\r\n const ids = match[1].split(/[\\s,]+/).filter(Boolean)\r\n if (ids.includes(ruleId)) return true\r\n }\r\n }\r\n\r\n return false\r\n}\r\n\r\n/**\r\n * Known Node.js built-in modules\r\n */\r\nexport const NODE_BUILTINS = new Set([\r\n 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster',\r\n 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel',\r\n 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https',\r\n 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks',\r\n 'process', 'punycode', 'querystring', 'readline', 'repl',\r\n 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'trace_events',\r\n 'tty', 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib',\r\n // node: prefixed are handled separately\r\n])\r\n","import { readFileSync } from 'node:fs'\r\nimport { fileURLToPath } from 'node:url'\r\nimport { dirname, resolve } from 'node:path'\r\n\r\nexport function getVersion(): string {\r\n try {\r\n // Try relative to the built file location\r\n const dir = dirname(fileURLToPath(import.meta.url))\r\n const pkg = JSON.parse(\r\n readFileSync(resolve(dir, '..', 'package.json'), 'utf-8'),\r\n )\r\n return pkg.version ?? '0.0.0'\r\n } catch {\r\n return '0.0.0'\r\n }\r\n}\r\n","import type { Category, CategoryScore, Finding } from './types.js'\r\n\r\nconst CATEGORIES: Category[] = ['security', 'reliability', 'performance', 'ai-quality']\r\n\r\nconst DEDUCTIONS: Record<string, number> = {\r\n critical: 10,\r\n warning: 3,\r\n info: 1,\r\n}\r\n\r\nexport function calculateScores(findings: Finding[]): {\r\n overallScore: number\r\n categoryScores: CategoryScore[]\r\n} {\r\n const categoryScores: CategoryScore[] = CATEGORIES.map(category => {\r\n const categoryFindings = findings.filter(f => f.category === category)\r\n let score = 100\r\n for (const f of categoryFindings) {\r\n score -= DEDUCTIONS[f.severity] ?? 0\r\n }\r\n return {\r\n category,\r\n score: Math.max(0, score),\r\n findingCount: categoryFindings.length,\r\n }\r\n })\r\n\r\n const overallScore = Math.round(\r\n categoryScores.reduce((sum, c) => sum + c.score, 0) / CATEGORIES.length,\r\n )\r\n\r\n return { overallScore, categoryScores }\r\n}\r\n\r\nexport function summarizeFindings(findings: Finding[]) {\r\n return {\r\n critical: findings.filter(f => f.severity === 'critical').length,\r\n warning: findings.filter(f => f.severity === 'warning').length,\r\n info: findings.filter(f => f.severity === 'info').length,\r\n }\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\nconst SECRET_PATTERNS: { name: string; pattern: RegExp }[] = [\r\n { name: 'Stripe secret key', pattern: /sk_live_[a-zA-Z0-9]{20,}/ },\r\n { name: 'Stripe test key', pattern: /sk_test_[a-zA-Z0-9]{20,}/ },\r\n { name: 'AWS access key', pattern: /AKIA[0-9A-Z]{16}/ },\r\n { name: 'AWS secret key', pattern: /(?:aws_secret_access_key|AWS_SECRET)\\s*[=:]\\s*['\"]?[A-Za-z0-9/+=]{40}['\"]?/ },\r\n { name: 'Supabase service role key', pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\.[A-Za-z0-9_-]{50,}\\.[A-Za-z0-9_-]{20,}/ },\r\n { name: 'OpenAI API key', pattern: /sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}/ },\r\n { name: 'GitHub token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },\r\n { name: 'GitHub fine-grained token', pattern: /github_pat_[A-Za-z0-9_]{22,}/ },\r\n { name: 'Generic API key assignment', pattern: /(?:api_key|apikey|api_secret|secret_key|private_key)\\s*[=:]\\s*['\"][a-zA-Z0-9_\\-]{20,}['\"]/ },\r\n { name: 'SendGrid API key', pattern: /SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}/ },\r\n]\r\n\r\nexport const secretsRule: Rule = {\r\n id: 'secrets',\r\n name: 'Hardcoded Secrets',\r\n description: 'Detects hardcoded API keys, tokens, and credentials in source code',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n const line = file.lines[i]\r\n\r\n // Skip comments (single-line and block)\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n for (const { name, pattern } of SECRET_PATTERNS) {\r\n const match = pattern.exec(line)\r\n if (match) {\r\n findings.push({\r\n ruleId: 'secrets',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: match.index + 1,\r\n message: `Hardcoded ${name} detected`,\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n }\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine, NODE_BUILTINS } from '../utils/patterns.js'\r\n\r\n// Packages that are commonly available without being in package.json\r\nconst IMPLICIT_PACKAGES = new Set([\r\n 'react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime',\r\n 'next', 'next/server', 'next/image', 'next/link', 'next/font',\r\n 'next/navigation', 'next/headers', 'next/dynamic', 'next/script',\r\n 'next/router', 'next/head', 'next/app', 'next/document',\r\n])\r\n\r\n// Line-level patterns to capture import sources\r\nconst LINE_IMPORT_PATTERNS = [\r\n /from\\s+['\"]([^'\"./][^'\"]*)['\"]/,\r\n /require\\s*\\(\\s*['\"]([^'\"./][^'\"]*)['\"]\\s*\\)/,\r\n /import\\s*\\(\\s*['\"]([^'\"./][^'\"]*)['\"]\\s*\\)/,\r\n]\r\n\r\nfunction getPackageName(importPath: string): string {\r\n if (importPath.startsWith('@')) {\r\n const parts = importPath.split('/')\r\n return parts.slice(0, 2).join('/')\r\n }\r\n return importPath.split('/')[0]\r\n}\r\n\r\nfunction isNodeBuiltin(name: string): boolean {\r\n if (name.startsWith('node:')) return true\r\n return NODE_BUILTINS.has(name)\r\n}\r\n\r\nfunction isPathAlias(importPath: string, tsconfigPaths: Set<string>): boolean {\r\n // Common convention aliases\r\n if (importPath.startsWith('@/') || importPath === '@') return true\r\n if (importPath.startsWith('~/') || importPath.startsWith('#/')) return true\r\n\r\n // Check against tsconfig.json paths\r\n for (const prefix of tsconfigPaths) {\r\n if (importPath === prefix || importPath.startsWith(prefix + '/')) return true\r\n }\r\n\r\n return false\r\n}\r\n\r\nexport const hallucinatedImportsRule: Rule = {\r\n id: 'hallucinated-imports',\r\n name: 'Hallucinated Imports',\r\n description: 'Detects imports of packages not declared in package.json and not Node.js built-ins',\r\n category: 'reliability',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'],\r\n\r\n check(file: FileContext, project: ProjectContext): Finding[] {\r\n if (!project.packageJson) return []\r\n\r\n const findings: Finding[] = []\r\n const seen = new Set<string>()\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n // Skip comments\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n for (const pattern of LINE_IMPORT_PATTERNS) {\r\n const match = pattern.exec(line)\r\n if (!match) continue\r\n\r\n const importPath = match[1]\r\n const pkgName = getPackageName(importPath)\r\n\r\n if (seen.has(pkgName)) continue\r\n seen.add(pkgName)\r\n\r\n if (isPathAlias(importPath, project.tsconfigPaths)) continue\r\n if (isNodeBuiltin(pkgName)) continue\r\n if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue\r\n if (project.declaredDependencies.has(pkgName)) continue\r\n\r\n findings.push({\r\n ruleId: 'hallucinated-imports',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: match.index + 1,\r\n message: `Package \"${pkgName}\" is imported but not in package.json`,\r\n severity: 'critical',\r\n category: 'reliability',\r\n })\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isApiRoute } from '../utils/patterns.js'\r\n\r\n// Routes that typically don't need auth\r\nconst AUTH_EXEMPT_PATTERNS = [\r\n /auth/i,\r\n /login/i,\r\n /signup/i,\r\n /register/i,\r\n /callback/i,\r\n /webhook/i,\r\n /health/i,\r\n /ping/i,\r\n /cron/i,\r\n /inngest/i,\r\n /stripe/i,\r\n /public/i,\r\n]\r\n\r\n// Patterns that indicate auth is being checked\r\nconst AUTH_PATTERNS = [\r\n /getServerSession\\s*\\(/,\r\n /getSession\\s*\\(/,\r\n /\\.auth\\.getUser\\s*\\(/,\r\n /auth\\(\\)/,\r\n /authenticate\\s*\\(/,\r\n /isAuthenticated/,\r\n /requireAuth/,\r\n /withAuth/,\r\n /NextAuth/,\r\n /getToken\\s*\\(/,\r\n /verifyToken\\s*\\(/,\r\n /jwt\\.verify\\s*\\(/,\r\n /createRouteHandlerClient/,\r\n /createServerComponentClient/,\r\n /authorization/i,\r\n /bearer/i,\r\n]\r\n\r\nexport const authChecksRule: Rule = {\r\n id: 'auth-checks',\r\n name: 'Missing Auth Checks',\r\n description: 'Detects API routes that lack authentication checks',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, project: ProjectContext): Finding[] {\r\n if (!isApiRoute(file.relativePath)) return []\r\n\r\n // Check if route is exempt\r\n for (const pattern of AUTH_EXEMPT_PATTERNS) {\r\n if (pattern.test(file.relativePath)) return []\r\n }\r\n\r\n // If project uses middleware-based auth, downgrade to info\r\n const severity = project.hasAuthMiddleware ? 'info' as const : 'critical' as const\r\n\r\n // Check if any auth pattern exists in the file\r\n for (const pattern of AUTH_PATTERNS) {\r\n if (pattern.test(file.content)) return []\r\n }\r\n\r\n // Find the line where the handler is exported\r\n let handlerLine = 1\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (/export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE|handler)/i.test(file.lines[i])) {\r\n handlerLine = i + 1\r\n break\r\n }\r\n }\r\n\r\n const message = project.hasAuthMiddleware\r\n ? 'API route has no inline auth check (middleware auth detected โ verify coverage)'\r\n : 'API route has no authentication check'\r\n\r\n return [{\r\n ruleId: 'auth-checks',\r\n file: file.relativePath,\r\n line: handlerLine,\r\n column: 1,\r\n message,\r\n severity,\r\n category: 'security',\r\n }]\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isClientComponent, isCommentLine } from '../utils/patterns.js'\r\n\r\n// Server-only env vars that should never appear in client code\r\nconst SERVER_ENV_PATTERN = /process\\.env\\.(?!NEXT_PUBLIC_)([A-Z][A-Z0-9_]*)/g\r\n\r\n// Env var names that are definitely server-only sensitive\r\nconst SENSITIVE_ENV_NAMES = new Set([\r\n 'DATABASE_URL',\r\n 'SUPABASE_SERVICE_ROLE_KEY',\r\n 'STRIPE_SECRET_KEY',\r\n 'STRIPE_WEBHOOK_SECRET',\r\n 'OPENAI_API_KEY',\r\n 'ANTHROPIC_API_KEY',\r\n 'AWS_SECRET_ACCESS_KEY',\r\n 'JWT_SECRET',\r\n 'SESSION_SECRET',\r\n 'REDIS_URL',\r\n 'SMTP_PASSWORD',\r\n 'SENDGRID_API_KEY',\r\n])\r\n\r\nexport const envExposureRule: Rule = {\r\n id: 'env-exposure',\r\n name: 'Environment Variable Exposure',\r\n description: 'Detects server environment variables used in client components and .env files not in .gitignore',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: [],\r\n\r\n check(file: FileContext, project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n // Check 1: .env not in .gitignore\r\n if (file.relativePath === '.gitignore') {\r\n if (!project.envInGitignore) {\r\n findings.push({\r\n ruleId: 'env-exposure',\r\n file: file.relativePath,\r\n line: 1,\r\n column: 1,\r\n message: '.env is not listed in .gitignore โ secrets may be committed',\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n }\r\n return findings\r\n }\r\n\r\n // Check 2: Server env vars in client components\r\n if (!['ts', 'tsx', 'js', 'jsx'].includes(file.ext)) return findings\r\n if (!isClientComponent(file.content)) return findings\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n const regex = new RegExp(SERVER_ENV_PATTERN.source, SERVER_ENV_PATTERN.flags)\r\n let match: RegExpExecArray | null\r\n while ((match = regex.exec(line)) !== null) {\r\n const envName = match[1]\r\n const isSensitive = SENSITIVE_ENV_NAMES.has(envName)\r\n findings.push({\r\n ruleId: 'env-exposure',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: match.index + 1,\r\n message: isSensitive\r\n ? `Sensitive server env var \"${envName}\" used in client component`\r\n : `Server env var \"${envName}\" used in client component (will be undefined at runtime)`,\r\n severity: isSensitive ? 'critical' : 'warning',\r\n category: 'security',\r\n })\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isApiRoute, isCommentLine } from '../utils/patterns.js'\r\n\r\nexport const errorHandlingRule: Rule = {\r\n id: 'error-handling',\r\n name: 'Missing Error Handling',\r\n description: 'Detects API routes without try/catch and empty catch blocks',\r\n category: 'reliability',\r\n severity: 'warning',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n // Check 1: API routes without try/catch\r\n if (isApiRoute(file.relativePath)) {\r\n const hasTryCatch = /try\\s*\\{/.test(file.content)\r\n if (!hasTryCatch) {\r\n let handlerLine = 1\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (/export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE|handler)/i.test(file.lines[i])) {\r\n handlerLine = i + 1\r\n break\r\n }\r\n }\r\n findings.push({\r\n ruleId: 'error-handling',\r\n file: file.relativePath,\r\n line: handlerLine,\r\n column: 1,\r\n message: 'API route handler has no try/catch block',\r\n severity: 'warning',\r\n category: 'reliability',\r\n })\r\n }\r\n }\r\n\r\n // Check 2: Empty catch blocks (all files)\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n // Single-line empty catch: catch (...) {} or catch {}\r\n if (/catch\\s*(\\([^)]*\\))?\\s*\\{\\s*\\}/.test(line)) {\r\n findings.push({\r\n ruleId: 'error-handling',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('catch') + 1,\r\n message: 'Empty catch block silently swallows errors',\r\n severity: 'warning',\r\n category: 'reliability',\r\n })\r\n continue\r\n }\r\n\r\n // Multi-line empty catch: catch (...) {\\n}\r\n if (/catch\\s*(\\([^)]*\\))?\\s*\\{\\s*$/.test(line)) {\r\n const nextLine = file.lines[i + 1]?.trim()\r\n if (nextLine === '}') {\r\n findings.push({\r\n ruleId: 'error-handling',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('catch') + 1,\r\n message: 'Empty catch block silently swallows errors',\r\n severity: 'warning',\r\n category: 'reliability',\r\n })\r\n }\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isApiRoute } from '../utils/patterns.js'\r\n\r\nconst VALIDATION_PATTERNS = [\r\n /\\.parse\\s*\\(/,\r\n /\\.safeParse\\s*\\(/,\r\n /\\.validate\\s*\\(/,\r\n /\\.validateSync\\s*\\(/,\r\n /Joi\\.object/,\r\n /z\\.object/,\r\n /z\\.string/,\r\n /z\\.number/,\r\n /z\\.array/,\r\n /yup\\.object/,\r\n /ajv/i,\r\n /typebox/i,\r\n /valibot/i,\r\n /typeof\\s+.*body/,\r\n]\r\n\r\nconst BODY_ACCESS_PATTERNS = [\r\n /req\\.body/,\r\n /request\\.json\\s*\\(\\)/,\r\n /req\\.json\\s*\\(\\)/,\r\n]\r\n\r\nexport const inputValidationRule: Rule = {\r\n id: 'input-validation',\r\n name: 'Missing Input Validation',\r\n description: 'Detects API routes that access request body without validation',\r\n category: 'security',\r\n severity: 'warning',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n if (!isApiRoute(file.relativePath)) return []\r\n\r\n const accessesBody = BODY_ACCESS_PATTERNS.some(p => p.test(file.content))\r\n if (!accessesBody) return []\r\n\r\n const hasValidation = VALIDATION_PATTERNS.some(p => p.test(file.content))\r\n if (hasValidation) return []\r\n\r\n let bodyLine = 1\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (BODY_ACCESS_PATTERNS.some(p => p.test(file.lines[i]))) {\r\n bodyLine = i + 1\r\n break\r\n }\r\n }\r\n\r\n return [{\r\n ruleId: 'input-validation',\r\n file: file.relativePath,\r\n line: bodyLine,\r\n column: 1,\r\n message: 'Request body accessed without validation (consider using Zod, Yup, or similar)',\r\n severity: 'warning',\r\n category: 'security',\r\n }]\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isApiRoute } from '../utils/patterns.js'\r\n\r\n// Patterns indicating rate limiting is present\r\nconst RATE_LIMIT_PATTERNS = [\r\n /rateLimit/i,\r\n /rateLimiter/i,\r\n /rate-limit/i,\r\n /upstash.*ratelimit/i,\r\n /Ratelimit/,\r\n /@upstash\\/ratelimit/,\r\n /express-rate-limit/,\r\n /limiter/i,\r\n /throttle/i,\r\n /slidingWindow/,\r\n /fixedWindow/,\r\n /tokenBucket/,\r\n]\r\n\r\n// Routes that typically don't need rate limiting\r\nconst EXEMPT_PATTERNS = [\r\n /health/i,\r\n /ping/i,\r\n /webhook/i,\r\n /cron/i,\r\n /inngest/i,\r\n]\r\n\r\nexport const rateLimitingRule: Rule = {\r\n id: 'rate-limiting',\r\n name: 'Missing Rate Limiting',\r\n description: 'Detects API routes without rate limiting',\r\n category: 'security',\r\n severity: 'warning',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n if (!isApiRoute(file.relativePath)) return []\r\n\r\n // Check if route is exempt\r\n for (const pattern of EXEMPT_PATTERNS) {\r\n if (pattern.test(file.relativePath)) return []\r\n }\r\n\r\n // Check if rate limiting exists in the file\r\n for (const pattern of RATE_LIMIT_PATTERNS) {\r\n if (pattern.test(file.content)) return []\r\n }\r\n\r\n // Find the handler line\r\n let handlerLine = 1\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (/export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE|handler)/i.test(file.lines[i])) {\r\n handlerLine = i + 1\r\n break\r\n }\r\n }\r\n\r\n return [{\r\n ruleId: 'rate-limiting',\r\n file: file.relativePath,\r\n line: handlerLine,\r\n column: 1,\r\n message: 'API route has no rate limiting',\r\n severity: 'warning',\r\n category: 'security',\r\n }]\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\nexport const corsConfigRule: Rule = {\r\n id: 'cors-config',\r\n name: 'Permissive CORS',\r\n description: 'Detects overly permissive CORS configuration',\r\n category: 'security',\r\n severity: 'warning',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n if (/['\"]Access-Control-Allow-Origin['\"]\\s*[,:]\\s*['\"]\\*['\"]/.test(line)) {\r\n findings.push({\r\n ruleId: 'cors-config',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('Access-Control') + 1,\r\n message: 'Access-Control-Allow-Origin set to \"*\" allows any domain',\r\n severity: 'warning',\r\n category: 'security',\r\n })\r\n }\r\n\r\n if (/cors\\(\\s*\\)/.test(line)) {\r\n findings.push({\r\n ruleId: 'cors-config',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('cors(') + 1,\r\n message: 'cors() called without config allows all origins',\r\n severity: 'warning',\r\n category: 'security',\r\n })\r\n }\r\n\r\n if (/origin\\s*:\\s*['\"]\\*['\"]/.test(line)) {\r\n findings.push({\r\n ruleId: 'cors-config',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('origin') + 1,\r\n message: 'CORS origin set to \"*\" allows any domain',\r\n severity: 'warning',\r\n category: 'security',\r\n })\r\n }\r\n\r\n if (/origin\\s*:\\s*true/.test(line)) {\r\n findings.push({\r\n ruleId: 'cors-config',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('origin') + 1,\r\n message: 'CORS origin set to true mirrors any requesting origin',\r\n severity: 'warning',\r\n category: 'security',\r\n })\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\nconst CONSOLE_LOG_THRESHOLD = 5\r\nconst ANY_TYPE_THRESHOLD = 5\r\nconst COMMENTED_CODE_THRESHOLD = 3\r\n\r\nexport const aiSmellsRule: Rule = {\r\n id: 'ai-smells',\r\n name: 'AI Code Smells',\r\n description: 'Detects TODOs, placeholder functions, excessive console.log, any types, and commented-out code',\r\n category: 'ai-quality',\r\n severity: 'info',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n let consoleLogCount = 0\r\n let anyTypeCount = 0\r\n let commentedCodeRun = 0\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n const line = file.lines[i]\r\n const trimmed = line.trim()\r\n\r\n // Inside block comments โ only check for commented code detection\r\n if (file.commentMap[i]) {\r\n commentedCodeRun = 0\r\n continue\r\n }\r\n\r\n // TODO / FIXME / HACK / XXX comments (single-line only)\r\n if (trimmed.startsWith('//')) {\r\n const todoMatch = trimmed.match(/\\/\\/\\s*(TODO|FIXME|HACK|XXX)\\b(.*)/)\r\n if (todoMatch) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf(todoMatch[1]) + 1,\r\n message: `${todoMatch[1]} comment: ${todoMatch[2].trim() || '(no description)'}`,\r\n severity: 'info',\r\n category: 'ai-quality',\r\n })\r\n }\r\n\r\n // Commented-out code detection\r\n const commentContent = trimmed.slice(2).trim()\r\n const looksLikeCode = /^(import |export |const |let |var |function |if |for |while |return |await |async |class |switch )/.test(commentContent) ||\r\n /[{};=]$/.test(commentContent) ||\r\n /^\\w+\\(/.test(commentContent)\r\n\r\n if (looksLikeCode) {\r\n commentedCodeRun++\r\n if (commentedCodeRun === COMMENTED_CODE_THRESHOLD) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: i + 1 - COMMENTED_CODE_THRESHOLD + 1,\r\n column: 1,\r\n message: `${COMMENTED_CODE_THRESHOLD}+ consecutive lines of commented-out code`,\r\n severity: 'info',\r\n category: 'ai-quality',\r\n })\r\n }\r\n } else {\r\n commentedCodeRun = 0\r\n }\r\n continue\r\n }\r\n\r\n commentedCodeRun = 0\r\n\r\n // Placeholder functions\r\n if (/(?:throw new Error|throw Error)\\s*\\(\\s*['\"]not implemented['\"]/i.test(line)) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: 1,\r\n message: 'Placeholder \"not implemented\" function',\r\n severity: 'warning',\r\n category: 'ai-quality',\r\n })\r\n }\r\n\r\n // console.log\r\n if (/console\\.log\\s*\\(/.test(line)) {\r\n consoleLogCount++\r\n }\r\n\r\n // Excessive `any` types (only in actual type positions)\r\n if (/:\\s*any\\b/.test(line) || /\\bas\\s+any\\b/.test(line) || /<any>/.test(line)) {\r\n anyTypeCount++\r\n }\r\n }\r\n\r\n if (consoleLogCount > CONSOLE_LOG_THRESHOLD) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: 1,\r\n column: 1,\r\n message: `${consoleLogCount} console.log statements (consider a proper logger)`,\r\n severity: 'warning',\r\n category: 'ai-quality',\r\n })\r\n }\r\n\r\n if (anyTypeCount > ANY_TYPE_THRESHOLD) {\r\n findings.push({\r\n ruleId: 'ai-smells',\r\n file: file.relativePath,\r\n line: 1,\r\n column: 1,\r\n message: `${anyTypeCount} uses of \"any\" type (consider proper typing)`,\r\n severity: 'warning',\r\n category: 'ai-quality',\r\n })\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\nexport const unsafeHtmlRule: Rule = {\r\n id: 'unsafe-html',\r\n name: 'Unsafe HTML Rendering',\r\n description: 'Detects dangerouslySetInnerHTML and other XSS vectors in JSX/DOM code',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n // Only match dangerouslySetInnerHTML in JSX attribute position (after = or with {)\r\n // Avoids matching inside string literals, variable names, or regex patterns\r\n if (/dangerouslySetInnerHTML\\s*=/.test(line) || /dangerouslySetInnerHTML\\s*:/.test(line)) {\r\n findings.push({\r\n ruleId: 'unsafe-html',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('dangerouslySetInnerHTML') + 1,\r\n message: 'dangerouslySetInnerHTML is an XSS risk โ sanitize with DOMPurify or similar',\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n }\r\n\r\n // innerHTML assignment (actual DOM mutation, not inside a string)\r\n if (/\\w\\.innerHTML\\s*=/.test(line)) {\r\n findings.push({\r\n ruleId: 'unsafe-html',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: line.indexOf('.innerHTML') + 1,\r\n message: 'Direct innerHTML assignment is an XSS risk',\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule, Finding, FileContext, ProjectContext } from '../types.js'\r\nimport { isCommentLine } from '../utils/patterns.js'\r\n\r\n// Patterns for unsafe SQL construction\r\nconst SQL_INJECTION_PATTERNS = [\r\n // Template literals with SQL keywords and interpolation\r\n { pattern: /`\\s*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\\b[^`]*\\$\\{/, message: 'SQL query built with template literal interpolation โ use parameterized queries' },\r\n // String concatenation with SQL\r\n { pattern: /(?:SELECT|INSERT|UPDATE|DELETE|DROP)\\b.*['\"]?\\s*\\+\\s*\\w/, message: 'SQL query built with string concatenation โ use parameterized queries' },\r\n // .query() or .execute() with template literal\r\n { pattern: /\\.(?:query|execute|raw)\\s*\\(\\s*`[^`]*\\$\\{/, message: 'Database query with template literal interpolation โ use parameterized queries' },\r\n]\r\n\r\nexport const sqlInjectionRule: Rule = {\r\n id: 'sql-injection',\r\n name: 'SQL Injection Risk',\r\n description: 'Detects SQL queries built with string interpolation or concatenation',\r\n category: 'security',\r\n severity: 'critical',\r\n fileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'],\r\n\r\n check(file: FileContext, _project: ProjectContext): Finding[] {\r\n const findings: Finding[] = []\r\n\r\n for (let i = 0; i < file.lines.length; i++) {\r\n if (isCommentLine(file.lines, i, file.commentMap)) continue\r\n\r\n const line = file.lines[i]\r\n\r\n for (const { pattern, message } of SQL_INJECTION_PATTERNS) {\r\n if (pattern.test(line)) {\r\n findings.push({\r\n ruleId: 'sql-injection',\r\n file: file.relativePath,\r\n line: i + 1,\r\n column: 1,\r\n message,\r\n severity: 'critical',\r\n category: 'security',\r\n })\r\n break // One finding per line is enough\r\n }\r\n }\r\n }\r\n\r\n return findings\r\n },\r\n}\r\n","import type { Rule } from '../types.js'\r\nimport { secretsRule } from './secrets.js'\r\nimport { hallucinatedImportsRule } from './hallucinated-imports.js'\r\nimport { authChecksRule } from './auth-checks.js'\r\nimport { envExposureRule } from './env-exposure.js'\r\nimport { errorHandlingRule } from './error-handling.js'\r\nimport { inputValidationRule } from './input-validation.js'\r\nimport { rateLimitingRule } from './rate-limiting.js'\r\nimport { corsConfigRule } from './cors-config.js'\r\nimport { aiSmellsRule } from './ai-smells.js'\r\nimport { unsafeHtmlRule } from './unsafe-html.js'\r\nimport { sqlInjectionRule } from './sql-injection.js'\r\n\r\nexport const rules: Rule[] = [\r\n secretsRule,\r\n hallucinatedImportsRule,\r\n authChecksRule,\r\n envExposureRule,\r\n errorHandlingRule,\r\n inputValidationRule,\r\n rateLimitingRule,\r\n corsConfigRule,\r\n aiSmellsRule,\r\n unsafeHtmlRule,\r\n sqlInjectionRule,\r\n]\r\n","import { walkFiles, readFileContext, buildProjectContext } from './utils/file-walker.js'\r\nimport { isLineSuppressed } from './utils/patterns.js'\r\nimport { getVersion } from './utils/version.js'\r\nimport { calculateScores, summarizeFindings } from './scorer.js'\r\nimport { rules } from './rules/index.js'\r\nimport type { Finding, ScanOptions, ScanResult } from './types.js'\r\nimport { resolve } from 'node:path'\r\n\r\nexport async function scan(options: ScanOptions): Promise<ScanResult> {\r\n const start = performance.now()\r\n const root = resolve(options.path)\r\n\r\n const filePaths = await walkFiles(root, options.ignore)\r\n const project = await buildProjectContext(root, filePaths)\r\n\r\n const findings: Finding[] = []\r\n\r\n for (const relativePath of filePaths) {\r\n const file = await readFileContext(root, relativePath)\r\n if (!file) continue\r\n\r\n for (const rule of rules) {\r\n if (\r\n rule.fileExtensions.length > 0 &&\r\n !rule.fileExtensions.includes(file.ext)\r\n ) {\r\n continue\r\n }\r\n\r\n const ruleFindings = rule.check(file, project)\r\n\r\n for (const finding of ruleFindings) {\r\n if (!isLineSuppressed(file.lines, finding.line - 1, finding.ruleId)) {\r\n findings.push(finding)\r\n }\r\n }\r\n }\r\n }\r\n\r\n const { overallScore, categoryScores } = calculateScores(findings)\r\n const summary = summarizeFindings(findings)\r\n\r\n return {\r\n version: getVersion(),\r\n scannedPath: options.path,\r\n filesScanned: filePaths.length,\r\n scanDurationMs: Math.round(performance.now() - start),\r\n findings,\r\n overallScore,\r\n categoryScores,\r\n summary,\r\n }\r\n}\r\n","import pc from 'picocolors'\r\nimport type { ScanResult, Finding } from './types.js'\r\n\r\nconst SEVERITY_COLORS = {\r\n critical: pc.red,\r\n warning: pc.yellow,\r\n info: pc.blue,\r\n} as const\r\n\r\nconst SEVERITY_LABELS = {\r\n critical: 'CRIT',\r\n warning: 'WARN',\r\n info: 'INFO',\r\n} as const\r\n\r\nfunction scoreColor(score: number): (s: string) => string {\r\n if (score >= 80) return pc.green\r\n if (score >= 50) return pc.yellow\r\n return pc.red\r\n}\r\n\r\nfunction groupByFile(findings: Finding[]): Map<string, Finding[]> {\r\n const map = new Map<string, Finding[]>()\r\n for (const f of findings) {\r\n const group = map.get(f.file) ?? []\r\n group.push(f)\r\n map.set(f.file, group)\r\n }\r\n return map\r\n}\r\n\r\nexport function reportPretty(result: ScanResult): string {\r\n const lines: string[] = []\r\n\r\n lines.push('')\r\n lines.push(pc.bold(' prodlint') + pc.dim(` v${result.version}`))\r\n lines.push(pc.dim(` Scanned ${result.filesScanned} files in ${result.scanDurationMs}ms`))\r\n lines.push('')\r\n\r\n // Findings grouped by file\r\n if (result.findings.length > 0) {\r\n const grouped = groupByFile(result.findings)\r\n for (const [file, findings] of grouped) {\r\n lines.push(pc.underline(file))\r\n for (const f of findings) {\r\n const color = SEVERITY_COLORS[f.severity]\r\n const label = SEVERITY_LABELS[f.severity]\r\n lines.push(\r\n ` ${pc.dim(`${f.line}:${f.column}`)} ${color(label)} ${f.message} ${pc.dim(f.ruleId)}`,\r\n )\r\n }\r\n lines.push('')\r\n }\r\n }\r\n\r\n // Category scores\r\n lines.push(pc.bold(' Scores'))\r\n for (const cs of result.categoryScores) {\r\n const color = scoreColor(cs.score)\r\n const bar = renderBar(cs.score)\r\n lines.push(` ${cs.category.padEnd(14)} ${color(String(cs.score).padStart(3))} ${bar} ${pc.dim(`(${cs.findingCount} issues)`)}`)\r\n }\r\n lines.push('')\r\n\r\n // Overall\r\n const overallColor = scoreColor(result.overallScore)\r\n lines.push(pc.bold(` Overall: ${overallColor(String(result.overallScore))}/100`))\r\n lines.push('')\r\n\r\n // Summary line\r\n const { critical, warning, info } = result.summary\r\n const parts: string[] = []\r\n if (critical > 0) parts.push(pc.red(`${critical} critical`))\r\n if (warning > 0) parts.push(pc.yellow(`${warning} warnings`))\r\n if (info > 0) parts.push(pc.blue(`${info} info`))\r\n if (parts.length === 0) {\r\n lines.push(pc.green(' No issues found!'))\r\n } else {\r\n lines.push(` ${parts.join(pc.dim(' ยท '))}`)\r\n }\r\n lines.push('')\r\n\r\n // Badge\r\n const badgeColor = result.overallScore >= 80 ? 'brightgreen' : result.overallScore >= 60 ? 'yellow' : 'red'\r\n const badgeUrl = `https://img.shields.io/badge/prodlint-${result.overallScore}%2F100-${badgeColor}`\r\n lines.push(pc.dim(' Add to your README:'))\r\n lines.push(pc.dim(` [](https://prodlint.com)`))\r\n lines.push('')\r\n\r\n return lines.join('\\n')\r\n}\r\n\r\nexport function reportJson(result: ScanResult): string {\r\n return JSON.stringify(result, null, 2)\r\n}\r\n\r\nfunction renderBar(score: number): string {\r\n const width = 20\r\n const filled = Math.round((score / 100) * width)\r\n const empty = width - filled\r\n const color = scoreColor(score)\r\n return color('โ'.repeat(filled)) + pc.dim('โ'.repeat(empty))\r\n}\r\n"],"mappings":";;;AAAA,SAAS,iBAAiB;;;ACA1B,OAAO,QAAQ;AACf,SAAS,UAAU,MAAM,gBAAgB;AACzC,SAAS,SAAS,SAAS,WAAW;;;ACC/B,SAAS,WAAW,cAA+B;AAExD,MAAI,iCAAiC,KAAK,YAAY,EAAG,QAAO;AAEhE,MAAI,eAAe,KAAK,YAAY,EAAG,QAAO;AAC9C,SAAO;AACT;AAKO,SAAS,kBAAkB,SAA0B;AAE1D,QAAM,aAAa,QAAQ,MAAM,GAAG,GAAG;AACvC,SAAO,uBAAuB,KAAK,UAAU;AAC/C;AAaO,SAAS,gBAAgB,OAA4B;AAC1D,QAAM,MAAM,IAAI,MAAe,MAAM,MAAM,EAAE,KAAK,KAAK;AACvD,MAAI,UAAU;AAEd,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,CAAC;AAEpB,QAAI,SAAS;AACX,UAAI,CAAC,IAAI;AACT,UAAI,KAAK,SAAS,IAAI,GAAG;AACvB,kBAAU;AAAA,MACZ;AACA;AAAA,IACF;AAGA,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC5B,UAAI,CAAC,IAAI;AACT,UAAI,CAAC,QAAQ,SAAS,IAAI,GAAG;AAC3B,kBAAU;AAAA,MACZ;AACA;AAAA,IACF;AAIA,QAAI,QAAQ,WAAW,GAAG,GAAG;AAE3B,UAAI,CAAC,IAAI;AAAA,IACX;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,cAAc,OAAiB,WAAmB,YAAgC;AAChG,MAAI,WAAW,SAAS,EAAG,QAAO;AAClC,QAAM,UAAU,MAAM,SAAS,GAAG,KAAK,KAAK;AAC5C,SAAO,QAAQ,WAAW,IAAI;AAChC;AAKO,SAAS,iBACd,OACA,WACA,QACS;AAET,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,YAAY,MAAM,QAAQ,WAAW,IAAI,KAAK,QAAQ,WAAW,IAAI,GAAG;AAC1E,YAAM,QAAQ,QAAQ,MAAM,yBAAyB;AACrD,UAAI,OAAO;AACT,cAAM,MAAM,MAAM,CAAC,EAAE,MAAM,QAAQ,EAAE,OAAO,OAAO;AACnD,YAAI,IAAI,SAAS,MAAM,EAAG,QAAO;AAAA,MACnC;AACA;AAAA,IACF;AACA;AAAA,EACF;AAGA,MAAI,YAAY,GAAG;AACjB,UAAM,WAAW,MAAM,YAAY,CAAC,EAAE,KAAK;AAC3C,UAAM,QAAQ,SAAS,MAAM,mCAAmC;AAChE,QAAI,OAAO;AACT,YAAM,MAAM,MAAM,CAAC,EAAE,MAAM,QAAQ,EAAE,OAAO,OAAO;AACnD,UAAI,IAAI,SAAS,MAAM,EAAG,QAAO;AAAA,IACnC;AAAA,EACF;AAEA,SAAO;AACT;AAKO,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EACnC;AAAA,EAAU;AAAA,EAAe;AAAA,EAAU;AAAA,EAAiB;AAAA,EACpD;AAAA,EAAW;AAAA,EAAa;AAAA,EAAU;AAAA,EAAS;AAAA,EAC3C;AAAA,EAAO;AAAA,EAAU;AAAA,EAAU;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAS;AAAA,EAClD;AAAA,EAAa;AAAA,EAAU;AAAA,EAAO;AAAA,EAAM;AAAA,EAAQ;AAAA,EAC5C;AAAA,EAAW;AAAA,EAAY;AAAA,EAAe;AAAA,EAAY;AAAA,EAClD;AAAA,EAAU;AAAA,EAAkB;AAAA,EAAO;AAAA,EAAU;AAAA,EAAO;AAAA,EACpD;AAAA,EAAO;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAkB;AAAA;AAE9D,CAAC;;;ADrHD,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,kBAAkB;AAAA,EACtB;AAAA,EAAM;AAAA,EAAO;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EACjC;AACF;AAEA,IAAM,gBAAgB,OAAO;AAE7B,eAAsB,UACpB,MACA,eAAyB,CAAC,GACP;AACnB,QAAM,WAAW,gBAAgB,IAAI,SAAO,QAAQ,GAAG,EAAE;AAEzD,WAAS,KAAK,WAAW,WAAW;AAEpC,WAAS,KAAK,eAAe;AAE7B,QAAM,QAAQ,MAAM,GAAG,UAAU;AAAA,IAC/B,KAAK;AAAA,IACL,QAAQ,CAAC,GAAG,iBAAiB,GAAG,YAAY;AAAA,IAC5C,UAAU;AAAA,IACV,KAAK;AAAA,IACL,qBAAqB;AAAA,EACvB,CAAC;AAED,SAAO,MAAM,KAAK;AACpB;AAEA,eAAsB,gBACpB,MACA,cAC6B;AAC7B,MAAI;AACF,UAAM,eAAe,QAAQ,MAAM,YAAY;AAG/C,UAAM,WAAW,MAAM,SAAS,IAAI;AACpC,UAAM,WAAW,MAAM,SAAS,YAAY;AAC5C,QAAI,CAAC,SAAS,WAAW,WAAW,GAAG,KAAK,aAAa,SAAU,QAAO;AAG1E,UAAM,YAAY,MAAM,KAAK,YAAY;AACzC,QAAI,UAAU,OAAO,cAAe,QAAO;AAE3C,UAAM,UAAU,MAAM,SAAS,cAAc,OAAO;AACpD,UAAM,QAAQ,QAAQ,MAAM,UAAU;AACtC,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,KAAK,QAAQ,YAAY,EAAE,MAAM,CAAC;AAAA;AAAA,MAClC,YAAY,gBAAgB,KAAK;AAAA,IACnC;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,oBACpB,MACA,OACyB;AACzB,MAAI,cAA8C;AAClD,MAAI,uBAAuB,oBAAI,IAAY;AAC3C,MAAI,gBAAgB,oBAAI,IAAY;AACpC,MAAI,oBAAoB;AACxB,MAAI,mBAAkC;AACtC,MAAI,iBAAiB;AAGrB,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,QAAQ,MAAM,cAAc,GAAG,OAAO;AACjE,kBAAc,KAAK,MAAM,GAAG;AAC5B,UAAM,OAAO;AAAA,MACX,GAAI,aAAa,gBAA0C,CAAC;AAAA,MAC5D,GAAI,aAAa,mBAA6C,CAAC;AAAA,MAC/D,GAAI,aAAa,oBAA8C,CAAC;AAAA,IAClE;AACA,2BAAuB,IAAI,IAAI,OAAO,KAAK,IAAI,CAAC;AAAA,EAClD,QAAQ;AAAA,EAER;AAGA,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,QAAQ,MAAM,eAAe,GAAG,OAAO;AAElE,UAAM,WAAW,IAAI,QAAQ,aAAa,EAAE;AAC5C,UAAM,WAAW,KAAK,MAAM,QAAQ;AACpC,UAAM,QAAQ,UAAU,iBAAiB;AACzC,QAAI,OAAO;AACT,iBAAW,SAAS,OAAO,KAAK,KAAK,GAAG;AAGtC,cAAM,SAAS,MAAM,QAAQ,UAAU,EAAE;AACzC,YAAI,OAAQ,eAAc,IAAI,MAAM;AAAA,MACtC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAGA,MAAI;AACF,eAAW,QAAQ,CAAC,iBAAiB,iBAAiB,qBAAqB,mBAAmB,GAAG;AAC/F,UAAI;AACF,cAAM,UAAU,MAAM,SAAS,QAAQ,MAAM,IAAI,GAAG,OAAO;AAC3D,cAAM,eAAe;AAAA,UACnB;AAAA,UAAe;AAAA,UAAY;AAAA,UAAY;AAAA,UACvC;AAAA,UAAoB;AAAA,UAAmB;AAAA,UACvC;AAAA,UAAmB;AAAA,UACnB;AAAA,UAAa;AAAA,UAAgB;AAAA,UAAQ;AAAA,QACvC;AACA,YAAI,aAAa,KAAK,OAAK,EAAE,KAAK,OAAO,CAAC,GAAG;AAC3C,8BAAoB;AACpB;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAGA,MAAI;AACF,uBAAmB,MAAM,SAAS,QAAQ,MAAM,YAAY,GAAG,OAAO;AACtE,qBAAiB,WAAW,KAAK,gBAAgB,KAC/C,aAAa,KAAK,gBAAgB,KAClC,eAAe,KAAK,gBAAgB,KACpC,kBAAkB,KAAK,gBAAgB;AAAA,EAC3C,QAAQ;AAAA,EAER;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,EACZ;AACF;;;AE1KA,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,SAAS,WAAAA,gBAAe;AAE1B,SAAS,aAAqB;AACnC,MAAI;AAEF,UAAM,MAAM,QAAQ,cAAc,YAAY,GAAG,CAAC;AAClD,UAAM,MAAM,KAAK;AAAA,MACf,aAAaA,SAAQ,KAAK,MAAM,cAAc,GAAG,OAAO;AAAA,IAC1D;AACA,WAAO,IAAI,WAAW;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACbA,IAAM,aAAyB,CAAC,YAAY,eAAe,eAAe,YAAY;AAEtF,IAAM,aAAqC;AAAA,EACzC,UAAU;AAAA,EACV,SAAS;AAAA,EACT,MAAM;AACR;AAEO,SAAS,gBAAgB,UAG9B;AACA,QAAM,iBAAkC,WAAW,IAAI,cAAY;AACjE,UAAM,mBAAmB,SAAS,OAAO,OAAK,EAAE,aAAa,QAAQ;AACrE,QAAI,QAAQ;AACZ,eAAW,KAAK,kBAAkB;AAChC,eAAS,WAAW,EAAE,QAAQ,KAAK;AAAA,IACrC;AACA,WAAO;AAAA,MACL;AAAA,MACA,OAAO,KAAK,IAAI,GAAG,KAAK;AAAA,MACxB,cAAc,iBAAiB;AAAA,IACjC;AAAA,EACF,CAAC;AAED,QAAM,eAAe,KAAK;AAAA,IACxB,eAAe,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC,IAAI,WAAW;AAAA,EACnE;AAEA,SAAO,EAAE,cAAc,eAAe;AACxC;AAEO,SAAS,kBAAkB,UAAqB;AACrD,SAAO;AAAA,IACL,UAAU,SAAS,OAAO,OAAK,EAAE,aAAa,UAAU,EAAE;AAAA,IAC1D,SAAS,SAAS,OAAO,OAAK,EAAE,aAAa,SAAS,EAAE;AAAA,IACxD,MAAM,SAAS,OAAO,OAAK,EAAE,aAAa,MAAM,EAAE;AAAA,EACpD;AACF;;;ACrCA,IAAM,kBAAuD;AAAA,EAC3D,EAAE,MAAM,qBAAqB,SAAS,2BAA2B;AAAA,EACjE,EAAE,MAAM,mBAAmB,SAAS,2BAA2B;AAAA,EAC/D,EAAE,MAAM,kBAAkB,SAAS,mBAAmB;AAAA,EACtD,EAAE,MAAM,kBAAkB,SAAS,6EAA6E;AAAA,EAChH,EAAE,MAAM,6BAA6B,SAAS,+EAA+E;AAAA,EAC7H,EAAE,MAAM,kBAAkB,SAAS,8CAA8C;AAAA,EACjF,EAAE,MAAM,gBAAgB,SAAS,2BAA2B;AAAA,EAC5D,EAAE,MAAM,6BAA6B,SAAS,+BAA+B;AAAA,EAC7E,EAAE,MAAM,8BAA8B,SAAS,4FAA4F;AAAA,EAC3I,EAAE,MAAM,oBAAoB,SAAS,2CAA2C;AAClF;AAEO,IAAM,cAAoB;AAAA,EAC/B,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,MAAM;AAAA,EAE/D,MAAM,MAAmB,UAAqC;AAC5D,UAAM,WAAsB,CAAC;AAE7B,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,YAAM,OAAO,KAAK,MAAM,CAAC;AAGzB,UAAI,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,EAAG;AAEnD,iBAAW,EAAE,MAAM,QAAQ,KAAK,iBAAiB;AAC/C,cAAM,QAAQ,QAAQ,KAAK,IAAI;AAC/B,YAAI,OAAO;AACT,mBAAS,KAAK;AAAA,YACZ,QAAQ;AAAA,YACR,MAAM,KAAK;AAAA,YACX,MAAM,IAAI;AAAA,YACV,QAAQ,MAAM,QAAQ;AAAA,YACtB,SAAS,aAAa,IAAI;AAAA,YAC1B,UAAU;AAAA,YACV,UAAU;AAAA,UACZ,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;AC/CA,IAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EAAS;AAAA,EAAa;AAAA,EAAqB;AAAA,EAC3C;AAAA,EAAQ;AAAA,EAAe;AAAA,EAAc;AAAA,EAAa;AAAA,EAClD;AAAA,EAAmB;AAAA,EAAgB;AAAA,EAAgB;AAAA,EACnD;AAAA,EAAe;AAAA,EAAa;AAAA,EAAY;AAC1C,CAAC;AAGD,IAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,eAAe,YAA4B;AAClD,MAAI,WAAW,WAAW,GAAG,GAAG;AAC9B,UAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,WAAO,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AAAA,EACnC;AACA,SAAO,WAAW,MAAM,GAAG,EAAE,CAAC;AAChC;AAEA,SAAS,cAAc,MAAuB;AAC5C,MAAI,KAAK,WAAW,OAAO,EAAG,QAAO;AACrC,SAAO,cAAc,IAAI,IAAI;AAC/B;AAEA,SAAS,YAAY,YAAoB,eAAqC;AAE5E,MAAI,WAAW,WAAW,IAAI,KAAK,eAAe,IAAK,QAAO;AAC9D,MAAI,WAAW,WAAW,IAAI,KAAK,WAAW,WAAW,IAAI,EAAG,QAAO;AAGvE,aAAW,UAAU,eAAe;AAClC,QAAI,eAAe,UAAU,WAAW,WAAW,SAAS,GAAG,EAAG,QAAO;AAAA,EAC3E;AAEA,SAAO;AACT;AAEO,IAAM,0BAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK;AAAA,EAEvD,MAAM,MAAmB,SAAoC;AAC3D,QAAI,CAAC,QAAQ,YAAa,QAAO,CAAC;AAElC,UAAM,WAAsB,CAAC;AAC7B,UAAM,OAAO,oBAAI,IAAY;AAE7B,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAE1C,UAAI,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,EAAG;AAEnD,YAAM,OAAO,KAAK,MAAM,CAAC;AAEzB,iBAAW,WAAW,sBAAsB;AAC1C,cAAM,QAAQ,QAAQ,KAAK,IAAI;AAC/B,YAAI,CAAC,MAAO;AAEZ,cAAM,aAAa,MAAM,CAAC;AAC1B,cAAM,UAAU,eAAe,UAAU;AAEzC,YAAI,KAAK,IAAI,OAAO,EAAG;AACvB,aAAK,IAAI,OAAO;AAEhB,YAAI,YAAY,YAAY,QAAQ,aAAa,EAAG;AACpD,YAAI,cAAc,OAAO,EAAG;AAC5B,YAAI,kBAAkB,IAAI,UAAU,KAAK,kBAAkB,IAAI,OAAO,EAAG;AACzE,YAAI,QAAQ,qBAAqB,IAAI,OAAO,EAAG;AAE/C,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,MAAM,QAAQ;AAAA,UACtB,SAAS,YAAY,OAAO;AAAA,UAC5B,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;ACzFA,IAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,iBAAuB;AAAA,EAClC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,KAAK;AAAA,EAEzC,MAAM,MAAmB,SAAoC;AAC3D,QAAI,CAAC,WAAW,KAAK,YAAY,EAAG,QAAO,CAAC;AAG5C,eAAW,WAAW,sBAAsB;AAC1C,UAAI,QAAQ,KAAK,KAAK,YAAY,EAAG,QAAO,CAAC;AAAA,IAC/C;AAGA,UAAM,WAAW,QAAQ,oBAAoB,SAAkB;AAG/D,eAAW,WAAW,eAAe;AACnC,UAAI,QAAQ,KAAK,KAAK,OAAO,EAAG,QAAO,CAAC;AAAA,IAC1C;AAGA,QAAI,cAAc;AAClB,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,sEAAsE,KAAK,KAAK,MAAM,CAAC,CAAC,GAAG;AAC7F,sBAAc,IAAI;AAClB;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,oBACpB,yFACA;AAEJ,WAAO,CAAC;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,KAAK;AAAA,MACX,MAAM;AAAA,MACN,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACF;;;AClFA,IAAM,qBAAqB;AAG3B,IAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,IAAM,kBAAwB;AAAA,EACnC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC;AAAA,EAEjB,MAAM,MAAmB,SAAoC;AAC3D,UAAM,WAAsB,CAAC;AAG7B,QAAI,KAAK,iBAAiB,cAAc;AACtC,UAAI,CAAC,QAAQ,gBAAgB;AAC3B,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AACA,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,CAAC,MAAM,OAAO,MAAM,KAAK,EAAE,SAAS,KAAK,GAAG,EAAG,QAAO;AAC3D,QAAI,CAAC,kBAAkB,KAAK,OAAO,EAAG,QAAO;AAE7C,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,EAAG;AAEnD,YAAM,OAAO,KAAK,MAAM,CAAC;AACzB,YAAM,QAAQ,IAAI,OAAO,mBAAmB,QAAQ,mBAAmB,KAAK;AAC5E,UAAI;AACJ,cAAQ,QAAQ,MAAM,KAAK,IAAI,OAAO,MAAM;AAC1C,cAAM,UAAU,MAAM,CAAC;AACvB,cAAM,cAAc,oBAAoB,IAAI,OAAO;AACnD,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,MAAM,QAAQ;AAAA,UACtB,SAAS,cACL,6BAA6B,OAAO,+BACpC,mBAAmB,OAAO;AAAA,UAC9B,UAAU,cAAc,aAAa;AAAA,UACrC,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;AC3EO,IAAM,oBAA0B;AAAA,EACrC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,KAAK;AAAA,EAEzC,MAAM,MAAmB,UAAqC;AAC5D,UAAM,WAAsB,CAAC;AAG7B,QAAI,WAAW,KAAK,YAAY,GAAG;AACjC,YAAM,cAAc,WAAW,KAAK,KAAK,OAAO;AAChD,UAAI,CAAC,aAAa;AAChB,YAAI,cAAc;AAClB,iBAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,cAAI,sEAAsE,KAAK,KAAK,MAAM,CAAC,CAAC,GAAG;AAC7F,0BAAc,IAAI;AAClB;AAAA,UACF;AAAA,QACF;AACA,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,IACF;AAGA,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,EAAG;AAEnD,YAAM,OAAO,KAAK,MAAM,CAAC;AAGzB,UAAI,iCAAiC,KAAK,IAAI,GAAG;AAC/C,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,KAAK,QAAQ,OAAO,IAAI;AAAA,UAChC,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AACD;AAAA,MACF;AAGA,UAAI,gCAAgC,KAAK,IAAI,GAAG;AAC9C,cAAM,WAAW,KAAK,MAAM,IAAI,CAAC,GAAG,KAAK;AACzC,YAAI,aAAa,KAAK;AACpB,mBAAS,KAAK;AAAA,YACZ,QAAQ;AAAA,YACR,MAAM,KAAK;AAAA,YACX,MAAM,IAAI;AAAA,YACV,QAAQ,KAAK,QAAQ,OAAO,IAAI;AAAA,YAChC,SAAS;AAAA,YACT,UAAU;AAAA,YACV,UAAU;AAAA,UACZ,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;ACzEA,IAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,sBAA4B;AAAA,EACvC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,KAAK;AAAA,EAEzC,MAAM,MAAmB,UAAqC;AAC5D,QAAI,CAAC,WAAW,KAAK,YAAY,EAAG,QAAO,CAAC;AAE5C,UAAM,eAAe,qBAAqB,KAAK,OAAK,EAAE,KAAK,KAAK,OAAO,CAAC;AACxE,QAAI,CAAC,aAAc,QAAO,CAAC;AAE3B,UAAM,gBAAgB,oBAAoB,KAAK,OAAK,EAAE,KAAK,KAAK,OAAO,CAAC;AACxE,QAAI,cAAe,QAAO,CAAC;AAE3B,QAAI,WAAW;AACf,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,qBAAqB,KAAK,OAAK,EAAE,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG;AACzD,mBAAW,IAAI;AACf;AAAA,MACF;AAAA,IACF;AAEA,WAAO,CAAC;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,KAAK;AAAA,MACX,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,UAAU;AAAA,MACV,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACF;;;ACzDA,IAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,mBAAyB;AAAA,EACpC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,KAAK;AAAA,EAEzC,MAAM,MAAmB,UAAqC;AAC5D,QAAI,CAAC,WAAW,KAAK,YAAY,EAAG,QAAO,CAAC;AAG5C,eAAW,WAAW,iBAAiB;AACrC,UAAI,QAAQ,KAAK,KAAK,YAAY,EAAG,QAAO,CAAC;AAAA,IAC/C;AAGA,eAAW,WAAW,qBAAqB;AACzC,UAAI,QAAQ,KAAK,KAAK,OAAO,EAAG,QAAO,CAAC;AAAA,IAC1C;AAGA,QAAI,cAAc;AAClB,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,sEAAsE,KAAK,KAAK,MAAM,CAAC,CAAC,GAAG;AAC7F,sBAAc,IAAI;AAClB;AAAA,MACF;AAAA,IACF;AAEA,WAAO,CAAC;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,KAAK;AAAA,MACX,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,UAAU;AAAA,MACV,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACF;;;ACjEO,IAAM,iBAAuB;AAAA,EAClC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK;AAAA,EAEvD,MAAM,MAAmB,UAAqC;AAC5D,UAAM,WAAsB,CAAC;AAE7B,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,EAAG;AAEnD,YAAM,OAAO,KAAK,MAAM,CAAC;AAEzB,UAAI,0DAA0D,KAAK,IAAI,GAAG;AACxE,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,KAAK,QAAQ,gBAAgB,IAAI;AAAA,UACzC,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAEA,UAAI,cAAc,KAAK,IAAI,GAAG;AAC5B,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,KAAK,QAAQ,OAAO,IAAI;AAAA,UAChC,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAEA,UAAI,0BAA0B,KAAK,IAAI,GAAG;AACxC,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,KAAK,QAAQ,QAAQ,IAAI;AAAA,UACjC,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAEA,UAAI,oBAAoB,KAAK,IAAI,GAAG;AAClC,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,KAAK,QAAQ,QAAQ,IAAI;AAAA,UACjC,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;ACnEA,IAAM,wBAAwB;AAC9B,IAAM,qBAAqB;AAC3B,IAAM,2BAA2B;AAE1B,IAAM,eAAqB;AAAA,EAChC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,KAAK;AAAA,EAEzC,MAAM,MAAmB,UAAqC;AAC5D,UAAM,WAAsB,CAAC;AAE7B,QAAI,kBAAkB;AACtB,QAAI,eAAe;AACnB,QAAI,mBAAmB;AAEvB,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,YAAM,OAAO,KAAK,MAAM,CAAC;AACzB,YAAM,UAAU,KAAK,KAAK;AAG1B,UAAI,KAAK,WAAW,CAAC,GAAG;AACtB,2BAAmB;AACnB;AAAA,MACF;AAGA,UAAI,QAAQ,WAAW,IAAI,GAAG;AAC5B,cAAM,YAAY,QAAQ,MAAM,oCAAoC;AACpE,YAAI,WAAW;AACb,mBAAS,KAAK;AAAA,YACZ,QAAQ;AAAA,YACR,MAAM,KAAK;AAAA,YACX,MAAM,IAAI;AAAA,YACV,QAAQ,KAAK,QAAQ,UAAU,CAAC,CAAC,IAAI;AAAA,YACrC,SAAS,GAAG,UAAU,CAAC,CAAC,aAAa,UAAU,CAAC,EAAE,KAAK,KAAK,kBAAkB;AAAA,YAC9E,UAAU;AAAA,YACV,UAAU;AAAA,UACZ,CAAC;AAAA,QACH;AAGA,cAAM,iBAAiB,QAAQ,MAAM,CAAC,EAAE,KAAK;AAC7C,cAAM,gBAAgB,qGAAqG,KAAK,cAAc,KAC5I,UAAU,KAAK,cAAc,KAC7B,SAAS,KAAK,cAAc;AAE9B,YAAI,eAAe;AACjB;AACA,cAAI,qBAAqB,0BAA0B;AACjD,qBAAS,KAAK;AAAA,cACZ,QAAQ;AAAA,cACR,MAAM,KAAK;AAAA,cACX,MAAM,IAAI,IAAI,2BAA2B;AAAA,cACzC,QAAQ;AAAA,cACR,SAAS,GAAG,wBAAwB;AAAA,cACpC,UAAU;AAAA,cACV,UAAU;AAAA,YACZ,CAAC;AAAA,UACH;AAAA,QACF,OAAO;AACL,6BAAmB;AAAA,QACrB;AACA;AAAA,MACF;AAEA,yBAAmB;AAGnB,UAAI,kEAAkE,KAAK,IAAI,GAAG;AAChF,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAGA,UAAI,oBAAoB,KAAK,IAAI,GAAG;AAClC;AAAA,MACF;AAGA,UAAI,YAAY,KAAK,IAAI,KAAK,eAAe,KAAK,IAAI,KAAK,QAAQ,KAAK,IAAI,GAAG;AAC7E;AAAA,MACF;AAAA,IACF;AAEA,QAAI,kBAAkB,uBAAuB;AAC3C,eAAS,KAAK;AAAA,QACZ,QAAQ;AAAA,QACR,MAAM,KAAK;AAAA,QACX,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS,GAAG,eAAe;AAAA,QAC3B,UAAU;AAAA,QACV,UAAU;AAAA,MACZ,CAAC;AAAA,IACH;AAEA,QAAI,eAAe,oBAAoB;AACrC,eAAS,KAAK;AAAA,QACZ,QAAQ;AAAA,QACR,MAAM,KAAK;AAAA,QACX,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS,GAAG,YAAY;AAAA,QACxB,UAAU;AAAA,QACV,UAAU;AAAA,MACZ,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AACF;;;ACzHO,IAAM,iBAAuB;AAAA,EAClC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,KAAK;AAAA,EAEzC,MAAM,MAAmB,UAAqC;AAC5D,UAAM,WAAsB,CAAC;AAE7B,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,EAAG;AAEnD,YAAM,OAAO,KAAK,MAAM,CAAC;AAIzB,UAAI,8BAA8B,KAAK,IAAI,KAAK,8BAA8B,KAAK,IAAI,GAAG;AACxF,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,KAAK,QAAQ,yBAAyB,IAAI;AAAA,UAClD,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAGA,UAAI,oBAAoB,KAAK,IAAI,GAAG;AAClC,iBAAS,KAAK;AAAA,UACZ,QAAQ;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,IAAI;AAAA,UACV,QAAQ,KAAK,QAAQ,YAAY,IAAI;AAAA,UACrC,SAAS;AAAA,UACT,UAAU;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;AC7CA,IAAM,yBAAyB;AAAA;AAAA,EAE7B,EAAE,SAAS,oEAAoE,SAAS,uFAAkF;AAAA;AAAA,EAE1K,EAAE,SAAS,2DAA2D,SAAS,6EAAwE;AAAA;AAAA,EAEvJ,EAAE,SAAS,6CAA6C,SAAS,sFAAiF;AACpJ;AAEO,IAAM,mBAAyB;AAAA,EACpC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK;AAAA,EAEvD,MAAM,MAAmB,UAAqC;AAC5D,UAAM,WAAsB,CAAC;AAE7B,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,EAAG;AAEnD,YAAM,OAAO,KAAK,MAAM,CAAC;AAEzB,iBAAW,EAAE,SAAS,QAAQ,KAAK,wBAAwB;AACzD,YAAI,QAAQ,KAAK,IAAI,GAAG;AACtB,mBAAS,KAAK;AAAA,YACZ,QAAQ;AAAA,YACR,MAAM,KAAK;AAAA,YACX,MAAM,IAAI;AAAA,YACV,QAAQ;AAAA,YACR;AAAA,YACA,UAAU;AAAA,YACV,UAAU;AAAA,UACZ,CAAC;AACD;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;AClCO,IAAM,QAAgB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;ACnBA,SAAS,WAAAC,gBAAe;AAExB,eAAsB,KAAK,SAA2C;AACpE,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,OAAOA,SAAQ,QAAQ,IAAI;AAEjC,QAAM,YAAY,MAAM,UAAU,MAAM,QAAQ,MAAM;AACtD,QAAM,UAAU,MAAM,oBAAoB,MAAM,SAAS;AAEzD,QAAM,WAAsB,CAAC;AAE7B,aAAW,gBAAgB,WAAW;AACpC,UAAM,OAAO,MAAM,gBAAgB,MAAM,YAAY;AACrD,QAAI,CAAC,KAAM;AAEX,eAAW,QAAQ,OAAO;AACxB,UACE,KAAK,eAAe,SAAS,KAC7B,CAAC,KAAK,eAAe,SAAS,KAAK,GAAG,GACtC;AACA;AAAA,MACF;AAEA,YAAM,eAAe,KAAK,MAAM,MAAM,OAAO;AAE7C,iBAAW,WAAW,cAAc;AAClC,YAAI,CAAC,iBAAiB,KAAK,OAAO,QAAQ,OAAO,GAAG,QAAQ,MAAM,GAAG;AACnE,mBAAS,KAAK,OAAO;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,EAAE,cAAc,eAAe,IAAI,gBAAgB,QAAQ;AACjE,QAAM,UAAU,kBAAkB,QAAQ;AAE1C,SAAO;AAAA,IACL,SAAS,WAAW;AAAA,IACpB,aAAa,QAAQ;AAAA,IACrB,cAAc,UAAU;AAAA,IACxB,gBAAgB,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;AAAA,IACpD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;ACpDA,OAAO,QAAQ;AAGf,IAAM,kBAAkB;AAAA,EACtB,UAAU,GAAG;AAAA,EACb,SAAS,GAAG;AAAA,EACZ,MAAM,GAAG;AACX;AAEA,IAAM,kBAAkB;AAAA,EACtB,UAAU;AAAA,EACV,SAAS;AAAA,EACT,MAAM;AACR;AAEA,SAAS,WAAW,OAAsC;AACxD,MAAI,SAAS,GAAI,QAAO,GAAG;AAC3B,MAAI,SAAS,GAAI,QAAO,GAAG;AAC3B,SAAO,GAAG;AACZ;AAEA,SAAS,YAAY,UAA6C;AAChE,QAAM,MAAM,oBAAI,IAAuB;AACvC,aAAW,KAAK,UAAU;AACxB,UAAM,QAAQ,IAAI,IAAI,EAAE,IAAI,KAAK,CAAC;AAClC,UAAM,KAAK,CAAC;AACZ,QAAI,IAAI,EAAE,MAAM,KAAK;AAAA,EACvB;AACA,SAAO;AACT;AAEO,SAAS,aAAa,QAA4B;AACvD,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,GAAG,KAAK,YAAY,IAAI,GAAG,IAAI,KAAK,OAAO,OAAO,EAAE,CAAC;AAChE,QAAM,KAAK,GAAG,IAAI,aAAa,OAAO,YAAY,aAAa,OAAO,cAAc,IAAI,CAAC;AACzF,QAAM,KAAK,EAAE;AAGb,MAAI,OAAO,SAAS,SAAS,GAAG;AAC9B,UAAM,UAAU,YAAY,OAAO,QAAQ;AAC3C,eAAW,CAAC,MAAM,QAAQ,KAAK,SAAS;AACtC,YAAM,KAAK,GAAG,UAAU,IAAI,CAAC;AAC7B,iBAAW,KAAK,UAAU;AACxB,cAAM,QAAQ,gBAAgB,EAAE,QAAQ;AACxC,cAAM,QAAQ,gBAAgB,EAAE,QAAQ;AACxC,cAAM;AAAA,UACJ,KAAK,GAAG,IAAI,GAAG,EAAE,IAAI,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,KAAK,EAAE,OAAO,KAAK,GAAG,IAAI,EAAE,MAAM,CAAC;AAAA,QAC1F;AAAA,MACF;AACA,YAAM,KAAK,EAAE;AAAA,IACf;AAAA,EACF;AAGA,QAAM,KAAK,GAAG,KAAK,UAAU,CAAC;AAC9B,aAAW,MAAM,OAAO,gBAAgB;AACtC,UAAM,QAAQ,WAAW,GAAG,KAAK;AACjC,UAAM,MAAM,UAAU,GAAG,KAAK;AAC9B,UAAM,KAAK,KAAK,GAAG,SAAS,OAAO,EAAE,CAAC,IAAI,MAAM,OAAO,GAAG,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,GAAG,IAAI,IAAI,GAAG,YAAY,UAAU,CAAC,EAAE;AAAA,EAClI;AACA,QAAM,KAAK,EAAE;AAGb,QAAM,eAAe,WAAW,OAAO,YAAY;AACnD,QAAM,KAAK,GAAG,KAAK,cAAc,aAAa,OAAO,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC;AACjF,QAAM,KAAK,EAAE;AAGb,QAAM,EAAE,UAAU,SAAS,KAAK,IAAI,OAAO;AAC3C,QAAM,QAAkB,CAAC;AACzB,MAAI,WAAW,EAAG,OAAM,KAAK,GAAG,IAAI,GAAG,QAAQ,WAAW,CAAC;AAC3D,MAAI,UAAU,EAAG,OAAM,KAAK,GAAG,OAAO,GAAG,OAAO,WAAW,CAAC;AAC5D,MAAI,OAAO,EAAG,OAAM,KAAK,GAAG,KAAK,GAAG,IAAI,OAAO,CAAC;AAChD,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,KAAK,GAAG,MAAM,oBAAoB,CAAC;AAAA,EAC3C,OAAO;AACL,UAAM,KAAK,KAAK,MAAM,KAAK,GAAG,IAAI,QAAK,CAAC,CAAC,EAAE;AAAA,EAC7C;AACA,QAAM,KAAK,EAAE;AAGb,QAAM,aAAa,OAAO,gBAAgB,KAAK,gBAAgB,OAAO,gBAAgB,KAAK,WAAW;AACtG,QAAM,WAAW,yCAAyC,OAAO,YAAY,UAAU,UAAU;AACjG,QAAM,KAAK,GAAG,IAAI,uBAAuB,CAAC;AAC1C,QAAM,KAAK,GAAG,IAAI,kBAAkB,QAAQ,0BAA0B,CAAC;AACvE,QAAM,KAAK,EAAE;AAEb,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,WAAW,QAA4B;AACrD,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;AAEA,SAAS,UAAU,OAAuB;AACxC,QAAM,QAAQ;AACd,QAAM,SAAS,KAAK,MAAO,QAAQ,MAAO,KAAK;AAC/C,QAAM,QAAQ,QAAQ;AACtB,QAAM,QAAQ,WAAW,KAAK;AAC9B,SAAO,MAAM,SAAI,OAAO,MAAM,CAAC,IAAI,GAAG,IAAI,SAAI,OAAO,KAAK,CAAC;AAC7D;;;AlBjGA,eAAe,OAAO;AACpB,QAAM,EAAE,QAAQ,YAAY,IAAI,UAAU;AAAA,IACxC,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACP,MAAM,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,MACxC,QAAQ,EAAE,MAAM,UAAU,UAAU,MAAM,SAAS,CAAC,EAAE;AAAA,MACtD,MAAM,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,MACpD,SAAS,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,IACzD;AAAA,EACF,CAAC;AAED,MAAI,OAAO,MAAM;AACf,cAAU;AACV,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,OAAO,SAAS;AAClB,YAAQ,IAAI,WAAW,CAAC;AACxB,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,aAAa,YAAY,CAAC,KAAK;AAErC,QAAM,SAAS,MAAM,KAAK;AAAA,IACxB,MAAM;AAAA,IACN,QAAQ,OAAO;AAAA,EACjB,CAAC;AAED,MAAI,OAAO,MAAM;AACf,YAAQ,IAAI,WAAW,MAAM,CAAC;AAAA,EAChC,OAAO;AACL,YAAQ,IAAI,aAAa,MAAM,CAAC;AAAA,EAClC;AAEA,MAAI,OAAO,QAAQ,WAAW,GAAG;AAC/B,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,SAAS,YAAY;AACnB,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAiBb;AACD;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,MAAM,mBAAmB,eAAe,QAAQ,IAAI,UAAU,eAAe;AACrF,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["resolve","resolve"]}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/utils/file-walker.ts
|
|
2
2
|
import fg from "fast-glob";
|
|
3
|
-
import { readFile, stat } from "fs/promises";
|
|
4
|
-
import { resolve, extname } from "path";
|
|
3
|
+
import { readFile, stat, realpath } from "fs/promises";
|
|
4
|
+
import { resolve, extname, sep } from "path";
|
|
5
5
|
|
|
6
6
|
// src/utils/patterns.ts
|
|
7
7
|
function isApiRoute(relativePath) {
|
|
@@ -148,17 +148,21 @@ async function walkFiles(root, extraIgnores = []) {
|
|
|
148
148
|
cwd: root,
|
|
149
149
|
ignore: [...DEFAULT_IGNORES, ...extraIgnores],
|
|
150
150
|
absolute: false,
|
|
151
|
-
dot: true
|
|
151
|
+
dot: true,
|
|
152
|
+
followSymbolicLinks: false
|
|
152
153
|
});
|
|
153
154
|
return files.sort();
|
|
154
155
|
}
|
|
155
156
|
async function readFileContext(root, relativePath) {
|
|
156
157
|
try {
|
|
157
158
|
const absolutePath = resolve(root, relativePath);
|
|
159
|
+
const realRoot = await realpath(root);
|
|
160
|
+
const realFile = await realpath(absolutePath);
|
|
161
|
+
if (!realFile.startsWith(realRoot + sep) && realFile !== realRoot) return null;
|
|
158
162
|
const fileStats = await stat(absolutePath);
|
|
159
163
|
if (fileStats.size > MAX_FILE_SIZE) return null;
|
|
160
164
|
const content = await readFile(absolutePath, "utf-8");
|
|
161
|
-
const lines = content.split(
|
|
165
|
+
const lines = content.split(/\r?\n|\r/);
|
|
162
166
|
return {
|
|
163
167
|
absolutePath,
|
|
164
168
|
relativePath,
|
|
@@ -1018,7 +1022,7 @@ async function scan(options) {
|
|
|
1018
1022
|
const summary = summarizeFindings(findings);
|
|
1019
1023
|
return {
|
|
1020
1024
|
version: getVersion(),
|
|
1021
|
-
scannedPath:
|
|
1025
|
+
scannedPath: options.path,
|
|
1022
1026
|
filesScanned: filePaths.length,
|
|
1023
1027
|
scanDurationMs: Math.round(performance.now() - start),
|
|
1024
1028
|
findings,
|