@scuton/dotenv-guard 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/.github/ISSUE_TEMPLATE/bug_report.md +22 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/workflows/ci.yml +31 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/auto.d.mts +2 -0
- package/dist/auto.d.ts +2 -0
- package/dist/auto.js +81 -0
- package/dist/auto.mjs +30 -0
- package/dist/chunk-QXZQR35R.mjs +222 -0
- package/dist/chunk-RLNPDDSP.mjs +19 -0
- package/dist/chunk-YYFNUR7Y.mjs +54 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +466 -0
- package/dist/cli.mjs +211 -0
- package/dist/index.d.mts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +331 -0
- package/dist/index.mjs +50 -0
- package/package.json +49 -0
- package/src/auto.ts +28 -0
- package/src/cli.ts +225 -0
- package/src/core/generator.ts +49 -0
- package/src/core/leak.ts +115 -0
- package/src/core/parser.ts +50 -0
- package/src/core/sync.ts +34 -0
- package/src/core/validator.ts +126 -0
- package/src/index.ts +42 -0
- package/src/utils/colors.ts +13 -0
- package/src/utils/helpers.ts +20 -0
- package/tests/generator.test.ts +104 -0
- package/tests/leak.test.ts +52 -0
- package/tests/parser.test.ts +82 -0
- package/tests/sync.test.ts +78 -0
- package/tests/validator.test.ts +127 -0
- package/tsconfig.json +21 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
interface EnvEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
line: number;
|
|
5
|
+
comment?: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
}
|
|
8
|
+
declare function parseEnvFile(content: string): EnvEntry[];
|
|
9
|
+
declare function parseEnvFileToMap(content: string): Map<string, string>;
|
|
10
|
+
|
|
11
|
+
interface SyncResult {
|
|
12
|
+
missing: string[];
|
|
13
|
+
extra: string[];
|
|
14
|
+
synced: string[];
|
|
15
|
+
examplePath: string;
|
|
16
|
+
envPath: string;
|
|
17
|
+
}
|
|
18
|
+
declare function syncCheck(envPath?: string, examplePath?: string): SyncResult;
|
|
19
|
+
|
|
20
|
+
type EnvType = 'string' | 'number' | 'boolean' | 'url' | 'email' | 'port' | 'enum';
|
|
21
|
+
interface ValidationRule {
|
|
22
|
+
key: string;
|
|
23
|
+
type?: EnvType;
|
|
24
|
+
required?: boolean;
|
|
25
|
+
enum?: string[];
|
|
26
|
+
pattern?: RegExp;
|
|
27
|
+
min?: number;
|
|
28
|
+
max?: number;
|
|
29
|
+
}
|
|
30
|
+
interface ValidationError {
|
|
31
|
+
key: string;
|
|
32
|
+
message: string;
|
|
33
|
+
value?: string;
|
|
34
|
+
}
|
|
35
|
+
declare function validate(env: Record<string, string | undefined>, rules: ValidationRule[]): ValidationError[];
|
|
36
|
+
declare function inferRules(exampleContent: string): ValidationRule[];
|
|
37
|
+
|
|
38
|
+
interface LeakResult {
|
|
39
|
+
leaks: LeakEntry[];
|
|
40
|
+
gitignoreHasEnv: boolean;
|
|
41
|
+
preCommitHookInstalled: boolean;
|
|
42
|
+
}
|
|
43
|
+
interface LeakEntry {
|
|
44
|
+
file: string;
|
|
45
|
+
line: number;
|
|
46
|
+
key: string;
|
|
47
|
+
severity: 'high' | 'medium' | 'low';
|
|
48
|
+
}
|
|
49
|
+
declare function checkLeaks(dir?: string): LeakResult;
|
|
50
|
+
declare function installPreCommitHook(dir?: string): void;
|
|
51
|
+
|
|
52
|
+
declare function generateExample(envPath?: string, outputPath?: string): string;
|
|
53
|
+
declare function ensureGitignore(dir?: string): boolean;
|
|
54
|
+
|
|
55
|
+
declare function guard(options?: {
|
|
56
|
+
envPath?: string;
|
|
57
|
+
examplePath?: string;
|
|
58
|
+
exitOnError?: boolean;
|
|
59
|
+
}): {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
errors: string[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export { type EnvEntry, type EnvType, type LeakEntry, type LeakResult, type SyncResult, type ValidationError, type ValidationRule, checkLeaks, ensureGitignore, generateExample, guard, inferRules, installPreCommitHook, parseEnvFile, parseEnvFileToMap, syncCheck, validate };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
interface EnvEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
line: number;
|
|
5
|
+
comment?: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
}
|
|
8
|
+
declare function parseEnvFile(content: string): EnvEntry[];
|
|
9
|
+
declare function parseEnvFileToMap(content: string): Map<string, string>;
|
|
10
|
+
|
|
11
|
+
interface SyncResult {
|
|
12
|
+
missing: string[];
|
|
13
|
+
extra: string[];
|
|
14
|
+
synced: string[];
|
|
15
|
+
examplePath: string;
|
|
16
|
+
envPath: string;
|
|
17
|
+
}
|
|
18
|
+
declare function syncCheck(envPath?: string, examplePath?: string): SyncResult;
|
|
19
|
+
|
|
20
|
+
type EnvType = 'string' | 'number' | 'boolean' | 'url' | 'email' | 'port' | 'enum';
|
|
21
|
+
interface ValidationRule {
|
|
22
|
+
key: string;
|
|
23
|
+
type?: EnvType;
|
|
24
|
+
required?: boolean;
|
|
25
|
+
enum?: string[];
|
|
26
|
+
pattern?: RegExp;
|
|
27
|
+
min?: number;
|
|
28
|
+
max?: number;
|
|
29
|
+
}
|
|
30
|
+
interface ValidationError {
|
|
31
|
+
key: string;
|
|
32
|
+
message: string;
|
|
33
|
+
value?: string;
|
|
34
|
+
}
|
|
35
|
+
declare function validate(env: Record<string, string | undefined>, rules: ValidationRule[]): ValidationError[];
|
|
36
|
+
declare function inferRules(exampleContent: string): ValidationRule[];
|
|
37
|
+
|
|
38
|
+
interface LeakResult {
|
|
39
|
+
leaks: LeakEntry[];
|
|
40
|
+
gitignoreHasEnv: boolean;
|
|
41
|
+
preCommitHookInstalled: boolean;
|
|
42
|
+
}
|
|
43
|
+
interface LeakEntry {
|
|
44
|
+
file: string;
|
|
45
|
+
line: number;
|
|
46
|
+
key: string;
|
|
47
|
+
severity: 'high' | 'medium' | 'low';
|
|
48
|
+
}
|
|
49
|
+
declare function checkLeaks(dir?: string): LeakResult;
|
|
50
|
+
declare function installPreCommitHook(dir?: string): void;
|
|
51
|
+
|
|
52
|
+
declare function generateExample(envPath?: string, outputPath?: string): string;
|
|
53
|
+
declare function ensureGitignore(dir?: string): boolean;
|
|
54
|
+
|
|
55
|
+
declare function guard(options?: {
|
|
56
|
+
envPath?: string;
|
|
57
|
+
examplePath?: string;
|
|
58
|
+
exitOnError?: boolean;
|
|
59
|
+
}): {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
errors: string[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export { type EnvEntry, type EnvType, type LeakEntry, type LeakResult, type SyncResult, type ValidationError, type ValidationRule, checkLeaks, ensureGitignore, generateExample, guard, inferRules, installPreCommitHook, parseEnvFile, parseEnvFileToMap, syncCheck, validate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
checkLeaks: () => checkLeaks,
|
|
24
|
+
ensureGitignore: () => ensureGitignore,
|
|
25
|
+
generateExample: () => generateExample,
|
|
26
|
+
guard: () => guard,
|
|
27
|
+
inferRules: () => inferRules,
|
|
28
|
+
installPreCommitHook: () => installPreCommitHook,
|
|
29
|
+
parseEnvFile: () => parseEnvFile,
|
|
30
|
+
parseEnvFileToMap: () => parseEnvFileToMap,
|
|
31
|
+
syncCheck: () => syncCheck,
|
|
32
|
+
validate: () => validate
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
|
|
36
|
+
// src/core/parser.ts
|
|
37
|
+
function parseEnvFile(content) {
|
|
38
|
+
const entries = [];
|
|
39
|
+
const lines = content.split("\n");
|
|
40
|
+
for (let i = 0; i < lines.length; i++) {
|
|
41
|
+
const raw = lines[i];
|
|
42
|
+
const trimmed = raw.trim();
|
|
43
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
44
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/);
|
|
45
|
+
if (!match) continue;
|
|
46
|
+
const key = match[1];
|
|
47
|
+
let value = match[2];
|
|
48
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
49
|
+
value = value.slice(1, -1);
|
|
50
|
+
}
|
|
51
|
+
const commentMatch = value.match(/\s+#\s/);
|
|
52
|
+
let comment;
|
|
53
|
+
if (commentMatch && !match[2].startsWith('"') && !match[2].startsWith("'")) {
|
|
54
|
+
comment = value.slice(commentMatch.index + commentMatch[0].length);
|
|
55
|
+
value = value.slice(0, commentMatch.index);
|
|
56
|
+
}
|
|
57
|
+
entries.push({ key, value, line: i + 1, comment, raw });
|
|
58
|
+
}
|
|
59
|
+
return entries;
|
|
60
|
+
}
|
|
61
|
+
function parseEnvFileToMap(content) {
|
|
62
|
+
const entries = parseEnvFile(content);
|
|
63
|
+
return new Map(entries.map((e) => [e.key, e.value]));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/core/sync.ts
|
|
67
|
+
var import_fs = require("fs");
|
|
68
|
+
function syncCheck(envPath = ".env", examplePath = ".env.example") {
|
|
69
|
+
if (!(0, import_fs.existsSync)(examplePath)) {
|
|
70
|
+
throw new Error(`${examplePath} not found. Run "dotenv-guard init" to create one.`);
|
|
71
|
+
}
|
|
72
|
+
const exampleContent = (0, import_fs.readFileSync)(examplePath, "utf-8");
|
|
73
|
+
const exampleKeys = new Set(parseEnvFile(exampleContent).map((e) => e.key));
|
|
74
|
+
let envKeys = /* @__PURE__ */ new Set();
|
|
75
|
+
if ((0, import_fs.existsSync)(envPath)) {
|
|
76
|
+
const envContent = (0, import_fs.readFileSync)(envPath, "utf-8");
|
|
77
|
+
envKeys = new Set(parseEnvFile(envContent).map((e) => e.key));
|
|
78
|
+
}
|
|
79
|
+
const missing = [...exampleKeys].filter((k) => !envKeys.has(k));
|
|
80
|
+
const extra = [...envKeys].filter((k) => !exampleKeys.has(k));
|
|
81
|
+
const synced = [...exampleKeys].filter((k) => envKeys.has(k));
|
|
82
|
+
return { missing, extra, synced, examplePath, envPath };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/core/validator.ts
|
|
86
|
+
function validate(env, rules) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
for (const rule of rules) {
|
|
89
|
+
const value = env[rule.key];
|
|
90
|
+
if (rule.required !== false && (value === void 0 || value === "")) {
|
|
91
|
+
errors.push({ key: rule.key, message: "is required but missing or empty" });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (value === void 0 || value === "") continue;
|
|
95
|
+
switch (rule.type) {
|
|
96
|
+
case "number": {
|
|
97
|
+
const num = Number(value);
|
|
98
|
+
if (isNaN(num)) {
|
|
99
|
+
errors.push({ key: rule.key, value, message: `must be a number, got "${value}"` });
|
|
100
|
+
} else {
|
|
101
|
+
if (rule.min !== void 0 && num < rule.min)
|
|
102
|
+
errors.push({ key: rule.key, value, message: `must be >= ${rule.min}` });
|
|
103
|
+
if (rule.max !== void 0 && num > rule.max)
|
|
104
|
+
errors.push({ key: rule.key, value, message: `must be <= ${rule.max}` });
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "boolean":
|
|
109
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(value.toLowerCase()))
|
|
110
|
+
errors.push({ key: rule.key, value, message: "must be a boolean (true/false/1/0/yes/no)" });
|
|
111
|
+
break;
|
|
112
|
+
case "url":
|
|
113
|
+
try {
|
|
114
|
+
new URL(value);
|
|
115
|
+
} catch {
|
|
116
|
+
errors.push({ key: rule.key, value, message: "must be a valid URL" });
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case "email":
|
|
120
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
|
121
|
+
errors.push({ key: rule.key, value, message: "must be a valid email" });
|
|
122
|
+
break;
|
|
123
|
+
case "port": {
|
|
124
|
+
const port = Number(value);
|
|
125
|
+
if (isNaN(port) || port < 1 || port > 65535)
|
|
126
|
+
errors.push({ key: rule.key, value, message: "must be a valid port (1-65535)" });
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "enum":
|
|
130
|
+
if (rule.enum && !rule.enum.includes(value))
|
|
131
|
+
errors.push({ key: rule.key, value, message: `must be one of: ${rule.enum.join(", ")}` });
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
if (rule.pattern && !rule.pattern.test(value))
|
|
135
|
+
errors.push({ key: rule.key, value, message: `does not match required pattern` });
|
|
136
|
+
}
|
|
137
|
+
return errors;
|
|
138
|
+
}
|
|
139
|
+
function inferRules(exampleContent) {
|
|
140
|
+
const lines = exampleContent.split("\n");
|
|
141
|
+
const rules = [];
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/);
|
|
144
|
+
if (!match) continue;
|
|
145
|
+
const key = match[1];
|
|
146
|
+
const rest = match[2];
|
|
147
|
+
const rule = { key, required: true };
|
|
148
|
+
const commentMatch = rest.match(/#\s*type:\s*(\w+)/i);
|
|
149
|
+
if (commentMatch) {
|
|
150
|
+
rule.type = commentMatch[1].toLowerCase();
|
|
151
|
+
}
|
|
152
|
+
if (rest.match(/#.*optional/i)) {
|
|
153
|
+
rule.required = false;
|
|
154
|
+
}
|
|
155
|
+
const enumMatch = rest.match(/#\s*enum:\s*([^\s]+)/i);
|
|
156
|
+
if (enumMatch) {
|
|
157
|
+
rule.type = "enum";
|
|
158
|
+
rule.enum = enumMatch[1].split(",");
|
|
159
|
+
}
|
|
160
|
+
if (!rule.type) {
|
|
161
|
+
const value = rest.split("#")[0].trim().replace(/^["']|["']$/g, "");
|
|
162
|
+
if (value.match(/^https?:\/\//)) rule.type = "url";
|
|
163
|
+
else if (value.match(/^\d+$/) && key.match(/PORT/i)) rule.type = "port";
|
|
164
|
+
else if (value.match(/^(true|false)$/i)) rule.type = "boolean";
|
|
165
|
+
else if (value.match(/^\d+$/)) rule.type = "number";
|
|
166
|
+
}
|
|
167
|
+
rules.push(rule);
|
|
168
|
+
}
|
|
169
|
+
return rules;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/core/leak.ts
|
|
173
|
+
var import_child_process = require("child_process");
|
|
174
|
+
var import_fs2 = require("fs");
|
|
175
|
+
var SENSITIVE_PATTERNS = [
|
|
176
|
+
/API[_-]?KEY/i,
|
|
177
|
+
/SECRET/i,
|
|
178
|
+
/PASSWORD/i,
|
|
179
|
+
/TOKEN/i,
|
|
180
|
+
/PRIVATE[_-]?KEY/i,
|
|
181
|
+
/DATABASE[_-]?URL/i,
|
|
182
|
+
/MONGO[_-]?URI/i,
|
|
183
|
+
/REDIS[_-]?URL/i,
|
|
184
|
+
/AWS[_-]?ACCESS/i,
|
|
185
|
+
/STRIPE/i,
|
|
186
|
+
/SENDGRID/i,
|
|
187
|
+
/TWILIO/i,
|
|
188
|
+
/ANTHROPIC/i,
|
|
189
|
+
/OPENAI/i
|
|
190
|
+
];
|
|
191
|
+
function checkLeaks(dir = ".") {
|
|
192
|
+
const leaks = [];
|
|
193
|
+
let gitignoreHasEnv = false;
|
|
194
|
+
const gitignorePath = `${dir}/.gitignore`;
|
|
195
|
+
if ((0, import_fs2.existsSync)(gitignorePath)) {
|
|
196
|
+
const content = (0, import_fs2.readFileSync)(gitignorePath, "utf-8");
|
|
197
|
+
gitignoreHasEnv = content.split("\n").some(
|
|
198
|
+
(line) => line.trim() === ".env" || line.trim() === ".env*" || line.trim() === ".env.local"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const tracked = (0, import_child_process.execSync)("git ls-files", { cwd: dir, encoding: "utf-8" });
|
|
203
|
+
const envFiles = tracked.split("\n").filter(
|
|
204
|
+
(f) => f.match(/^\.env($|\.)/) && !f.endsWith(".example") && !f.endsWith(".template")
|
|
205
|
+
);
|
|
206
|
+
for (const file of envFiles) {
|
|
207
|
+
const content = (0, import_fs2.readFileSync)(`${dir}/${file}`, "utf-8");
|
|
208
|
+
const lines = content.split("\n");
|
|
209
|
+
for (let i = 0; i < lines.length; i++) {
|
|
210
|
+
const match = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)/);
|
|
211
|
+
if (match && match[2].trim().length > 0) {
|
|
212
|
+
const key = match[1];
|
|
213
|
+
const severity = SENSITIVE_PATTERNS.some((p) => p.test(key)) ? "high" : "medium";
|
|
214
|
+
leaks.push({ file, line: i + 1, key, severity });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
let preCommitHookInstalled = false;
|
|
221
|
+
const hookPath = `${dir}/.git/hooks/pre-commit`;
|
|
222
|
+
if ((0, import_fs2.existsSync)(hookPath)) {
|
|
223
|
+
const hookContent = (0, import_fs2.readFileSync)(hookPath, "utf-8");
|
|
224
|
+
preCommitHookInstalled = hookContent.includes("dotenv-guard");
|
|
225
|
+
}
|
|
226
|
+
return { leaks, gitignoreHasEnv, preCommitHookInstalled };
|
|
227
|
+
}
|
|
228
|
+
function installPreCommitHook(dir = ".") {
|
|
229
|
+
const hookDir = `${dir}/.git/hooks`;
|
|
230
|
+
const hookPath = `${hookDir}/pre-commit`;
|
|
231
|
+
const hookScript = `#!/bin/sh
|
|
232
|
+
# dotenv-guard pre-commit hook \u2014 prevent .env leaks
|
|
233
|
+
# Installed by: npx dotenv-guard install-hook
|
|
234
|
+
|
|
235
|
+
ENV_FILES=$(git diff --cached --name-only | grep -E '^\\.env' | grep -v '\\.example$' | grep -v '\\.template$')
|
|
236
|
+
|
|
237
|
+
if [ -n "$ENV_FILES" ]; then
|
|
238
|
+
echo ""
|
|
239
|
+
echo "\\033[31m\u2716 dotenv-guard: Blocked commit \u2014 .env file(s) detected:\\033[0m"
|
|
240
|
+
echo ""
|
|
241
|
+
for f in $ENV_FILES; do
|
|
242
|
+
echo " \\033[33m\u2192 $f\\033[0m"
|
|
243
|
+
done
|
|
244
|
+
echo ""
|
|
245
|
+
echo " Remove with: git reset HEAD <file>"
|
|
246
|
+
echo " Or force commit: git commit --no-verify"
|
|
247
|
+
echo ""
|
|
248
|
+
exit 1
|
|
249
|
+
fi
|
|
250
|
+
`;
|
|
251
|
+
(0, import_fs2.mkdirSync)(hookDir, { recursive: true });
|
|
252
|
+
(0, import_fs2.writeFileSync)(hookPath, hookScript, "utf-8");
|
|
253
|
+
try {
|
|
254
|
+
(0, import_fs2.chmodSync)(hookPath, "755");
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/core/generator.ts
|
|
260
|
+
var import_fs3 = require("fs");
|
|
261
|
+
function generateExample(envPath = ".env", outputPath = ".env.example") {
|
|
262
|
+
if (!(0, import_fs3.existsSync)(envPath)) throw new Error(`${envPath} not found`);
|
|
263
|
+
const content = (0, import_fs3.readFileSync)(envPath, "utf-8");
|
|
264
|
+
const entries = parseEnvFile(content);
|
|
265
|
+
const lines = ["# Environment Variables", "# Copy this file to .env and fill in the values", ""];
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
let placeholder = "";
|
|
268
|
+
if (entry.value.match(/^https?:\/\//)) placeholder = "https://... # type:url";
|
|
269
|
+
else if (entry.value.match(/^\d+$/)) placeholder = "0 # type:number";
|
|
270
|
+
else if (entry.value.match(/^(true|false)$/i)) placeholder = "false # type:boolean";
|
|
271
|
+
else placeholder = "# type:string";
|
|
272
|
+
lines.push(`${entry.key}=${placeholder}`);
|
|
273
|
+
}
|
|
274
|
+
const output = lines.join("\n") + "\n";
|
|
275
|
+
(0, import_fs3.writeFileSync)(outputPath, output, "utf-8");
|
|
276
|
+
return output;
|
|
277
|
+
}
|
|
278
|
+
function ensureGitignore(dir = ".") {
|
|
279
|
+
const gitignorePath = `${dir}/.gitignore`;
|
|
280
|
+
const envEntries = [".env", ".env.local", ".env.*.local"];
|
|
281
|
+
let content = "";
|
|
282
|
+
if ((0, import_fs3.existsSync)(gitignorePath)) {
|
|
283
|
+
content = (0, import_fs3.readFileSync)(gitignorePath, "utf-8");
|
|
284
|
+
}
|
|
285
|
+
const lines = content.split("\n");
|
|
286
|
+
const toAdd = envEntries.filter((e) => !lines.some((l) => l.trim() === e));
|
|
287
|
+
if (toAdd.length > 0) {
|
|
288
|
+
const addition = "\n# Environment variables\n" + toAdd.join("\n") + "\n";
|
|
289
|
+
(0, import_fs3.appendFileSync)(gitignorePath, addition, "utf-8");
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/index.ts
|
|
296
|
+
function guard(options) {
|
|
297
|
+
const envPath = options?.envPath ?? ".env";
|
|
298
|
+
const examplePath = options?.examplePath ?? ".env.example";
|
|
299
|
+
const exitOnError = options?.exitOnError ?? true;
|
|
300
|
+
const errors = [];
|
|
301
|
+
try {
|
|
302
|
+
const sync = syncCheck(envPath, examplePath);
|
|
303
|
+
if (sync.missing.length > 0) {
|
|
304
|
+
errors.push(`Missing variables: ${sync.missing.join(", ")}`);
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
if (!err.message.includes("not found")) {
|
|
308
|
+
errors.push(err.message);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (errors.length > 0 && exitOnError) {
|
|
312
|
+
console.error("\n dotenv-guard errors:\n");
|
|
313
|
+
errors.forEach((e) => console.error(` \u2716 ${e}`));
|
|
314
|
+
console.error("");
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
return { ok: errors.length === 0, errors };
|
|
318
|
+
}
|
|
319
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
320
|
+
0 && (module.exports = {
|
|
321
|
+
checkLeaks,
|
|
322
|
+
ensureGitignore,
|
|
323
|
+
generateExample,
|
|
324
|
+
guard,
|
|
325
|
+
inferRules,
|
|
326
|
+
installPreCommitHook,
|
|
327
|
+
parseEnvFile,
|
|
328
|
+
parseEnvFileToMap,
|
|
329
|
+
syncCheck,
|
|
330
|
+
validate
|
|
331
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkLeaks,
|
|
3
|
+
ensureGitignore,
|
|
4
|
+
generateExample,
|
|
5
|
+
inferRules,
|
|
6
|
+
installPreCommitHook,
|
|
7
|
+
validate
|
|
8
|
+
} from "./chunk-QXZQR35R.mjs";
|
|
9
|
+
import {
|
|
10
|
+
parseEnvFile,
|
|
11
|
+
parseEnvFileToMap,
|
|
12
|
+
syncCheck
|
|
13
|
+
} from "./chunk-YYFNUR7Y.mjs";
|
|
14
|
+
|
|
15
|
+
// src/index.ts
|
|
16
|
+
function guard(options) {
|
|
17
|
+
const envPath = options?.envPath ?? ".env";
|
|
18
|
+
const examplePath = options?.examplePath ?? ".env.example";
|
|
19
|
+
const exitOnError = options?.exitOnError ?? true;
|
|
20
|
+
const errors = [];
|
|
21
|
+
try {
|
|
22
|
+
const sync = syncCheck(envPath, examplePath);
|
|
23
|
+
if (sync.missing.length > 0) {
|
|
24
|
+
errors.push(`Missing variables: ${sync.missing.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (!err.message.includes("not found")) {
|
|
28
|
+
errors.push(err.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (errors.length > 0 && exitOnError) {
|
|
32
|
+
console.error("\n dotenv-guard errors:\n");
|
|
33
|
+
errors.forEach((e) => console.error(` \u2716 ${e}`));
|
|
34
|
+
console.error("");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
return { ok: errors.length === 0, errors };
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
checkLeaks,
|
|
41
|
+
ensureGitignore,
|
|
42
|
+
generateExample,
|
|
43
|
+
guard,
|
|
44
|
+
inferRules,
|
|
45
|
+
installPreCommitHook,
|
|
46
|
+
parseEnvFile,
|
|
47
|
+
parseEnvFileToMap,
|
|
48
|
+
syncCheck,
|
|
49
|
+
validate
|
|
50
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scuton/dotenv-guard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Validate env vars, sync .env with .env.example, prevent secret leaks. Zero dependencies.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"dotenv-guard": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./auto": {
|
|
18
|
+
"import": "./dist/auto.mjs",
|
|
19
|
+
"require": "./dist/auto.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup src/index.ts src/cli.ts src/auto.ts --format cjs,esm --dts --clean",
|
|
24
|
+
"dev": "tsup src/index.ts src/cli.ts src/auto.ts --format cjs,esm --watch",
|
|
25
|
+
"test": "vitest",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"dotenv", "env", "environment", "variables", "validation",
|
|
30
|
+
"guard", "security", "secrets", "leak-prevention", "config",
|
|
31
|
+
"typescript", "cli", "zero-dependency"
|
|
32
|
+
],
|
|
33
|
+
"author": "Scuton Technology <info@scuton.com>",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/scuton-technology/dotenv-guard.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/scuton-technology/dotenv-guard",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=16"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.6.0",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"vitest": "^2.0.0",
|
|
47
|
+
"@types/node": "^20.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/auto.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Bu dosya import edildiğinde otomatik olarak .env kontrolü yapar
|
|
2
|
+
// Kullanım: import 'dotenv-guard/auto';
|
|
3
|
+
|
|
4
|
+
import { syncCheck } from './core/sync.js';
|
|
5
|
+
import { red, yellow, bold } from './utils/colors.js';
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const result = syncCheck();
|
|
9
|
+
|
|
10
|
+
if (result.missing.length > 0) {
|
|
11
|
+
console.error('');
|
|
12
|
+
console.error(red(bold(' ✖ dotenv-guard: Missing environment variables')));
|
|
13
|
+
console.error('');
|
|
14
|
+
for (const key of result.missing) {
|
|
15
|
+
console.error(yellow(` → ${key}`));
|
|
16
|
+
}
|
|
17
|
+
console.error('');
|
|
18
|
+
console.error(` These variables are in ${result.examplePath} but missing from ${result.envPath}`);
|
|
19
|
+
console.error(` Run: ${bold('dotenv-guard sync')} to see details`);
|
|
20
|
+
console.error('');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
} catch (err: any) {
|
|
24
|
+
// .env.example yoksa sessizce geç (her projede olmayabilir)
|
|
25
|
+
if (!err.message.includes('not found')) {
|
|
26
|
+
console.warn(yellow(` ⚠ dotenv-guard: ${err.message}`));
|
|
27
|
+
}
|
|
28
|
+
}
|