@preflight-agent/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runApiChecks = runApiChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ async function runApiChecks(dir) {
11
+ const results = [];
12
+ const apiFiles = await (0, glob_1.glob)(`${dir}/src/app/api/**/*.ts`, {
13
+ ignore: ['**/node_modules/**']
14
+ });
15
+ if (apiFiles.length === 0) {
16
+ const altFiles = await (0, glob_1.glob)(`${dir}/pages/api/**/*.ts`, { ignore: ['**/node_modules/**'] });
17
+ if (altFiles.length === 0) {
18
+ results.push({ status: 'warn', message: 'No API routes found \u2014 skipping API checks' });
19
+ return results;
20
+ }
21
+ apiFiles.push(...altFiles);
22
+ }
23
+ let validatedRoutes = 0;
24
+ const unvalidatedRoutes = [];
25
+ for (const file of apiFiles) {
26
+ const content = fs_1.default.readFileSync(file, 'utf-8');
27
+ const hasPOST = content.includes('POST') || content.includes('req.body') || content.includes('request.json()');
28
+ const hasValidation = content.includes('z.object') ||
29
+ content.includes('yup.object') ||
30
+ content.includes('v.object') ||
31
+ content.includes('.parse(') ||
32
+ content.includes('.safeParse(') ||
33
+ content.includes('.validate(');
34
+ if (hasPOST && hasValidation) {
35
+ validatedRoutes++;
36
+ }
37
+ else if (hasPOST && !hasValidation) {
38
+ unvalidatedRoutes.push(path_1.default.relative(dir, file));
39
+ }
40
+ }
41
+ if (unvalidatedRoutes.length === 0) {
42
+ results.push({ status: 'pass', message: 'Input validation (Zod/Yup) found on POST routes' });
43
+ }
44
+ else {
45
+ results.push({
46
+ status: 'warn',
47
+ message: `${unvalidatedRoutes.length} POST route${unvalidatedRoutes.length > 1 ? 's' : ''} missing input validation`,
48
+ });
49
+ for (const route of unvalidatedRoutes.slice(0, 2)) {
50
+ results.push({ status: 'warn', message: 'No input validation detected', file: route });
51
+ }
52
+ }
53
+ const allFiles = await (0, glob_1.glob)(`${dir}/src/**/*.ts`, { ignore: ['**/node_modules/**'] });
54
+ const hasRateLimit = allFiles.some(file => {
55
+ const content = fs_1.default.readFileSync(file, 'utf-8');
56
+ return (content.includes('ratelimit') ||
57
+ content.includes('rate-limit') ||
58
+ content.includes('upstash') ||
59
+ content.includes('redis') ||
60
+ content.includes('limiter'));
61
+ });
62
+ results.push(hasRateLimit
63
+ ? { status: 'pass', message: 'Rate limiting detected' }
64
+ : { status: 'warn', message: 'No rate limiting found \u2014 your API is open to abuse' });
65
+ return results;
66
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runAuthChecks = runAuthChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ async function runAuthChecks(dir) {
11
+ const results = [];
12
+ const middlewarePaths = [
13
+ path_1.default.join(dir, 'middleware.ts'),
14
+ path_1.default.join(dir, 'src/middleware.ts'),
15
+ path_1.default.join(dir, 'middleware.js'),
16
+ ];
17
+ const hasMiddleware = middlewarePaths.some(fs_1.default.existsSync);
18
+ if (hasMiddleware) {
19
+ results.push({ status: 'pass', message: 'Auth middleware file found' });
20
+ }
21
+ else {
22
+ results.push({ status: 'warn', message: 'No middleware.ts found \u2014 protected routes may be unsecured' });
23
+ }
24
+ const envExamplePath = path_1.default.join(dir, '.env.example');
25
+ if (fs_1.default.existsSync(envExamplePath)) {
26
+ const envExample = fs_1.default.readFileSync(envExamplePath, 'utf-8');
27
+ const hasAuthSecret = envExample.includes('JWT_SECRET') ||
28
+ envExample.includes('NEXTAUTH_SECRET') ||
29
+ envExample.includes('AUTH_SECRET');
30
+ if (hasAuthSecret) {
31
+ results.push({ status: 'pass', message: 'Auth secret documented in .env.example' });
32
+ }
33
+ else {
34
+ results.push({ status: 'warn', message: 'No JWT_SECRET or NEXTAUTH_SECRET in .env.example' });
35
+ }
36
+ }
37
+ const apiFiles = await (0, glob_1.glob)(`${dir}/src/app/api/**/*.ts`, { ignore: ['**/node_modules/**'] });
38
+ const suspiciousRoutes = [];
39
+ for (const file of apiFiles) {
40
+ const content = fs_1.default.readFileSync(file, 'utf-8');
41
+ const hasAuthCheck = content.includes('getServerSession') ||
42
+ content.includes('auth()') ||
43
+ content.includes('verifyToken') ||
44
+ content.includes('supabase.auth') ||
45
+ content.includes('requireAuth') ||
46
+ content.includes('middleware');
47
+ const looksPrivate = content.includes('userId') ||
48
+ content.includes('user_id') ||
49
+ content.includes('DELETE') ||
50
+ content.includes('UPDATE') ||
51
+ file.includes('/admin/') ||
52
+ file.includes('/user/') ||
53
+ file.includes('/profile/');
54
+ if (looksPrivate && !hasAuthCheck) {
55
+ suspiciousRoutes.push(path_1.default.relative(dir, file));
56
+ }
57
+ }
58
+ if (suspiciousRoutes.length > 0) {
59
+ for (const route of suspiciousRoutes.slice(0, 3)) {
60
+ results.push({
61
+ status: 'warn',
62
+ message: 'No auth check detected in route that may require it',
63
+ file: route,
64
+ });
65
+ }
66
+ }
67
+ else if (apiFiles.length > 0) {
68
+ results.push({ status: 'pass', message: 'Auth checks found in API routes' });
69
+ }
70
+ return results;
71
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runDatabaseChecks = runDatabaseChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ async function runDatabaseChecks(dir) {
11
+ const results = [];
12
+ const migrationFiles = await (0, glob_1.glob)(`${dir}/**/*.sql`, {
13
+ ignore: ['**/node_modules/**']
14
+ });
15
+ const supabaseDirs = [
16
+ path_1.default.join(dir, 'supabase'),
17
+ path_1.default.join(dir, 'src/supabase'),
18
+ ];
19
+ const hasSupabaseDir = supabaseDirs.some(fs_1.default.existsSync);
20
+ if (hasSupabaseDir || migrationFiles.length > 0) {
21
+ let hasRLS = false;
22
+ for (const file of migrationFiles) {
23
+ const content = fs_1.default.readFileSync(file, 'utf-8');
24
+ if (content.toLowerCase().includes('row level security') || content.toLowerCase().includes('enable rls')) {
25
+ hasRLS = true;
26
+ break;
27
+ }
28
+ }
29
+ results.push(hasRLS
30
+ ? { status: 'pass', message: 'Row Level Security (RLS) enabled in migrations' }
31
+ : { status: 'fail', message: 'No RLS policies found in SQL migrations \u2014 your database may be fully open' });
32
+ }
33
+ else {
34
+ results.push({ status: 'warn', message: 'No SQL migrations found \u2014 skipping RLS check' });
35
+ }
36
+ const envExamplePath = path_1.default.join(dir, '.env.example');
37
+ if (fs_1.default.existsSync(envExamplePath)) {
38
+ const env = fs_1.default.readFileSync(envExamplePath, 'utf-8');
39
+ if (env.includes('DATABASE_URL') || env.includes('SUPABASE_URL') || env.includes('NEXT_PUBLIC_SUPABASE_URL')) {
40
+ results.push({ status: 'pass', message: 'Database URL documented in .env.example' });
41
+ }
42
+ else {
43
+ results.push({ status: 'warn', message: 'No database URL found in .env.example' });
44
+ }
45
+ }
46
+ return results;
47
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runGraphqlChecks = runGraphqlChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ async function runGraphqlChecks(dir) {
11
+ const results = [];
12
+ const gqlFiles = await (0, glob_1.glob)(`${dir}/**/*.{graphql,gql}`, { ignore: ['**/node_modules/**'] });
13
+ const gqlTsFiles = await (0, glob_1.glob)(`${dir}/src/**/*.ts`, { ignore: ['**/node_modules/**'] });
14
+ const allGqlFiles = [...gqlFiles, ...gqlTsFiles.filter(f => {
15
+ const content = fs_1.default.readFileSync(f, 'utf-8');
16
+ return content.includes('graphql') || content.includes('GraphQL') || content.includes('gql`') || content.includes('apollo') || content.includes('urql');
17
+ })];
18
+ if (allGqlFiles.length === 0) {
19
+ results.push({ status: 'warn', message: 'No GraphQL files detected \u2014 skipping GraphQL checks' });
20
+ return results;
21
+ }
22
+ let hasDepthLimit = false;
23
+ let depthFile;
24
+ for (const file of allGqlFiles) {
25
+ const content = fs_1.default.readFileSync(file, 'utf-8');
26
+ if (content.includes('depthLimit') ||
27
+ content.includes('queryDepth') ||
28
+ content.includes('maxDepth') ||
29
+ content.includes('validationRules') ||
30
+ content.includes('complexity')) {
31
+ hasDepthLimit = true;
32
+ depthFile = path_1.default.relative(dir, file);
33
+ break;
34
+ }
35
+ }
36
+ if (hasDepthLimit) {
37
+ results.push({ status: 'pass', message: 'GraphQL query depth limiting found', file: depthFile });
38
+ }
39
+ else {
40
+ results.push({ status: 'warn', message: 'No GraphQL depth limiting found \u2014 malicious queries can exhaust your server' });
41
+ }
42
+ let hasAuth = false;
43
+ let authFile;
44
+ for (const file of allGqlFiles) {
45
+ const content = fs_1.default.readFileSync(file, 'utf-8');
46
+ if (content.includes('context') && (content.includes('token') ||
47
+ content.includes('auth') ||
48
+ content.includes('session') ||
49
+ content.includes('bearer'))) {
50
+ hasAuth = true;
51
+ authFile = path_1.default.relative(dir, file);
52
+ break;
53
+ }
54
+ }
55
+ if (hasAuth) {
56
+ results.push({ status: 'pass', message: 'GraphQL authentication context found', file: authFile });
57
+ }
58
+ else {
59
+ results.push({ status: 'warn', message: 'No GraphQL auth context detected \u2014 your GraphQL endpoint may be public' });
60
+ }
61
+ let introspectionDisabled = false;
62
+ for (const file of allGqlFiles) {
63
+ const content = fs_1.default.readFileSync(file, 'utf-8');
64
+ if (content.includes('introspection') && (content.includes('false') || content.includes('disabled') || content.includes('0'))) {
65
+ introspectionDisabled = true;
66
+ break;
67
+ }
68
+ }
69
+ results.push(introspectionDisabled
70
+ ? { status: 'pass', message: 'GraphQL introspection disabled in production' }
71
+ : { status: 'warn', message: 'GraphQL introspection may be enabled \u2014 attackers can discover your entire schema' });
72
+ return results;
73
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runVulnerabilityChecks = exports.runRealtimeChecks = exports.runGraphqlChecks = exports.runWebChecks = exports.runApiChecks = exports.runDatabaseChecks = exports.runPaymentChecks = exports.runAuthChecks = exports.runSecurityChecks = void 0;
4
+ var security_1 = require("./security");
5
+ Object.defineProperty(exports, "runSecurityChecks", { enumerable: true, get: function () { return security_1.runSecurityChecks; } });
6
+ var auth_1 = require("./auth");
7
+ Object.defineProperty(exports, "runAuthChecks", { enumerable: true, get: function () { return auth_1.runAuthChecks; } });
8
+ var payments_1 = require("./payments");
9
+ Object.defineProperty(exports, "runPaymentChecks", { enumerable: true, get: function () { return payments_1.runPaymentChecks; } });
10
+ var database_1 = require("./database");
11
+ Object.defineProperty(exports, "runDatabaseChecks", { enumerable: true, get: function () { return database_1.runDatabaseChecks; } });
12
+ var api_1 = require("./api");
13
+ Object.defineProperty(exports, "runApiChecks", { enumerable: true, get: function () { return api_1.runApiChecks; } });
14
+ var web_1 = require("./web");
15
+ Object.defineProperty(exports, "runWebChecks", { enumerable: true, get: function () { return web_1.runWebChecks; } });
16
+ var graphql_1 = require("./graphql");
17
+ Object.defineProperty(exports, "runGraphqlChecks", { enumerable: true, get: function () { return graphql_1.runGraphqlChecks; } });
18
+ var realtime_1 = require("./realtime");
19
+ Object.defineProperty(exports, "runRealtimeChecks", { enumerable: true, get: function () { return realtime_1.runRealtimeChecks; } });
20
+ var vulnerabilities_1 = require("./vulnerabilities");
21
+ Object.defineProperty(exports, "runVulnerabilityChecks", { enumerable: true, get: function () { return vulnerabilities_1.runVulnerabilityChecks; } });
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runPaymentChecks = runPaymentChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ async function runPaymentChecks(dir) {
11
+ const results = [];
12
+ const webhookFiles = await (0, glob_1.glob)(`${dir}/**/{webhook,webhooks,mpesa,stripe,payment}/**/*.{ts,tsx,js}`, { ignore: ['**/node_modules/**', '**/.next/**'] });
13
+ const apiFiles = await (0, glob_1.glob)(`${dir}/src/app/api/**/*.ts`, { ignore: ['**/node_modules/**'] });
14
+ const allFiles = [...new Set([...webhookFiles, ...apiFiles])];
15
+ const paymentRelatedFiles = allFiles.filter(f => /webhook|stripe|mpesa|payment|daraja|stk/i.test(f));
16
+ if (paymentRelatedFiles.length === 0) {
17
+ results.push({ status: 'warn', message: 'No payment-related files detected \u2014 skipping payment checks' });
18
+ return results;
19
+ }
20
+ let hasSignatureValidation = false;
21
+ let signatureFile;
22
+ for (const file of paymentRelatedFiles) {
23
+ const content = fs_1.default.readFileSync(file, 'utf-8');
24
+ if (content.includes('constructEvent') ||
25
+ content.includes('stripe.webhooks') ||
26
+ content.includes('X-Mpesa-Signature') ||
27
+ content.includes('validateWebhook') ||
28
+ content.includes('verifySignature') ||
29
+ content.includes('crypto.timingSafeEqual') ||
30
+ content.includes('hmac')) {
31
+ hasSignatureValidation = true;
32
+ signatureFile = path_1.default.relative(dir, file);
33
+ break;
34
+ }
35
+ }
36
+ if (hasSignatureValidation) {
37
+ results.push({ status: 'pass', message: 'Webhook signature validation found', file: signatureFile });
38
+ }
39
+ else {
40
+ const firstFile = path_1.default.relative(dir, paymentRelatedFiles[0]);
41
+ results.push({
42
+ status: 'fail',
43
+ message: 'No webhook signature validation found \u2014 anyone can fake payment events',
44
+ file: firstFile,
45
+ });
46
+ }
47
+ let hasErrorHandling = false;
48
+ for (const file of paymentRelatedFiles) {
49
+ const content = fs_1.default.readFileSync(file, 'utf-8');
50
+ if (content.includes('try') && content.includes('catch')) {
51
+ hasErrorHandling = true;
52
+ break;
53
+ }
54
+ }
55
+ if (hasErrorHandling) {
56
+ results.push({ status: 'pass', message: 'Error handling (try/catch) found in payment routes' });
57
+ }
58
+ else {
59
+ results.push({ status: 'warn', message: 'No try/catch found in payment files \u2014 unhandled failures will crash' });
60
+ }
61
+ let hasIdempotency = false;
62
+ for (const file of paymentRelatedFiles) {
63
+ const content = fs_1.default.readFileSync(file, 'utf-8');
64
+ if (content.includes('idempotencyKey') ||
65
+ content.includes('idempotency_key') ||
66
+ content.includes('TransactionID') ||
67
+ content.includes('CheckoutRequestID')) {
68
+ hasIdempotency = true;
69
+ break;
70
+ }
71
+ }
72
+ results.push(hasIdempotency
73
+ ? { status: 'pass', message: 'Idempotency keys found in payment flow' }
74
+ : { status: 'warn', message: 'No idempotency keys detected \u2014 duplicate payments may occur' });
75
+ return results;
76
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runRealtimeChecks = runRealtimeChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ async function runRealtimeChecks(dir) {
11
+ const results = [];
12
+ const tsFiles = await (0, glob_1.glob)(`${dir}/src/**/*.ts`, { ignore: ['**/node_modules/**', '**/.next/**'] });
13
+ const realtimeFiles = tsFiles.filter(f => {
14
+ const content = fs_1.default.readFileSync(f, 'utf-8');
15
+ return (content.includes('websocket') ||
16
+ content.includes('WebSocket') ||
17
+ content.includes('ws://') ||
18
+ content.includes('wss://') ||
19
+ content.includes('socket') ||
20
+ content.includes('subscription') ||
21
+ content.includes('realtime') ||
22
+ content.includes('supabase') && content.includes('channel') ||
23
+ content.includes('broadcast') ||
24
+ content.includes('presence'));
25
+ });
26
+ if (realtimeFiles.length === 0) {
27
+ results.push({ status: 'warn', message: 'No real-time features detected \u2014 skipping real-time checks' });
28
+ return results;
29
+ }
30
+ let hasCleanup = false;
31
+ let cleanupFile;
32
+ for (const file of realtimeFiles) {
33
+ const content = fs_1.default.readFileSync(file, 'utf-8');
34
+ if (content.includes('unsubscribe') ||
35
+ content.includes('unmount') ||
36
+ content.includes('cleanup') ||
37
+ content.includes('disconnect') ||
38
+ content.includes('removeChannel') ||
39
+ content.includes('destroy') ||
40
+ content.includes('close()')) {
41
+ hasCleanup = true;
42
+ cleanupFile = path_1.default.relative(dir, file);
43
+ break;
44
+ }
45
+ }
46
+ if (hasCleanup) {
47
+ results.push({ status: 'pass', message: 'Real-time connection cleanup found', file: cleanupFile });
48
+ }
49
+ else {
50
+ results.push({ status: 'warn', message: 'No real-time cleanup detected \u2014 connections may leak and cause memory issues' });
51
+ }
52
+ let hasReconnect = false;
53
+ for (const file of realtimeFiles) {
54
+ const content = fs_1.default.readFileSync(file, 'utf-8');
55
+ if (content.includes('reconnect') ||
56
+ content.includes('retry') ||
57
+ content.includes('backoff')) {
58
+ hasReconnect = true;
59
+ break;
60
+ }
61
+ }
62
+ results.push(hasReconnect
63
+ ? { status: 'pass', message: 'Reconnection logic found for real-time connections' }
64
+ : { status: 'warn', message: 'No reconnection logic detected \u2014 temporary network issues will drop connections permanently' });
65
+ let hasAuth = false;
66
+ for (const file of realtimeFiles) {
67
+ const content = fs_1.default.readFileSync(file, 'utf-8');
68
+ if (content.includes('token') ||
69
+ content.includes('auth') && (content.includes('channel') || content.includes('socket')) ||
70
+ content.includes('accessToken')) {
71
+ hasAuth = true;
72
+ break;
73
+ }
74
+ }
75
+ results.push(hasAuth
76
+ ? { status: 'pass', message: 'Authentication found on real-time connections' }
77
+ : { status: 'warn', message: 'No authentication detected on real-time channels \u2014 anyone can connect' });
78
+ return results;
79
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runSecurityChecks = runSecurityChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ const DANGEROUS_PATTERNS = [
11
+ { pattern: /supabase.*service_role/i, label: 'Supabase service role key' },
12
+ { pattern: /sk-[a-zA-Z0-9]{20,}/, label: 'OpenAI API key' },
13
+ { pattern: /STRIPE_SECRET_KEY/, label: 'Stripe secret key' },
14
+ { pattern: /ANTHROPIC_API_KEY/, label: 'Anthropic API key' },
15
+ { pattern: /Consumer_Secret/i, label: 'M-Pesa consumer secret' },
16
+ ];
17
+ const CLIENT_SIDE_DIRS = ['components', 'app', 'pages', 'src/app', 'src/pages', 'src/components', 'src/lib'];
18
+ async function runSecurityChecks(dir) {
19
+ const results = [];
20
+ const gitignorePath = path_1.default.join(dir, '.gitignore');
21
+ if (fs_1.default.existsSync(gitignorePath)) {
22
+ const gitignore = fs_1.default.readFileSync(gitignorePath, 'utf-8');
23
+ if (gitignore.includes('.env')) {
24
+ results.push({ status: 'pass', message: '.env is gitignored' });
25
+ }
26
+ else {
27
+ results.push({ status: 'fail', message: '.env is NOT gitignored \u2014 your secrets will be committed' });
28
+ }
29
+ }
30
+ else {
31
+ results.push({ status: 'warn', message: 'No .gitignore found' });
32
+ }
33
+ const envExamplePath = path_1.default.join(dir, '.env.example');
34
+ if (fs_1.default.existsSync(envExamplePath)) {
35
+ results.push({ status: 'pass', message: '.env.example exists' });
36
+ }
37
+ else {
38
+ results.push({ status: 'warn', message: 'No .env.example \u2014 collaborators won\'t know what vars to set' });
39
+ }
40
+ const clientFiles = [];
41
+ for (const clientDir of CLIENT_SIDE_DIRS) {
42
+ const fullDir = path_1.default.join(dir, clientDir);
43
+ if (fs_1.default.existsSync(fullDir)) {
44
+ const files = await (0, glob_1.glob)(`${fullDir}/**/*.{ts,tsx,js,jsx}`, { ignore: ['**/node_modules/**'] });
45
+ clientFiles.push(...files);
46
+ }
47
+ }
48
+ for (const file of clientFiles) {
49
+ const content = fs_1.default.readFileSync(file, 'utf-8');
50
+ const lines = content.split('\n');
51
+ for (const { pattern, label } of DANGEROUS_PATTERNS) {
52
+ for (let i = 0; i < lines.length; i++) {
53
+ if (pattern.test(lines[i]) && !lines[i].trim().startsWith('//') && !lines[i].trim().startsWith('*')) {
54
+ const relativePath = path_1.default.relative(dir, file);
55
+ results.push({
56
+ status: 'fail',
57
+ message: `${label} found in client-side code`,
58
+ file: relativePath,
59
+ line: i + 1,
60
+ });
61
+ }
62
+ }
63
+ }
64
+ }
65
+ if (clientFiles.length > 0 && !results.some(r => r.status === 'fail' && r.message.includes('found in client-side'))) {
66
+ results.push({ status: 'pass', message: 'No dangerous secrets found in client-side code' });
67
+ }
68
+ return results;
69
+ }