@maestro-js/config 1.0.0-alpha.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 +198 -0
- package/dist/chunk-AVYD4MV2.js +187 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +6 -0
- package/dist/load.d.ts +2 -0
- package/dist/load.js +23 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# @maestro-js/config
|
|
2
|
+
|
|
3
|
+
Standalone configuration loader that reads `.env` files (plaintext or AES-256-GCM encrypted), imports a TypeScript or JavaScript config module, and writes the resolved key-value pairs into `process.env`. Zero `@maestro-js` dependencies.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Config } from '@maestro-js/config'
|
|
9
|
+
|
|
10
|
+
// Load config into process.env (existing keys are NOT overwritten)
|
|
11
|
+
await Config.loadConfigFile({
|
|
12
|
+
rootDir: process.cwd(),
|
|
13
|
+
configName: 'app',
|
|
14
|
+
envName: 'production',
|
|
15
|
+
encryptionKey: process.env.MAESTRO_ENV_ENCRYPTION_KEY ?? null
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or auto-load at startup without application code:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node --import @maestro-js/config/load --config app --env production --root /path/to/project
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API Reference
|
|
26
|
+
|
|
27
|
+
### ConfigOptions
|
|
28
|
+
|
|
29
|
+
Every primary function accepts this options object:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
type ConfigOptions = {
|
|
33
|
+
rootDir: string // Project root directory
|
|
34
|
+
configName: string // Config file name without extension (looked up in <rootDir>/config/)
|
|
35
|
+
envName: string | null // Environment name (.env.<envName>) or null for .env
|
|
36
|
+
encryptionKey: string | null // 64-char hex key for encrypted .env files, or null
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Config.loadConfigFile(options: ConfigOptions): Promise<void>
|
|
41
|
+
|
|
42
|
+
Read the `.env` file, import the config file, and write the resolved values into `process.env`. Keys already present in `process.env` are not overwritten.
|
|
43
|
+
|
|
44
|
+
### Config.getConfig(options: ConfigOptions): Promise<NodeJS.Dict<string>>
|
|
45
|
+
|
|
46
|
+
Same as `loadConfigFile` but returns the resolved config object without modifying `process.env`.
|
|
47
|
+
|
|
48
|
+
### Config.getEnvVars(options): NodeJS.Dict<string>
|
|
49
|
+
|
|
50
|
+
Read and parse an env file (plaintext or encrypted). Returns `{}` if no env file is found. Throws if an encrypted file is found but no `encryptionKey` is provided.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
Config.getEnvVars({ rootDir: '.', envName: null, encryptionKey: null })
|
|
54
|
+
// => { FOO: 'bar', BAZ: 'qux' }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Config.resolveConfig(module: any): Promise<NodeJS.Dict<string>>
|
|
58
|
+
|
|
59
|
+
Normalize an imported config module into a flat string dictionary. Handles four export shapes: named exports, default object, default sync function, and default async function. Numbers and booleans are stringified. Objects and arrays throw.
|
|
60
|
+
|
|
61
|
+
### Config.generateEncryptedEnvFile(options): void
|
|
62
|
+
|
|
63
|
+
Read a plaintext `.env` file and write its AES-256-GCM encrypted counterpart (`.env.encrypted` or `.env.<envName>.encrypted`).
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
Config.generateEncryptedEnvFile({
|
|
67
|
+
rootDir: process.cwd(),
|
|
68
|
+
envName: null,
|
|
69
|
+
encryptionKey: key
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Config.generateDecryptedEnvFile(options): void
|
|
74
|
+
|
|
75
|
+
Read an encrypted `.env.encrypted` file and write the decrypted plaintext. Throws if the target file already exists unless `force: true`.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
Config.generateDecryptedEnvFile({
|
|
79
|
+
rootDir: process.cwd(),
|
|
80
|
+
envName: 'production',
|
|
81
|
+
encryptionKey: key,
|
|
82
|
+
force: true
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Config.generateEncryptionKey(): string
|
|
87
|
+
|
|
88
|
+
Generate a cryptographically random 32-byte key as a 64-character hex string.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const key = Config.generateEncryptionKey()
|
|
92
|
+
// => '7a3f8b2c...' (64 hex chars)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Config.Resolve<T> (type utility)
|
|
96
|
+
|
|
97
|
+
Extract the resolved key-value shape from a config module type. Use in an `environment.d.ts` file to make `process.env` type-safe.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import type { Config } from '@maestro-js/config'
|
|
101
|
+
import type appConfig from './config/app.ts'
|
|
102
|
+
|
|
103
|
+
declare global {
|
|
104
|
+
namespace NodeJS {
|
|
105
|
+
interface ProcessEnv extends Config.Resolve<typeof appConfig> {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Common Patterns
|
|
111
|
+
|
|
112
|
+
### Config file with named exports
|
|
113
|
+
|
|
114
|
+
Place config files in `<rootDir>/config/`. Supported extensions: `.mts`, `.cts`, `.ts`, `.mjs`, `.cjs`, `.js`.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// config/app.ts
|
|
118
|
+
export const DB_HOST = 'localhost'
|
|
119
|
+
export const DB_PORT = '3306'
|
|
120
|
+
export const DEBUG = true // stringified to 'true'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Config file as a function (derive values from .env)
|
|
124
|
+
|
|
125
|
+
When the config exports a function, it receives the merged env vars (process.env values take precedence over .env file values).
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// config/app.ts
|
|
129
|
+
export default (env: Record<string, string | undefined>) => ({
|
|
130
|
+
DATABASE_URL: `mysql://${env.DB_USER}:${env.DB_PASS}@${env.DB_HOST}:${env.DB_PORT}/${env.DB_NAME}`,
|
|
131
|
+
API_KEY: env.API_KEY
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Async config function
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// config/app.ts
|
|
139
|
+
export default async (env: Record<string, string | undefined>) => {
|
|
140
|
+
return {
|
|
141
|
+
SECRET: env.SECRET,
|
|
142
|
+
COMPUTED: await someAsyncOperation()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Env file resolution order
|
|
148
|
+
|
|
149
|
+
When `envName` is provided (e.g., `'production'`), files are checked in this order:
|
|
150
|
+
1. `.env.production`
|
|
151
|
+
2. `.env.production.encrypted`
|
|
152
|
+
3. `.env`
|
|
153
|
+
4. `.env.encrypted`
|
|
154
|
+
|
|
155
|
+
When `envName` is null:
|
|
156
|
+
1. `.env`
|
|
157
|
+
2. `.env.encrypted`
|
|
158
|
+
|
|
159
|
+
### Encryption round-trip
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const key = Config.generateEncryptionKey()
|
|
163
|
+
|
|
164
|
+
// Encrypt: reads .env, writes .env.encrypted
|
|
165
|
+
Config.generateEncryptedEnvFile({ rootDir: '.', envName: null, encryptionKey: key })
|
|
166
|
+
|
|
167
|
+
// Decrypt: reads .env.encrypted, writes .env
|
|
168
|
+
Config.generateDecryptedEnvFile({ rootDir: '.', envName: null, encryptionKey: key, force: true })
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Encrypted values use the format `<iv_base64>.<authTag_base64>.<ciphertext_base64>` per key.
|
|
172
|
+
|
|
173
|
+
### Auto-load entry point
|
|
174
|
+
|
|
175
|
+
The `@maestro-js/config/load` export is a standalone script intended for `node --import`. It parses CLI arguments and calls `Config.loadConfigFile`.
|
|
176
|
+
|
|
177
|
+
CLI flags:
|
|
178
|
+
- `--config <name>` (required) -- config file name without extension
|
|
179
|
+
- `--env <name>` -- environment name
|
|
180
|
+
- `--key <hex>` -- encryption key (falls back to `MAESTRO_ENV_ENCRYPTION_KEY` env var)
|
|
181
|
+
- `--root <path>` -- project root (defaults to `process.cwd()`)
|
|
182
|
+
|
|
183
|
+
## Cross-Package Integration
|
|
184
|
+
|
|
185
|
+
Config is a standalone package with zero `@maestro-js` dependencies. It is used by:
|
|
186
|
+
|
|
187
|
+
- **CLI** -- The Maestro CLI uses `Config.generateEncryptedEnvFile`, `Config.generateDecryptedEnvFile`, and `Config.generateEncryptionKey` for the `env:encrypt`, `env:decrypt`, and `env:gen-key` commands.
|
|
188
|
+
- **Application boot** -- Applications use `Config.loadConfigFile` (or `node --import @maestro-js/config/load`) to populate `process.env` before initializing other Maestro packages that read from `process.env`.
|
|
189
|
+
- **Type safety** -- The `Config.Resolve` utility type lets applications create a typed `process.env` derived from the config module shape.
|
|
190
|
+
|
|
191
|
+
## Testing
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pnpm --filter @maestro-js/config test
|
|
195
|
+
cd packages/config && npx beartest ./tests/config.test.ts
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Tests use `beartest-js` with `expect`. They create temporary directories, write config/env files, and verify loading, encryption round-trips, and error cases.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import "async_hooks";
|
|
3
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { parseEnv } from "util";
|
|
7
|
+
async function getConfig({ rootDir, configName, envName, encryptionKey }) {
|
|
8
|
+
const env = getEnvVars({ rootDir, envName, encryptionKey });
|
|
9
|
+
const configFilePath = getConfigFilePath({ rootDir, configName });
|
|
10
|
+
if (!configFilePath) {
|
|
11
|
+
return env;
|
|
12
|
+
}
|
|
13
|
+
const config = await resolveConfig(import(configFilePath), env);
|
|
14
|
+
return config;
|
|
15
|
+
}
|
|
16
|
+
async function loadConfigFile(options) {
|
|
17
|
+
const config = await getConfig(options);
|
|
18
|
+
for (const [key, value] of Object.entries(config)) {
|
|
19
|
+
if (process.env[key] === void 0) {
|
|
20
|
+
process.env[key] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function resolveEnvValues(dotEnvFileValues) {
|
|
25
|
+
const processKeys = Object.keys(process.env);
|
|
26
|
+
const envFileKeys = Object.keys(dotEnvFileValues);
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
[.../* @__PURE__ */ new Set([...processKeys, ...envFileKeys])].map((key) => [key, process.env[key] ?? dotEnvFileValues[key]])
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
async function resolveConfig(module, env) {
|
|
32
|
+
let config = await module;
|
|
33
|
+
if (config && typeof config === "object" && "default" in config) {
|
|
34
|
+
config = config.default;
|
|
35
|
+
}
|
|
36
|
+
const raw = typeof config === "function" ? await config(resolveEnvValues(env)) : config;
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
result[key] = value;
|
|
41
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
42
|
+
result[key] = String(value);
|
|
43
|
+
} else if (value != null) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Config key "${key}" has unsupported type ${typeof value}. Only string, number, and boolean values are allowed.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
var CONFIG_EXTENSIONS = [".mts", ".cts", ".ts", ".mjs", ".cjs", ".js"];
|
|
52
|
+
function getConfigFilePath({ rootDir, configName }) {
|
|
53
|
+
for (const ext of CONFIG_EXTENSIONS) {
|
|
54
|
+
const filePath = join(rootDir, "config", `${configName}${ext}`);
|
|
55
|
+
if (existsSync(filePath)) {
|
|
56
|
+
return filePath;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function getEnvVars({
|
|
62
|
+
rootDir,
|
|
63
|
+
envName,
|
|
64
|
+
encryptionKey
|
|
65
|
+
}) {
|
|
66
|
+
const envFile = getEnvFilePath({ rootDir, envName });
|
|
67
|
+
if (!envFile) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
const envFileContents = readFileSync(envFile, "utf-8");
|
|
71
|
+
const isEncrypted = envFile.endsWith(".encrypted");
|
|
72
|
+
if (isEncrypted) {
|
|
73
|
+
if (!encryptionKey) {
|
|
74
|
+
throw new Error(`Cannot decrypt ${envFile}. Provide an encryption key via --key or MAESTRO_ENV_ENCRYPTION_KEY`);
|
|
75
|
+
} else {
|
|
76
|
+
const decrypted = decryptEnvFileContents(envFileContents, encryptionKey);
|
|
77
|
+
return decrypted;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
const parsed = parseEnv(envFileContents);
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function generateEncryptedEnvFile({
|
|
85
|
+
rootDir,
|
|
86
|
+
envName: envNameParam,
|
|
87
|
+
encryptionKey
|
|
88
|
+
}) {
|
|
89
|
+
const envName = envNameParam || process.env.ENV_NAME || null;
|
|
90
|
+
const sourcePath = join(rootDir, envFileName(envName));
|
|
91
|
+
const destPath = join(rootDir, encryptedEnvFileName(envName));
|
|
92
|
+
const contents = readFileSync(sourcePath, "utf-8");
|
|
93
|
+
parseEnv(contents);
|
|
94
|
+
const encrypted = encryptEnvFileContents(contents, encryptionKey);
|
|
95
|
+
writeFileSync(destPath, envRecordToFileContents(encrypted));
|
|
96
|
+
}
|
|
97
|
+
function generateDecryptedEnvFile({
|
|
98
|
+
rootDir,
|
|
99
|
+
envName: envNameParam,
|
|
100
|
+
encryptionKey,
|
|
101
|
+
force
|
|
102
|
+
}) {
|
|
103
|
+
const envName = envNameParam || process.env.ENV_NAME || null;
|
|
104
|
+
const sourcePath = join(rootDir, encryptedEnvFileName(envName));
|
|
105
|
+
const destPath = join(rootDir, envFileName(envName));
|
|
106
|
+
if (!force && existsSync(destPath)) {
|
|
107
|
+
throw new Error(`${destPath} already exists. Use force to overwrite.`);
|
|
108
|
+
}
|
|
109
|
+
const contents = readFileSync(sourcePath, "utf-8");
|
|
110
|
+
const decrypted = decryptEnvFileContents(contents, encryptionKey);
|
|
111
|
+
writeFileSync(destPath, envRecordToFileContents(decrypted));
|
|
112
|
+
}
|
|
113
|
+
function generateEncryptionKey() {
|
|
114
|
+
return randomBytes(32).toString("hex");
|
|
115
|
+
}
|
|
116
|
+
function envFileName(envName) {
|
|
117
|
+
return envName ? `.env.${envName}` : ".env";
|
|
118
|
+
}
|
|
119
|
+
function encryptedEnvFileName(envName) {
|
|
120
|
+
return `${envFileName(envName)}.encrypted`;
|
|
121
|
+
}
|
|
122
|
+
function envRecordToFileContents(env) {
|
|
123
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
124
|
+
}
|
|
125
|
+
function validateEncryptionKey(encryptionKey) {
|
|
126
|
+
const keyBuffer = Buffer.from(encryptionKey, "hex");
|
|
127
|
+
if (keyBuffer.length !== 32) {
|
|
128
|
+
throw new Error(`Encryption key must be 32 bytes (64 hex characters), got ${keyBuffer.length} bytes`);
|
|
129
|
+
}
|
|
130
|
+
return keyBuffer;
|
|
131
|
+
}
|
|
132
|
+
function encryptEnvFileContents(envFileContents, encryptionKey) {
|
|
133
|
+
const env = parseEnv(envFileContents);
|
|
134
|
+
const keyBuffer = validateEncryptionKey(encryptionKey);
|
|
135
|
+
const result = {};
|
|
136
|
+
for (const [name, value] of Object.entries(env)) {
|
|
137
|
+
const iv = randomBytes(12);
|
|
138
|
+
const cipher = createCipheriv("aes-256-gcm", keyBuffer, iv);
|
|
139
|
+
const ciphertext = Buffer.concat([cipher.update(value, "utf-8"), cipher.final()]);
|
|
140
|
+
const authTag = cipher.getAuthTag();
|
|
141
|
+
result[name] = `${iv.toString("base64")}.${authTag.toString("base64")}.${ciphertext.toString("base64")}`;
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
function decryptEnvFileContents(envFileContents, encryptionKey) {
|
|
146
|
+
const env = parseEnv(envFileContents);
|
|
147
|
+
const keyBuffer = validateEncryptionKey(encryptionKey);
|
|
148
|
+
const result = {};
|
|
149
|
+
for (const [name, value] of Object.entries(env)) {
|
|
150
|
+
const parts = value.split(".");
|
|
151
|
+
if (parts.length !== 3) {
|
|
152
|
+
throw new Error(`Invalid encrypted value format for key "${name}"`);
|
|
153
|
+
}
|
|
154
|
+
const [ivB64, authTagB64, ciphertextB64] = parts;
|
|
155
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
156
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
157
|
+
const ciphertext = Buffer.from(ciphertextB64, "base64");
|
|
158
|
+
const decipher = createDecipheriv("aes-256-gcm", keyBuffer, iv);
|
|
159
|
+
decipher.setAuthTag(authTag);
|
|
160
|
+
result[name] = decipher.update(ciphertext).toString("utf-8") + decipher.final("utf-8");
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
function getEnvFilePath({ rootDir, envName: envNameParam }) {
|
|
165
|
+
const envName = envNameParam || process.env.ENV_NAME;
|
|
166
|
+
const candidates = envName ? [envFileName(envName), encryptedEnvFileName(envName), envFileName(null), encryptedEnvFileName(null)] : [envFileName(null), encryptedEnvFileName(null)];
|
|
167
|
+
for (const fileName of candidates) {
|
|
168
|
+
const filePath = join(rootDir, fileName);
|
|
169
|
+
if (existsSync(filePath)) {
|
|
170
|
+
return filePath;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
var Config = {
|
|
176
|
+
getConfig,
|
|
177
|
+
resolveConfig,
|
|
178
|
+
loadConfigFile,
|
|
179
|
+
getEnvVars,
|
|
180
|
+
generateEncryptedEnvFile,
|
|
181
|
+
generateDecryptedEnvFile,
|
|
182
|
+
generateEncryptionKey
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
Config
|
|
187
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
type ConfigOptions = {
|
|
2
|
+
rootDir: string;
|
|
3
|
+
configName: string;
|
|
4
|
+
envName: string | null;
|
|
5
|
+
encryptionKey: string | null;
|
|
6
|
+
};
|
|
7
|
+
/** Reads the `.env` file, imports the config file, and returns the resolved config. Does not modify `process.env`. */
|
|
8
|
+
declare function getConfig({ rootDir, configName, envName, encryptionKey }: ConfigOptions): Promise<NodeJS.Dict<string>>;
|
|
9
|
+
/** Reads the `.env` file, imports the config file, and writes the result into `process.env`. Keys already present are not overwritten. */
|
|
10
|
+
declare function loadConfigFile(options: ConfigOptions): Promise<void>;
|
|
11
|
+
/** Normalizes an imported config module into a flat string dictionary. Handles default exports, named exports, sync/async functions. */
|
|
12
|
+
declare function resolveConfig(module: any, env: NodeJS.Dict<string>): Promise<NodeJS.Dict<string>>;
|
|
13
|
+
/** Reads and parses an env file (plaintext or encrypted). Returns `{}` if no env file is found. */
|
|
14
|
+
declare function getEnvVars({ rootDir, envName, encryptionKey }: {
|
|
15
|
+
rootDir: string;
|
|
16
|
+
envName: string | null;
|
|
17
|
+
encryptionKey: string | null;
|
|
18
|
+
}): NodeJS.Dict<string>;
|
|
19
|
+
/** Reads a plaintext `.env` file and writes its AES-256-GCM encrypted counterpart. */
|
|
20
|
+
declare function generateEncryptedEnvFile({ rootDir, envName: envNameParam, encryptionKey }: {
|
|
21
|
+
rootDir: string;
|
|
22
|
+
envName: string | null;
|
|
23
|
+
encryptionKey: string;
|
|
24
|
+
}): void;
|
|
25
|
+
/** Reads an encrypted `.env.encrypted` file and writes the decrypted plaintext. Throws if the target exists unless `force` is true. */
|
|
26
|
+
declare function generateDecryptedEnvFile({ rootDir, envName: envNameParam, encryptionKey, force }: {
|
|
27
|
+
rootDir: string;
|
|
28
|
+
envName: string | null;
|
|
29
|
+
encryptionKey: string;
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}): void;
|
|
32
|
+
/** Generates a cryptographically random 32-byte key as a 64-character hex string. */
|
|
33
|
+
declare function generateEncryptionKey(): string;
|
|
34
|
+
type ConfigValue = Record<string, string | undefined>;
|
|
35
|
+
type Unwrap<T> = T extends (...any: any[]) => Promise<infer R> ? R : T extends (...any: any[]) => infer R ? R : T;
|
|
36
|
+
declare const Config: {
|
|
37
|
+
getConfig: typeof getConfig;
|
|
38
|
+
resolveConfig: typeof resolveConfig;
|
|
39
|
+
loadConfigFile: typeof loadConfigFile;
|
|
40
|
+
getEnvVars: typeof getEnvVars;
|
|
41
|
+
generateEncryptedEnvFile: typeof generateEncryptedEnvFile;
|
|
42
|
+
generateDecryptedEnvFile: typeof generateDecryptedEnvFile;
|
|
43
|
+
generateEncryptionKey: typeof generateEncryptionKey;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Configuration loader for Node.js applications.
|
|
47
|
+
*
|
|
48
|
+
* Reads `.env` files (plaintext or AES-256-GCM encrypted) and imports a TypeScript or JavaScript
|
|
49
|
+
* config file whose exported values are written to `process.env`. When the config file exports a
|
|
50
|
+
* function, it receives the parsed `.env` vars as its argument, so you can derive computed values
|
|
51
|
+
* (e.g., building a database URL from individual host/port/user parts). Existing `process.env`
|
|
52
|
+
* keys take precedence and are not overwritten.
|
|
53
|
+
*
|
|
54
|
+
* Config has zero `@maestro-js` dependencies. It is a standalone utility used by the CLI for
|
|
55
|
+
* `env:encrypt`, `env:decrypt`, and `env:gen-key` commands, and by applications at boot time
|
|
56
|
+
* via `node --import @maestro-js/config/load`.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { Config } from '@maestro-js/config'
|
|
61
|
+
*
|
|
62
|
+
* // Load config into process.env
|
|
63
|
+
* await Config.loadConfigFile({
|
|
64
|
+
* rootDir: process.cwd(),
|
|
65
|
+
* configName: 'app',
|
|
66
|
+
* envName: 'production',
|
|
67
|
+
* encryptionKey: process.env.MAESTRO_ENV_ENCRYPTION_KEY ?? null
|
|
68
|
+
* })
|
|
69
|
+
*
|
|
70
|
+
* // Or get config without modifying process.env
|
|
71
|
+
* const config = await Config.getConfig({ rootDir: '.', configName: 'app', envName: null, encryptionKey: null })
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
declare namespace Config {
|
|
75
|
+
/**
|
|
76
|
+
* Extracts the resolved key-value shape from a config module type. Handles all four export shapes:
|
|
77
|
+
* named exports, default object, default sync function, and default async function. Use it in an
|
|
78
|
+
* `environment.d.ts` file to make `process.env` type-safe.
|
|
79
|
+
*/
|
|
80
|
+
type Resolve<T extends ConfigValue | ((...any: any[]) => ConfigValue) | ((...any: any[]) => Promise<ConfigValue>) | {
|
|
81
|
+
default: ConfigValue | ((...any: any[]) => ConfigValue) | ((...any: any[]) => Promise<ConfigValue>);
|
|
82
|
+
}> = T extends {
|
|
83
|
+
default: infer D;
|
|
84
|
+
} ? Unwrap<D> : Unwrap<T>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { Config };
|
package/dist/index.js
ADDED
package/dist/load.d.ts
ADDED
package/dist/load.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Config
|
|
3
|
+
} from "./chunk-AVYD4MV2.js";
|
|
4
|
+
|
|
5
|
+
// src/load.ts
|
|
6
|
+
import "path";
|
|
7
|
+
var { configName, envName, encryptionKey, rootDir } = parseArgs(process.argv);
|
|
8
|
+
await Config.loadConfigFile({ rootDir, configName, envName, encryptionKey });
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const configName2 = getArgValue(argv, "--config");
|
|
11
|
+
if (!configName2) {
|
|
12
|
+
throw new Error("Missing required --config <name> argument");
|
|
13
|
+
}
|
|
14
|
+
const envName2 = getArgValue(argv, "--env") ?? null;
|
|
15
|
+
const encryptionKey2 = getArgValue(argv, "--key") ?? process.env["MAESTRO_ENV_ENCRYPTION_KEY"] ?? null;
|
|
16
|
+
const rootDir2 = getArgValue(argv, "--root") ?? process.cwd();
|
|
17
|
+
return { configName: configName2, envName: envName2, encryptionKey: encryptionKey2, rootDir: rootDir2 };
|
|
18
|
+
}
|
|
19
|
+
function getArgValue(argv, flag) {
|
|
20
|
+
const index = argv.indexOf(flag);
|
|
21
|
+
if (index === -1 || index + 1 >= argv.length) return void 0;
|
|
22
|
+
return argv[index + 1];
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestro-js/config",
|
|
3
|
+
"description": "Configuration loader for .env files and TypeScript/JavaScript config modules with AES-256-GCM encryption support. Use when working with @maestro-js/config, including loading config into process.env, reading/writing encrypted .env files, creating config modules, deriving computed env values, and generating encryption keys. Key capabilities: load config files, parse .env files (plaintext or encrypted), encrypt/decrypt env files, generate encryption keys, resolve config modules (named exports, default exports, sync/async functions), type-safe process.env via Config.Resolve utility type, and auto-load via node --import @maestro-js/config/load.",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./load": {
|
|
11
|
+
"types": "./dist/load.d.ts",
|
|
12
|
+
"default": "./dist/load.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^22.19.11"
|
|
17
|
+
},
|
|
18
|
+
"version": "1.0.0-alpha.0",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "restricted"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"license": "UNLICENSED",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.18.0"
|
|
28
|
+
},
|
|
29
|
+
"repository": "https://github.com/Marcato-Partners/maestro-js",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"watch": "tsup --watch",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "beartest ./tests/**/*",
|
|
35
|
+
"format": "prettier --write src/ tests/",
|
|
36
|
+
"lint": "prettier --check src/ tests/"
|
|
37
|
+
}
|
|
38
|
+
}
|