envprotect 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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +53 -0
- package/dist/createEnv.d.ts +26 -0
- package/dist/createEnv.js +53 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +27 -0
- package/dist/generate.d.ts +5 -0
- package/dist/generate.js +94 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/loader.d.ts +10 -0
- package/dist/loader.js +57 -0
- package/dist/masking.d.ts +23 -0
- package/dist/masking.js +57 -0
- package/dist/sensitive.d.ts +9 -0
- package/dist/sensitive.js +10 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 arsheriff2k3
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# envProtect
|
|
2
|
+
|
|
3
|
+
Type-safe environment variable validation with Zod.
|
|
4
|
+
|
|
5
|
+
Stop crashing at 2 AM. Load, validate, and protect your env vars with full TypeScript inference, secrets masking, and `.env.example` generation.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install envprotect zod
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createEnv, sensitive } from 'envprotect';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
|
|
19
|
+
const env = createEnv({
|
|
20
|
+
schema: {
|
|
21
|
+
DATABASE_URL: z.string().url(),
|
|
22
|
+
PORT: z.coerce.number().default(3000),
|
|
23
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
24
|
+
API_SECRET: sensitive(z.string().min(32)),
|
|
25
|
+
DEBUG: z.coerce.boolean().default(false),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
env.PORT // number (not string)
|
|
30
|
+
env.DEBUG // boolean (not "false")
|
|
31
|
+
env.API_SECRET // string (masked in JSON.stringify)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Load + Validate** - Loads `.env` files and validates with Zod in one call
|
|
37
|
+
- **TypeScript Inference** - Full type inference from your schema. IDE autocomplete works.
|
|
38
|
+
- **Secrets Masking** - `JSON.stringify(env)` and `console.log(env)` mask sensitive values as `[MASKED]`
|
|
39
|
+
- **Fail Fast** - Clear error messages at startup, not crashes at 2 AM
|
|
40
|
+
- **`.env.example` Generation** - `npx envprotect generate --schema ./src/env.ts`
|
|
41
|
+
- **Zero Runtime Deps** - Zod is a peer dependency, not bundled
|
|
42
|
+
- **Framework Agnostic** - Works with Node.js, Bun, Deno, and edge runtimes
|
|
43
|
+
|
|
44
|
+
## Error Messages
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
envprotect: 2 environment variables failed validation:
|
|
48
|
+
|
|
49
|
+
DATABASE_URL: Required
|
|
50
|
+
Expected: string
|
|
51
|
+
Received: undefined
|
|
52
|
+
|
|
53
|
+
PORT: Expected number, received nan
|
|
54
|
+
Expected: number
|
|
55
|
+
Received: "abc"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Secrets Masking
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const env = createEnv({
|
|
62
|
+
schema: {
|
|
63
|
+
PUBLIC_KEY: z.string(),
|
|
64
|
+
SECRET_KEY: sensitive(z.string()),
|
|
65
|
+
},
|
|
66
|
+
source: { PUBLIC_KEY: 'pk_123', SECRET_KEY: 'sk_secret_456' },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(JSON.stringify(env));
|
|
70
|
+
// {"PUBLIC_KEY":"pk_123","SECRET_KEY":"[MASKED]"}
|
|
71
|
+
|
|
72
|
+
// Direct access still works
|
|
73
|
+
env.SECRET_KEY // "sk_secret_456"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Masking Behavior
|
|
77
|
+
|
|
78
|
+
| Path | Masked? |
|
|
79
|
+
|------|---------|
|
|
80
|
+
| `JSON.stringify(env)` | Yes |
|
|
81
|
+
| `console.log(env)` | Yes |
|
|
82
|
+
| `util.inspect(env)` | Yes |
|
|
83
|
+
| `String(env)` | Yes |
|
|
84
|
+
| `env.SECRET` (direct access) | No (by design) |
|
|
85
|
+
| `const { SECRET } = env` | No (known limitation) |
|
|
86
|
+
| `{ ...env }` | No (known limitation) |
|
|
87
|
+
|
|
88
|
+
## Generate `.env.example`
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx envprotect generate --schema ./src/env.ts --output .env.example
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API
|
|
95
|
+
|
|
96
|
+
### `createEnv(options)`
|
|
97
|
+
|
|
98
|
+
| Option | Type | Default | Description |
|
|
99
|
+
|--------|------|---------|-------------|
|
|
100
|
+
| `schema` | `Record<string, ZodType>` | required | Zod schema for each env var |
|
|
101
|
+
| `files` | `string[]` | `['.env', '.env.local']` | `.env` files to load |
|
|
102
|
+
| `source` | `Record<string, string>` | `process.env` | Override env source |
|
|
103
|
+
| `sensitive` | `string[]` | `[]` | Keys to mask in serialization |
|
|
104
|
+
| `cwd` | `string` | `process.cwd()` | Working directory for `.env` files |
|
|
105
|
+
|
|
106
|
+
### `sensitive(schema)`
|
|
107
|
+
|
|
108
|
+
Marks a Zod schema as sensitive. Values are masked in `JSON.stringify` and `console.log`.
|
|
109
|
+
|
|
110
|
+
### `generateEnvExample(schema, sensitiveKeys?)`
|
|
111
|
+
|
|
112
|
+
Generates `.env.example` content from a schema.
|
|
113
|
+
|
|
114
|
+
## Benchmarks
|
|
115
|
+
|
|
116
|
+
| Metric | Value |
|
|
117
|
+
|--------|-------|
|
|
118
|
+
| Validation speed | 3.0 us/call |
|
|
119
|
+
| Bundle size | 12 KB (unpacked) |
|
|
120
|
+
| Source | 470 lines, 7 modules |
|
|
121
|
+
| Runtime deps | 0 (Zod is peer) |
|
|
122
|
+
| Tests | 33 passing |
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
const command = args[0];
|
|
6
|
+
if (command === "generate") {
|
|
7
|
+
const outputIdx = args.indexOf("--output");
|
|
8
|
+
const output = outputIdx !== -1 ? args[outputIdx + 1] : ".env.example";
|
|
9
|
+
const schemaIdx = args.indexOf("--schema");
|
|
10
|
+
const schemaPath = schemaIdx !== -1 ? args[schemaIdx + 1] : undefined;
|
|
11
|
+
if (!schemaPath) {
|
|
12
|
+
console.log("Usage: envprotect generate --schema ./src/env.ts --output .env.example");
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log("Your schema file must export a `schema` object and optionally a `sensitive` array.");
|
|
15
|
+
console.log("");
|
|
16
|
+
console.log("Example schema file (src/env.ts):");
|
|
17
|
+
console.log(' import { z } from "zod";');
|
|
18
|
+
console.log(" export const schema = {");
|
|
19
|
+
console.log(" DATABASE_URL: z.string().url().describe('PostgreSQL connection string'),");
|
|
20
|
+
console.log(" PORT: z.coerce.number().default(3000),");
|
|
21
|
+
console.log(" };");
|
|
22
|
+
console.log(' export const sensitive = ["DATABASE_URL"];');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const fullSchemaPath = resolve(process.cwd(), schemaPath);
|
|
26
|
+
import(fullSchemaPath)
|
|
27
|
+
.then(async (mod) => {
|
|
28
|
+
const { generateEnvExample } = await import("./generate.js");
|
|
29
|
+
const schema = mod.schema || mod.default?.schema;
|
|
30
|
+
const sensitive = mod.sensitive || mod.default?.sensitive || [];
|
|
31
|
+
if (!schema) {
|
|
32
|
+
console.error(`Error: No "schema" export found in ${schemaPath}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const content = generateEnvExample(schema, sensitive);
|
|
36
|
+
const outputPath = resolve(process.cwd(), output);
|
|
37
|
+
writeFileSync(outputPath, content, "utf-8");
|
|
38
|
+
console.log(`Generated ${output} with ${Object.keys(schema).length} variables`);
|
|
39
|
+
})
|
|
40
|
+
.catch((err) => {
|
|
41
|
+
console.error(`Error loading schema from ${schemaPath}:`, err.message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log("envprotect CLI");
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log("Commands:");
|
|
49
|
+
console.log(" generate Generate a .env.example file from your schema");
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log("Usage:");
|
|
52
|
+
console.log(" envprotect generate --schema ./src/env.ts --output .env.example");
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z, type ZodType } from "zod";
|
|
2
|
+
export interface SensitiveSchema {
|
|
3
|
+
_sensitive?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export type EnvSchema = Record<string, ZodType<any> & Partial<SensitiveSchema>>;
|
|
6
|
+
export interface CreateEnvOptions<T extends EnvSchema> {
|
|
7
|
+
/** Zod schema for each environment variable */
|
|
8
|
+
schema: T;
|
|
9
|
+
/** .env files to load (in order, later overrides earlier). Default: ['.env', '.env.local'] */
|
|
10
|
+
files?: string[];
|
|
11
|
+
/** Working directory for resolving .env files. Default: process.cwd() */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Override source of environment variables (default: process.env) */
|
|
14
|
+
source?: Record<string, string | undefined>;
|
|
15
|
+
/** Keys to mask in JSON/log output */
|
|
16
|
+
sensitive?: (keyof T)[];
|
|
17
|
+
}
|
|
18
|
+
type InferSchema<T extends EnvSchema> = {
|
|
19
|
+
[K in keyof T]: z.infer<T[K]>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Load, validate, and return a fully typed environment object.
|
|
23
|
+
* Fails fast at startup with clear error messages if any variables are invalid or missing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createEnv<T extends EnvSchema>(options: CreateEnvOptions<T>): Readonly<InferSchema<T>>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { loadEnvFiles } from "./loader.js";
|
|
2
|
+
import { EnvValidationError, zodIssueToEnvError } from "./errors.js";
|
|
3
|
+
import { createMaskedEnv } from "./masking.js";
|
|
4
|
+
/**
|
|
5
|
+
* Load, validate, and return a fully typed environment object.
|
|
6
|
+
* Fails fast at startup with clear error messages if any variables are invalid or missing.
|
|
7
|
+
*/
|
|
8
|
+
export function createEnv(options) {
|
|
9
|
+
const { schema, files = [".env", ".env.local"], cwd, source, sensitive = [], } = options;
|
|
10
|
+
// 1. Load .env files
|
|
11
|
+
const fileVars = loadEnvFiles(files, cwd);
|
|
12
|
+
// 2. Merge: .env files < process.env (process.env wins)
|
|
13
|
+
const envSource = source ?? process.env;
|
|
14
|
+
const merged = { ...fileVars };
|
|
15
|
+
for (const key of Object.keys(schema)) {
|
|
16
|
+
if (envSource[key] !== undefined) {
|
|
17
|
+
merged[key] = envSource[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// 3. Validate each variable
|
|
21
|
+
const errors = [];
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const [key, zodSchema] of Object.entries(schema)) {
|
|
24
|
+
const raw = merged[key];
|
|
25
|
+
const parseResult = zodSchema.safeParse(raw);
|
|
26
|
+
if (parseResult.success) {
|
|
27
|
+
result[key] = parseResult.data;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
for (const issue of parseResult.error.issues) {
|
|
31
|
+
errors.push(zodIssueToEnvError(key, issue));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 4. Throw aggregated errors
|
|
36
|
+
if (errors.length > 0) {
|
|
37
|
+
throw new EnvValidationError(errors);
|
|
38
|
+
}
|
|
39
|
+
// 5. Build sensitive keys set
|
|
40
|
+
const sensitiveKeys = new Set();
|
|
41
|
+
for (const key of sensitive) {
|
|
42
|
+
sensitiveKeys.add(key);
|
|
43
|
+
}
|
|
44
|
+
// Also check for _sensitive flag on schema
|
|
45
|
+
for (const [key, zodSchema] of Object.entries(schema)) {
|
|
46
|
+
if (zodSchema._sensitive) {
|
|
47
|
+
sensitiveKeys.add(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 6. Apply masking proxy and freeze
|
|
51
|
+
const env = createMaskedEnv(result, sensitiveKeys);
|
|
52
|
+
return Object.freeze(env);
|
|
53
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ZodIssue } from "zod";
|
|
2
|
+
export interface EnvError {
|
|
3
|
+
key: string;
|
|
4
|
+
message: string;
|
|
5
|
+
expected?: string;
|
|
6
|
+
received?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class EnvValidationError extends Error {
|
|
9
|
+
readonly errors: EnvError[];
|
|
10
|
+
constructor(errors: EnvError[]);
|
|
11
|
+
}
|
|
12
|
+
export declare function zodIssueToEnvError(key: string, issue: ZodIssue): EnvError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class EnvValidationError extends Error {
|
|
2
|
+
errors;
|
|
3
|
+
constructor(errors) {
|
|
4
|
+
const header = `envprotect: ${errors.length} environment variable${errors.length > 1 ? "s" : ""} failed validation:\n`;
|
|
5
|
+
const details = errors
|
|
6
|
+
.map((e) => {
|
|
7
|
+
let msg = ` ${e.key}: ${e.message}`;
|
|
8
|
+
if (e.expected)
|
|
9
|
+
msg += `\n Expected: ${e.expected}`;
|
|
10
|
+
if (e.received)
|
|
11
|
+
msg += `\n Received: ${e.received}`;
|
|
12
|
+
return msg;
|
|
13
|
+
})
|
|
14
|
+
.join("\n\n");
|
|
15
|
+
super(header + details);
|
|
16
|
+
this.name = "EnvValidationError";
|
|
17
|
+
this.errors = errors;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function zodIssueToEnvError(key, issue) {
|
|
21
|
+
return {
|
|
22
|
+
key,
|
|
23
|
+
message: issue.message,
|
|
24
|
+
expected: "expected" in issue ? String(issue.expected) : undefined,
|
|
25
|
+
received: "received" in issue ? String(issue.received) : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
function getZodTypeInfo(schema) {
|
|
2
|
+
const def = schema._def;
|
|
3
|
+
if (def.typeName === "ZodDefault") {
|
|
4
|
+
const inner = getZodTypeInfo(def.innerType);
|
|
5
|
+
return { ...inner };
|
|
6
|
+
}
|
|
7
|
+
if (def.typeName === "ZodOptional") {
|
|
8
|
+
const inner = getZodTypeInfo(def.innerType);
|
|
9
|
+
return { ...inner };
|
|
10
|
+
}
|
|
11
|
+
if (def.typeName === "ZodEffects") {
|
|
12
|
+
// z.coerce.*
|
|
13
|
+
const inner = getZodTypeInfo(def.schema);
|
|
14
|
+
return { ...inner };
|
|
15
|
+
}
|
|
16
|
+
if (def.typeName === "ZodString")
|
|
17
|
+
return { type: "string", innerDef: def };
|
|
18
|
+
if (def.typeName === "ZodNumber")
|
|
19
|
+
return { type: "number", innerDef: def };
|
|
20
|
+
if (def.typeName === "ZodBoolean")
|
|
21
|
+
return { type: "boolean", innerDef: def };
|
|
22
|
+
if (def.typeName === "ZodEnum")
|
|
23
|
+
return { type: def.values.join(" | "), innerDef: def };
|
|
24
|
+
return { type: "string", innerDef: def };
|
|
25
|
+
}
|
|
26
|
+
function isOptional(schema) {
|
|
27
|
+
const def = schema._def;
|
|
28
|
+
if (def.typeName === "ZodOptional")
|
|
29
|
+
return true;
|
|
30
|
+
if (def.typeName === "ZodDefault")
|
|
31
|
+
return true;
|
|
32
|
+
if (def.typeName === "ZodEffects")
|
|
33
|
+
return isOptional(def.schema);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function getDefault(schema) {
|
|
37
|
+
const def = schema._def;
|
|
38
|
+
if (def.typeName === "ZodDefault") {
|
|
39
|
+
const val = def.defaultValue();
|
|
40
|
+
return val !== undefined ? String(val) : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (def.typeName === "ZodOptional")
|
|
43
|
+
return getDefault(def.innerType);
|
|
44
|
+
if (def.typeName === "ZodEffects")
|
|
45
|
+
return getDefault(def.schema);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function getDescription(schema) {
|
|
49
|
+
if (schema.description)
|
|
50
|
+
return schema.description;
|
|
51
|
+
const def = schema._def;
|
|
52
|
+
if (def.typeName === "ZodDefault")
|
|
53
|
+
return getDescription(def.innerType);
|
|
54
|
+
if (def.typeName === "ZodOptional")
|
|
55
|
+
return getDescription(def.innerType);
|
|
56
|
+
if (def.typeName === "ZodEffects")
|
|
57
|
+
return getDescription(def.schema);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Generate .env.example content from a schema.
|
|
62
|
+
*/
|
|
63
|
+
export function generateEnvExample(schema, sensitiveKeys = []) {
|
|
64
|
+
const sensitiveSet = new Set(sensitiveKeys);
|
|
65
|
+
const lines = [
|
|
66
|
+
"# Environment Variables",
|
|
67
|
+
"# Generated by envprotect",
|
|
68
|
+
"#",
|
|
69
|
+
"",
|
|
70
|
+
];
|
|
71
|
+
for (const [key, zodSchema] of Object.entries(schema)) {
|
|
72
|
+
const optional = isOptional(zodSchema);
|
|
73
|
+
const defaultVal = getDefault(zodSchema);
|
|
74
|
+
const description = getDescription(zodSchema);
|
|
75
|
+
const { type } = getZodTypeInfo(zodSchema);
|
|
76
|
+
const isSensitive = sensitiveSet.has(key) || zodSchema._sensitive;
|
|
77
|
+
const parts = [];
|
|
78
|
+
parts.push(optional ? "(optional" : "(required");
|
|
79
|
+
if (defaultVal !== undefined)
|
|
80
|
+
parts[parts.length - 1] += `, default: ${defaultVal}`;
|
|
81
|
+
parts[parts.length - 1] += ")";
|
|
82
|
+
if (isSensitive)
|
|
83
|
+
parts.push("sensitive");
|
|
84
|
+
parts.push(type);
|
|
85
|
+
if (description)
|
|
86
|
+
parts.push(description);
|
|
87
|
+
const comment = parts.join(" ");
|
|
88
|
+
const value = defaultVal ?? "";
|
|
89
|
+
lines.push(`# ${comment}`);
|
|
90
|
+
lines.push(`${key}=${value}`);
|
|
91
|
+
lines.push("");
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createEnv } from "./createEnv.js";
|
|
2
|
+
export type { CreateEnvOptions, EnvSchema } from "./createEnv.js";
|
|
3
|
+
export { sensitive } from "./sensitive.js";
|
|
4
|
+
export { generateEnvExample } from "./generate.js";
|
|
5
|
+
export { EnvValidationError } from "./errors.js";
|
|
6
|
+
export type { EnvError } from "./errors.js";
|
|
7
|
+
export { loadEnvFiles, parseEnvFile } from "./loader.js";
|
package/dist/index.js
ADDED
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a .env file content into key-value pairs.
|
|
3
|
+
* Handles quoted values, comments, empty lines, and multiline values.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseEnvFile(content: string): Record<string, string>;
|
|
6
|
+
/**
|
|
7
|
+
* Load environment variables from .env files.
|
|
8
|
+
* Later files override earlier ones. process.env always takes precedence.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadEnvFiles(files: string[], cwd?: string): Record<string, string>;
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Parse a .env file content into key-value pairs.
|
|
5
|
+
* Handles quoted values, comments, empty lines, and multiline values.
|
|
6
|
+
*/
|
|
7
|
+
export function parseEnvFile(content) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const line of content.split("\n")) {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
// Skip empty lines and comments
|
|
12
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
13
|
+
continue;
|
|
14
|
+
const eqIndex = trimmed.indexOf("=");
|
|
15
|
+
if (eqIndex === -1)
|
|
16
|
+
continue;
|
|
17
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
18
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
19
|
+
// Remove surrounding quotes
|
|
20
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
21
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
22
|
+
value = value.slice(1, -1);
|
|
23
|
+
}
|
|
24
|
+
// Remove inline comments (only for unquoted values)
|
|
25
|
+
if (!trimmed.slice(eqIndex + 1).trim().startsWith('"')) {
|
|
26
|
+
const commentIndex = value.indexOf(" #");
|
|
27
|
+
if (commentIndex !== -1) {
|
|
28
|
+
value = value.slice(0, commentIndex).trim();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (key) {
|
|
32
|
+
result[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load environment variables from .env files.
|
|
39
|
+
* Later files override earlier ones. process.env always takes precedence.
|
|
40
|
+
*/
|
|
41
|
+
export function loadEnvFiles(files, cwd = process.cwd()) {
|
|
42
|
+
const loaded = {};
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const filePath = resolve(cwd, file);
|
|
45
|
+
if (!existsSync(filePath))
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(filePath, "utf-8");
|
|
49
|
+
const parsed = parseEnvFile(content);
|
|
50
|
+
Object.assign(loaded, parsed);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Skip files that can't be read
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return loaded;
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a masked env wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Serialization paths return masked values:
|
|
5
|
+
* - JSON.stringify(env) → toJSON()
|
|
6
|
+
* - console.log(env) → nodejs.util.inspect.custom
|
|
7
|
+
* - util.inspect(env) → nodejs.util.inspect.custom
|
|
8
|
+
* - String(env) / `${env}` → toString() / Symbol.toPrimitive
|
|
9
|
+
*
|
|
10
|
+
* Direct property access (env.SECRET) always returns the real value.
|
|
11
|
+
* This is intentional — masking prevents accidental bulk serialization,
|
|
12
|
+
* not deliberate single-field access.
|
|
13
|
+
*
|
|
14
|
+
* KNOWN LIMITATIONS (documented, by design):
|
|
15
|
+
* const { SECRET } = env; → real value (destructuring = direct access)
|
|
16
|
+
* { ...env } → real values (spread = per-key access)
|
|
17
|
+
* Object.assign({}, env) → real values (same as spread)
|
|
18
|
+
* Object.values(env) → real values (per-key access)
|
|
19
|
+
*
|
|
20
|
+
* These are deliberate access patterns. Masking targets accidental
|
|
21
|
+
* serialization: console.log(env), JSON.stringify(env), error reporters.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createMaskedEnv<T extends Record<string, unknown>>(values: T, sensitiveKeys: Set<string>): T;
|
package/dist/masking.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const MASKED = "[MASKED]";
|
|
2
|
+
/**
|
|
3
|
+
* Build a plain object with sensitive keys masked.
|
|
4
|
+
*/
|
|
5
|
+
function buildMaskedSnapshot(target, sensitiveKeys) {
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const key of Object.keys(target)) {
|
|
8
|
+
result[key] = sensitiveKeys.has(key) ? MASKED : target[key];
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a masked env wrapper.
|
|
14
|
+
*
|
|
15
|
+
* Serialization paths return masked values:
|
|
16
|
+
* - JSON.stringify(env) → toJSON()
|
|
17
|
+
* - console.log(env) → nodejs.util.inspect.custom
|
|
18
|
+
* - util.inspect(env) → nodejs.util.inspect.custom
|
|
19
|
+
* - String(env) / `${env}` → toString() / Symbol.toPrimitive
|
|
20
|
+
*
|
|
21
|
+
* Direct property access (env.SECRET) always returns the real value.
|
|
22
|
+
* This is intentional — masking prevents accidental bulk serialization,
|
|
23
|
+
* not deliberate single-field access.
|
|
24
|
+
*
|
|
25
|
+
* KNOWN LIMITATIONS (documented, by design):
|
|
26
|
+
* const { SECRET } = env; → real value (destructuring = direct access)
|
|
27
|
+
* { ...env } → real values (spread = per-key access)
|
|
28
|
+
* Object.assign({}, env) → real values (same as spread)
|
|
29
|
+
* Object.values(env) → real values (per-key access)
|
|
30
|
+
*
|
|
31
|
+
* These are deliberate access patterns. Masking targets accidental
|
|
32
|
+
* serialization: console.log(env), JSON.stringify(env), error reporters.
|
|
33
|
+
*/
|
|
34
|
+
export function createMaskedEnv(values, sensitiveKeys) {
|
|
35
|
+
if (sensitiveKeys.size === 0)
|
|
36
|
+
return values;
|
|
37
|
+
return new Proxy(values, {
|
|
38
|
+
get(target, prop, receiver) {
|
|
39
|
+
// JSON.stringify calls toJSON()
|
|
40
|
+
if (prop === "toJSON") {
|
|
41
|
+
return () => buildMaskedSnapshot(target, sensitiveKeys);
|
|
42
|
+
}
|
|
43
|
+
// console.log / util.inspect in Node.js
|
|
44
|
+
if (prop === Symbol.for("nodejs.util.inspect.custom")) {
|
|
45
|
+
return (_depth, _opts) => buildMaskedSnapshot(target, sensitiveKeys);
|
|
46
|
+
}
|
|
47
|
+
// String(env) or `${env}`
|
|
48
|
+
if (prop === Symbol.toPrimitive) {
|
|
49
|
+
return (_hint) => JSON.stringify(buildMaskedSnapshot(target, sensitiveKeys));
|
|
50
|
+
}
|
|
51
|
+
if (prop === "toString") {
|
|
52
|
+
return () => JSON.stringify(buildMaskedSnapshot(target, sensitiveKeys));
|
|
53
|
+
}
|
|
54
|
+
return Reflect.get(target, prop, receiver);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ZodType } from "zod";
|
|
2
|
+
import type { SensitiveSchema } from "./createEnv.js";
|
|
3
|
+
/**
|
|
4
|
+
* Mark a Zod schema as sensitive. Values will be masked in logs and JSON output.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sensitive(z.string().min(32))
|
|
8
|
+
*/
|
|
9
|
+
export declare function sensitive<T extends ZodType<any>>(schema: T): T & SensitiveSchema;
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "envprotect",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Type-safe environment variable validation with Zod. Load, validate, and protect your env vars with full TypeScript inference, secrets masking, and .env.example generation.",
|
|
5
|
+
"author": "arsheriff2k3",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/arsheriff2k3/envprotect"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/arsheriff2k3/envprotect#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/arsheriff2k3/envprotect/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"envprotect": "./dist/cli.js"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"env",
|
|
39
|
+
"environment",
|
|
40
|
+
"validation",
|
|
41
|
+
"zod",
|
|
42
|
+
"typescript",
|
|
43
|
+
"dotenv",
|
|
44
|
+
"config",
|
|
45
|
+
"type-safe",
|
|
46
|
+
"secrets",
|
|
47
|
+
"masking",
|
|
48
|
+
"envprotect"
|
|
49
|
+
],
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"zod": "^3.22.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "latest",
|
|
55
|
+
"typescript": "^5.5.0",
|
|
56
|
+
"zod": "^3.23.0"
|
|
57
|
+
}
|
|
58
|
+
}
|