ship18ion 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # πŸš€ ship18ion
2
+
3
+ > **"Production Readiness Inspector" for your apps.**
4
+
5
+ `ship18ion` (read as "ship-tion") is a CLI tool designed to prevent production disasters before they happen. It scans your codebase for environment configuration issues, leaked secrets, dangerous security misconfigurations, and build artifacts that shouldn't be there.
6
+
7
+ Think of it as `eslint` but for **deployability**.
8
+
9
+ ![npm version](https://img.shields.io/npm/v/ship18ion)
10
+ ![license](https://img.shields.io/npm/l/ship18ion)
11
+
12
+ ## ✨ Features
13
+
14
+ - **🌱 Environment Hygiene**:
15
+ - Finds unused variables in your `.env` files.
16
+ - Detects usage of `process.env.VAR` that are missing definitions.
17
+ - Supports `.env`, `.env.production` and `import.meta.env` (Vite).
18
+
19
+ - **πŸ” Secret Detection**:
20
+ - Catches hardcoded secrets (AWS keys, Stripe keys, generic private keys).
21
+ - Uses entropy heuristics to find potential secrets hidden in plain sight.
22
+ - **Next.js Safety**: Warns if `NEXT_PUBLIC_` variables contain high-entropy strings (potential accidental leaks).
23
+
24
+ - **⚠️ Security & Config**:
25
+ - Alerts on `debug: true` or `NODE_ENV` mismatches.
26
+ - Detects dangerous CORS configurations (`origin: '*'`).
27
+ - Finds hardcoded database connection strings.
28
+
29
+ - **πŸ“¦ Build Safety**:
30
+ - Prevents source maps (`.map`) from leaking into production builds.
31
+ - Ensures `.env` files are not bundled into build output directories.
32
+ - Checks for dev dependencies (like `eslint`) accidentally listed in `dependencies`.
33
+
34
+ - **πŸ‘· CI/CD Ready**:
35
+ - Zero config by default.
36
+ - Returns exit code `1` on failure to block bad builds.
37
+
38
+ ## πŸ“¦ Installation
39
+
40
+ You can use it directly with `npx`:
41
+
42
+ ```bash
43
+ npx ship18ion check
44
+ ```
45
+
46
+ Or install it as a dev dependency:
47
+
48
+ ```bash
49
+ npm install --save-dev ship18ion
50
+ ```
51
+
52
+ ## πŸš€ Usage
53
+
54
+ Run the check in your project root:
55
+
56
+ ```bash
57
+ npx ship18ion
58
+ ```
59
+
60
+ ### CI Mode
61
+ For Continuous Integration pipelines (GitHub Actions, GitLab CI, etc.), use the `--ci` flag for minimal output and standard exit codes:
62
+
63
+ ```bash
64
+ npx ship18ion check --ci
65
+ ```
66
+
67
+ ## βš™οΈ Configuration
68
+
69
+ `ship18ion` works out of the box, but you can customize it by creating a `ship18ion.config.json` file in your root directory:
70
+
71
+ ```json
72
+ {
73
+ "env": {
74
+ "required": ["DATABASE_URL", "JWT_SECRET"],
75
+ "disallowed": ["DEBUG_TOKEN"]
76
+ },
77
+ "security": {
78
+ "noCorsWildcard": true,
79
+ "requireRateLimit": false
80
+ },
81
+ "ignore": [
82
+ "**/legacy-code/**",
83
+ "**/test-fixtures/**"
84
+ ]
85
+ }
86
+ ```
87
+
88
+ ## πŸ›‘οΈ Rules Breakdown
89
+
90
+ | Category | Rule | Description |
91
+ |----------|------|-------------|
92
+ | **Env** | `env-unused` | A variable is defined in `.env` but never referenced in code. |
93
+ | **Env** | `env-missing` | A variable is used in code (`process.env.X`) but not defined. |
94
+ | **Secrets** | `secret-pattern` | Matches regex for known keys (AWS, Stripe, OpenAI). |
95
+ | **Next.js** | `nextjs-public-secret` | High-entropy string found in `NEXT_PUBLIC_` variable. |
96
+ | **Security** | `security-cors` | Detects wildcard `Access-Control-Allow-Origin`. |
97
+ | **Git** | `git-dirty` | Warns if deploying with uncommitted changes. |
98
+
99
+ ## 🀝 Contributing
100
+
101
+ Contributions are welcome! Please feel free to submit a Pull Request.
102
+
103
+ ## πŸ“„ License
104
+
105
+ ISC
package/SHIPPING.md ADDED
@@ -0,0 +1,57 @@
1
+ # Shipping ship18ion
2
+
3
+ ## 1. Local Testing (Link)
4
+ To test `ship18ion` on your other local projects without publishing, use `npm link`.
5
+
6
+ 1. **In this directory (ship18ion):**
7
+ ```bash
8
+ npm run build
9
+ npm link
10
+ ```
11
+ This creates a global symlink to your local version.
12
+
13
+ 2. **In your target project directory:**
14
+ ```bash
15
+ npm link ship18ion
16
+ ```
17
+ Now you can run:
18
+ ```bash
19
+ npx ship18ion check
20
+ ```
21
+
22
+ 3. **To unlink:**
23
+ ```bash
24
+ # In target project
25
+ npm unlink -g ship18ion
26
+
27
+ # In ship18ion directory
28
+ npm unlink -g
29
+ ```
30
+
31
+ ## 2. Publishing to NPM
32
+ To share this tool with the world.
33
+
34
+ 1. **Login to NPM:**
35
+ ```bash
36
+ npm login
37
+ ```
38
+
39
+ 2. **Prepare:**
40
+ - Ensure `package.json` has the correct version.
41
+ - Run tests: `npm test` (or verifying script).
42
+
43
+ 3. **Publish:**
44
+ ```bash
45
+ npm publish
46
+ ```
47
+
48
+ If you have a scoped package (e.g. `@yourname/ship18ion`), use:
49
+ ```bash
50
+ npm publish --access public
51
+ ```
52
+
53
+ ## 3. Usage for Users
54
+ Once published, anyone can use it without installation:
55
+ ```bash
56
+ npx ship18ion@latest
57
+ ```
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const config_1 = require("../engine/config");
10
+ const runner_1 = require("../engine/runner");
11
+ const console_1 = require("../reporters/console");
12
+ const program = new commander_1.Command();
13
+ program
14
+ .name('ship18ion')
15
+ .description('Production Readiness Inspector')
16
+ .version('0.1.0');
17
+ program
18
+ .command('check', { isDefault: true })
19
+ .description('Run production readiness checks')
20
+ .option('--ci', 'Run in CI mode (minimal output, exit codes)')
21
+ .action(async (options) => {
22
+ // console.log(chalk.blue('Starting ship18ion checks...'));
23
+ const cwd = process.cwd();
24
+ const config = await (0, config_1.loadConfig)(cwd);
25
+ try {
26
+ const results = await (0, runner_1.runChecks)(config, cwd);
27
+ // Uses console reporter for both normal and CI for now (it handles exit codes)
28
+ (0, console_1.reportConsole)(results, cwd);
29
+ }
30
+ catch (e) {
31
+ console.error(chalk_1.default.red('Error running checks:'), e);
32
+ process.exit(1);
33
+ }
34
+ });
35
+ program.parse(process.argv);
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.findEnvUsages = findEnvUsages;
40
+ const fs_1 = __importDefault(require("fs"));
41
+ const parser = __importStar(require("@babel/parser"));
42
+ const traverse_1 = __importDefault(require("@babel/traverse"));
43
+ const t = __importStar(require("@babel/types"));
44
+ function findEnvUsages(filePath) {
45
+ if (!fs_1.default.existsSync(filePath))
46
+ return [];
47
+ const code = fs_1.default.readFileSync(filePath, 'utf-8');
48
+ const usages = [];
49
+ // Only parse JS/TS files
50
+ if (!/\.(js|ts|jsx|tsx)$/.test(filePath)) {
51
+ return [];
52
+ }
53
+ try {
54
+ const ast = parser.parse(code, {
55
+ sourceType: 'module',
56
+ plugins: ['typescript', 'jsx'],
57
+ });
58
+ (0, traverse_1.default)(ast, {
59
+ MemberExpression(path) {
60
+ // 1. Check for process.env.VAR
61
+ if (t.isMemberExpression(path.node.object) &&
62
+ t.isIdentifier(path.node.object.object) &&
63
+ path.node.object.object.name === 'process' &&
64
+ t.isIdentifier(path.node.object.property) &&
65
+ path.node.object.property.name === 'env') {
66
+ if (t.isIdentifier(path.node.property)) {
67
+ usages.push({
68
+ name: path.node.property.name,
69
+ line: path.node.loc?.start.line || 0,
70
+ file: filePath
71
+ });
72
+ }
73
+ else if (t.isStringLiteral(path.node.property)) {
74
+ usages.push({
75
+ name: path.node.property.value,
76
+ line: path.node.loc?.start.line || 0,
77
+ file: filePath
78
+ });
79
+ }
80
+ }
81
+ // 2. Check for import.meta.env.VAR (Vite)
82
+ // AST structure: MemberExpression
83
+ // object: MemberExpression
84
+ // object: MetaProperty (import.meta)
85
+ // property: Identifier (env)
86
+ // property: Identifier (VAR)
87
+ if (t.isMemberExpression(path.node.object) &&
88
+ t.isMetaProperty(path.node.object.object) &&
89
+ path.node.object.object.meta.name === 'import' &&
90
+ path.node.object.object.property.name === 'meta' &&
91
+ t.isIdentifier(path.node.object.property) &&
92
+ path.node.object.property.name === 'env') {
93
+ if (t.isIdentifier(path.node.property)) {
94
+ usages.push({
95
+ name: path.node.property.name,
96
+ line: path.node.loc?.start.line || 0,
97
+ file: filePath
98
+ });
99
+ }
100
+ }
101
+ }
102
+ });
103
+ }
104
+ catch (e) {
105
+ // console.warn(`Failed to parse ${filePath}:`, e);
106
+ }
107
+ return usages;
108
+ }
@@ -0,0 +1,22 @@
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.loadConfig = loadConfig;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ async function loadConfig(cwd = process.cwd()) {
10
+ const configPath = path_1.default.join(cwd, 'ship18ion.config.json');
11
+ if (fs_1.default.existsSync(configPath)) {
12
+ const content = fs_1.default.readFileSync(configPath, 'utf-8');
13
+ try {
14
+ return JSON.parse(content);
15
+ }
16
+ catch (e) {
17
+ console.error('Failed to parse config file:', e);
18
+ return {};
19
+ }
20
+ }
21
+ return {};
22
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runChecks = runChecks;
4
+ const scanner_1 = require("./scanner");
5
+ const env_1 = require("../rules/env");
6
+ const secrets_1 = require("../rules/secrets");
7
+ const security_1 = require("../rules/security");
8
+ const build_1 = require("../rules/build");
9
+ const nextjs_1 = require("../rules/frameworks/nextjs");
10
+ const git_1 = require("../rules/git");
11
+ async function runChecks(config, cwd) {
12
+ const files = await (0, scanner_1.scanFiles)(cwd, config.ignore);
13
+ const ctx = { config, files, cwd };
14
+ const results = [];
15
+ // Run all checks
16
+ results.push(...await (0, env_1.checkEnvVars)(ctx));
17
+ results.push(...await (0, secrets_1.checkSecrets)(ctx));
18
+ results.push(...await (0, security_1.checkSecurity)(ctx));
19
+ results.push(...await (0, build_1.checkDependencies)(ctx));
20
+ results.push(...await (0, build_1.checkBuild)(ctx));
21
+ results.push(...await (0, nextjs_1.checkNextJs)(ctx));
22
+ results.push(...await (0, git_1.checkGit)(ctx));
23
+ return results;
24
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scanFiles = scanFiles;
4
+ const glob_1 = require("glob");
5
+ async function scanFiles(cwd, ignore = []) {
6
+ const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/.git/**'];
7
+ // Scan for relevant files: JS/TS code, Configs (JSON/YAML), Env files
8
+ return (0, glob_1.glob)('**/*.{js,ts,jsx,tsx,json,yaml,yml,env,env.*}', {
9
+ cwd,
10
+ ignore: [...defaultIgnore, ...ignore],
11
+ absolute: true,
12
+ dot: true, // Include .env files
13
+ });
14
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SECRET_PATTERNS = void 0;
4
+ exports.calculateEntropy = calculateEntropy;
5
+ exports.isHighEntropy = isHighEntropy;
6
+ exports.SECRET_PATTERNS = [
7
+ { name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/ },
8
+ { name: 'Google API Key', regex: /AIza[0-9A-Za-z\\-_]{35}/ },
9
+ { name: 'Stripe Secret Key', regex: /sk_live_[0-9a-zA-Z]{24}/ },
10
+ { name: 'GitHub Personal Access Token', regex: /ghp_[0-9a-zA-Z]{36}/ },
11
+ { name: 'Generic Private Key', regex: /-----BEGIN .* PRIVATE KEY-----/ },
12
+ { name: 'Slack Bot Token', regex: /xoxb-[0-9]{11}-[0-9]{12}-[0-9a-zA-Z]{24}/ },
13
+ { name: 'OpenAI API Key', regex: /sk-[a-zA-Z0-9]{48}/ }
14
+ ];
15
+ function calculateEntropy(str) {
16
+ const len = str.length;
17
+ const frequencies = Array.from(str).reduce((freq, char) => {
18
+ freq[char] = (freq[char] || 0) + 1;
19
+ return freq;
20
+ }, {});
21
+ return Object.values(frequencies).reduce((sum, f) => {
22
+ const p = f / len;
23
+ return sum - (p * Math.log2(p));
24
+ }, 0);
25
+ }
26
+ function isHighEntropy(str, threshold = 4.5) {
27
+ return calculateEntropy(str) > threshold;
28
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,61 @@
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.reportConsole = reportConsole;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const CATEGORIES = {
10
+ 'env': { icon: '🌱', label: 'Environment' },
11
+ 'secret': { icon: 'πŸ”', label: 'Secrets' },
12
+ 'security': { icon: '⚠️', label: 'Security' },
13
+ 'dep': { icon: 'πŸ“¦', label: 'Dependency & Build' },
14
+ 'build': { icon: 'πŸ“¦', label: 'Dependency & Build' },
15
+ };
16
+ function getCategory(ruleId) {
17
+ const prefix = ruleId.split('-')[0];
18
+ return CATEGORIES[prefix] || { icon: '❓', label: 'Other' };
19
+ }
20
+ function reportConsole(results, cwd) {
21
+ if (results.length === 0) {
22
+ console.log(chalk_1.default.green('\nβœ… Production Readiness Check Passed!\n'));
23
+ return;
24
+ }
25
+ const fails = results.filter(r => r.status === 'fail');
26
+ const warns = results.filter(r => r.status === 'warn');
27
+ if (fails.length > 0) {
28
+ console.log(chalk_1.default.red('\n❌ Production Readiness Check Failed\n'));
29
+ }
30
+ else {
31
+ console.log(chalk_1.default.yellow('\n⚠️ Production Readiness Check Passed with Warnings\n'));
32
+ }
33
+ // Group by category
34
+ const grouped = {};
35
+ results.forEach(r => {
36
+ const cat = getCategory(r.ruleId);
37
+ const key = `${cat.icon} ${cat.label}`;
38
+ if (!grouped[key])
39
+ grouped[key] = [];
40
+ grouped[key].push(r);
41
+ });
42
+ for (const [category, items] of Object.entries(grouped)) {
43
+ console.log(chalk_1.default.bold(category));
44
+ for (const item of items) {
45
+ const sym = item.status === 'fail' ? chalk_1.default.red('βœ–') : chalk_1.default.yellow('!');
46
+ const location = item.file ? `${path_1.default.relative(cwd, item.file)}${item.line ? `:${item.line}` : ''}` : '';
47
+ console.log(` ${sym} ${item.message} ${chalk_1.default.gray(location)}`);
48
+ }
49
+ console.log('');
50
+ }
51
+ const summary = [];
52
+ if (fails.length > 0)
53
+ summary.push(chalk_1.default.red(`${fails.length} errors`));
54
+ if (warns.length > 0)
55
+ summary.push(chalk_1.default.yellow(`${warns.length} warnings`));
56
+ console.log(`Summary: ${summary.join(', ')}`);
57
+ console.log('');
58
+ if (fails.length > 0) {
59
+ process.exit(1);
60
+ }
61
+ }
@@ -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.checkDependencies = checkDependencies;
7
+ exports.checkBuild = checkBuild;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ async function checkDependencies(ctx) {
10
+ const results = [];
11
+ const packageJsons = ctx.files.filter(f => f.endsWith('package.json') && !f.includes('node_modules'));
12
+ const devToolsInProd = ['eslint', 'jest', 'mocha', 'nodemon', 'ts-node', 'typescript', 'webpack', 'babel-loader'];
13
+ for (const pkgFile of packageJsons) {
14
+ try {
15
+ const content = JSON.parse(fs_1.default.readFileSync(pkgFile, 'utf-8'));
16
+ const deps = content.dependencies || {};
17
+ for (const tool of devToolsInProd) {
18
+ if (deps[tool]) {
19
+ results.push({
20
+ status: 'warn',
21
+ message: `Dev dependency found in 'dependencies': ${tool}. Should be in 'devDependencies'?`,
22
+ ruleId: 'dep-dev-in-prod',
23
+ file: pkgFile
24
+ });
25
+ }
26
+ }
27
+ }
28
+ catch (e) {
29
+ // ignore
30
+ }
31
+ }
32
+ return results;
33
+ }
34
+ async function checkBuild(ctx) {
35
+ const results = [];
36
+ // Check for source maps in potential build dirs (dist, build, out, .next)
37
+ // Scanner ignores dist by default, but if we want to check build artifacts we might need to scan explicitly OR assumes user runs this in root.
38
+ // If the scanner ignores 'dist', we won't see them.
39
+ // So this check is effective only if scanner INCLUDES build dirs or we explicitly look for them.
40
+ // Let's explicitly check common build folders in CWD if they exist, ignoring scanner's ignore list for this specific check?
41
+ // Or just warn if we find .map files in the file list (meaning they were NOT ignored/cleaned).
42
+ const mapFiles = ctx.files.filter(f => f.endsWith('.map'));
43
+ for (const file of mapFiles) {
44
+ // Only if it looks like a build artifact
45
+ if (file.includes('/dist/') || file.includes('/build/') || file.includes('/.next/')) {
46
+ results.push({
47
+ status: 'warn',
48
+ message: 'Source map found in build output (information leak)',
49
+ ruleId: 'build-source-map',
50
+ file
51
+ });
52
+ }
53
+ }
54
+ // Check for .env in build folders
55
+ const envInBuild = ctx.files.filter(f => (f.endsWith('.env') || f.includes('.env.')) &&
56
+ (f.includes('/dist/') || f.includes('/build/') || f.includes('/.next/')));
57
+ for (const file of envInBuild) {
58
+ results.push({
59
+ status: 'fail',
60
+ message: 'Environment file found in build output!',
61
+ ruleId: 'build-env-leak',
62
+ file
63
+ });
64
+ }
65
+ return results;
66
+ }
@@ -0,0 +1,98 @@
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.checkEnvVars = checkEnvVars;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const dotenv_1 = __importDefault(require("dotenv"));
9
+ const ast_1 = require("../engine/ast");
10
+ async function checkEnvVars(ctx) {
11
+ const results = [];
12
+ const declaredEnvs = new Set();
13
+ const usedEnvs = new Map();
14
+ // 1. Find and parse .env files (definition detection)
15
+ const envFiles = ctx.files.filter(f => f.match(/\.env(\..+)?$/));
16
+ for (const file of envFiles) {
17
+ const content = fs_1.default.readFileSync(file, 'utf-8');
18
+ try {
19
+ const parsed = dotenv_1.default.parse(content);
20
+ Object.keys(parsed).forEach(k => declaredEnvs.add(k));
21
+ }
22
+ catch (e) {
23
+ results.push({
24
+ status: 'warn',
25
+ message: `Failed to parse env file: ${file}`,
26
+ ruleId: 'env-parse-error',
27
+ file: file
28
+ });
29
+ }
30
+ }
31
+ // 2. Scan for usages
32
+ const codeFiles = ctx.files.filter(f => f.match(/\.(js|ts|jsx|tsx)$/));
33
+ for (const file of codeFiles) {
34
+ const usages = (0, ast_1.findEnvUsages)(file);
35
+ for (const u of usages) {
36
+ if (!usedEnvs.has(u.name)) {
37
+ usedEnvs.set(u.name, []);
38
+ }
39
+ usedEnvs.get(u.name)?.push({ file, line: u.line });
40
+ }
41
+ }
42
+ // 3. Rule: Unused env vars
43
+ // Fail if Env vars exist but never used
44
+ for (const env of declaredEnvs) {
45
+ if (!usedEnvs.has(env)) {
46
+ // Ignore some common framework vars if needed, but strict mode says unused is bad.
47
+ results.push({
48
+ status: 'warn', // Warn for now, maybe fail? User said "Fail if Env vars exist but never used"
49
+ message: `Unused environment variable: ${env}`,
50
+ ruleId: 'env-unused',
51
+ file: envFiles[0] // Just point to first env file for now
52
+ });
53
+ }
54
+ }
55
+ // 4. Rule: Missing required env vars
56
+ // "App references process.env.X But it’s not defined anywhere"
57
+ // Also check strict list from config
58
+ const required = ctx.config.env?.required || [];
59
+ // Check missing from strict config
60
+ for (const req of required) {
61
+ if (!declaredEnvs.has(req)) {
62
+ results.push({
63
+ status: 'fail',
64
+ message: `Missing required environment variable (configured): ${req}`,
65
+ ruleId: 'env-missing-config',
66
+ });
67
+ }
68
+ }
69
+ // Check usage without definition
70
+ const commonSystemVars = ['NODE_ENV', 'PORT', 'CI'];
71
+ for (const [env, locs] of usedEnvs) {
72
+ if (!declaredEnvs.has(env) && !commonSystemVars.includes(env)) {
73
+ // Check if it is in disallowed list?
74
+ if (ctx.config.env?.disallowed?.includes(env)) {
75
+ results.push({
76
+ status: 'fail',
77
+ message: `Disallowed environment variable used: ${env}`,
78
+ ruleId: 'env-disallowed',
79
+ file: locs[0].file,
80
+ line: locs[0].line
81
+ });
82
+ }
83
+ else {
84
+ // It's used but not in .env.
85
+ // We should probably warn unless we are in strict mode.
86
+ // User said: "Fail if Required env var is missing" -> checking usage implies requirement.
87
+ results.push({
88
+ status: 'warn',
89
+ message: `Environment variable used but not defined in .env: ${env}`,
90
+ ruleId: 'env-missing-definition',
91
+ file: locs[0].file,
92
+ line: locs[0].line
93
+ });
94
+ }
95
+ }
96
+ }
97
+ return results;
98
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkNextJs = checkNextJs;
4
+ const ast_1 = require("../../engine/ast");
5
+ async function checkNextJs(ctx) {
6
+ const results = [];
7
+ // 1. Check for NEXT_PUBLIC_ secrets
8
+ const codeFiles = ctx.files.filter(f => f.match(/\.(js|ts|jsx|tsx)$/));
9
+ for (const file of codeFiles) {
10
+ const usages = (0, ast_1.findEnvUsages)(file);
11
+ for (const usage of usages) {
12
+ if (usage.name.startsWith('NEXT_PUBLIC_')) {
13
+ // Heuristic: Does it look like a secret?
14
+ // e.g. NEXT_PUBLIC_SECRET_KEY, NEXT_PUBLIC_API_SECRET
15
+ if (usage.name.match(/SECRET|PASSWORD|TOKEN|KEY|AUTH/i)) {
16
+ // Exception: PUBLIC_KEY is often safe
17
+ if (!usage.name.match(/PUBLIC_KEY/i)) {
18
+ results.push({
19
+ status: 'warn',
20
+ message: `Potential secret exposed via NEXT_PUBLIC_ variable: ${usage.name}`,
21
+ ruleId: 'nextjs-public-secret',
22
+ file: file,
23
+ line: usage.line
24
+ });
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ return results;
31
+ }