@splashcodex/api-key-manager 5.2.0 → 5.3.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 +206 -206
- package/bin/cli.js +180 -180
- package/dist/env/loader.js +14 -30
- package/dist/env/loader.js.map +1 -1
- package/dist/index.d.ts +30 -13
- package/dist/index.js +106 -31
- package/dist/index.js.map +1 -1
- package/dist/persistence/file.js +12 -1
- package/dist/persistence/file.js.map +1 -1
- package/dist/presets/base.d.ts +6 -0
- package/dist/presets/base.js +37 -1
- package/dist/presets/base.js.map +1 -1
- package/package.json +155 -151
- package/src/env/index.ts +14 -14
- package/src/env/loader.ts +225 -249
- package/src/index.ts +1155 -1071
- package/src/persistence/file.ts +96 -89
- package/src/persistence/index.ts +7 -7
- package/src/persistence/memory.ts +43 -43
- package/src/presets/anthropic.ts +78 -78
- package/src/presets/base.ts +383 -344
- package/src/presets/gemini.ts +84 -84
- package/src/presets/index.ts +11 -11
- package/src/presets/multi.ts +290 -290
- package/src/presets/openai.ts +69 -69
package/src/env/loader.ts
CHANGED
|
@@ -1,249 +1,225 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized Environment Loader
|
|
3
|
-
*
|
|
4
|
-
* Loads environment variables from ~/codedex/env/ so that all 35+ projects
|
|
5
|
-
* share a single source of truth for secrets. Portable via Google Drive.
|
|
6
|
-
*
|
|
7
|
-
* Directory structure:
|
|
8
|
-
* ~/codedex/env/
|
|
9
|
-
* common.env — REDIS_URL, DATABASE_URL, shared across all projects
|
|
10
|
-
* llm.env — GEMINI_API_KEY, OPENAI_API_KEY (comma-separated or JSON arrays)
|
|
11
|
-
* stripe.env — STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
|
|
12
|
-
* <custom>.env — Any additional groupings
|
|
13
|
-
*
|
|
14
|
-
* @module env/loader
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* // Load all env files from ~/codedex/env/
|
|
19
|
-
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
20
|
-
* loadCentralEnv();
|
|
21
|
-
*
|
|
22
|
-
* // Now process.env has all the values
|
|
23
|
-
* console.log(process.env.GEMINI_API_KEY);
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* // Load specific files only
|
|
29
|
-
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
30
|
-
* loadCentralEnv({ files: ['llm.env', 'common.env'] });
|
|
31
|
-
* ```
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```ts
|
|
35
|
-
* // Custom env directory (e.g., team-shared location)
|
|
36
|
-
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
37
|
-
* loadCentralEnv({ envDir: '/shared/team/env' });
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
42
|
-
import { join } from 'path';
|
|
43
|
-
import { homedir } from 'os';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* If
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
loaded
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* KEY=
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Read a single value from the centralized env without loading everything.
|
|
229
|
-
* Useful for one-off lookups.
|
|
230
|
-
*/
|
|
231
|
-
export function getCentralEnvVar(key: string, options?: { envDir?: string }): string | undefined {
|
|
232
|
-
const envDir = options?.envDir || getDefaultEnvDir();
|
|
233
|
-
|
|
234
|
-
if (!existsSync(envDir)) return undefined;
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
const files = readdirSync(envDir).filter(f => f.endsWith('.env')).sort();
|
|
238
|
-
|
|
239
|
-
for (const fileName of files) {
|
|
240
|
-
const content = readFileSync(join(envDir, fileName), 'utf-8');
|
|
241
|
-
const vars = parseEnvFile(content);
|
|
242
|
-
if (vars.has(key)) return vars.get(key);
|
|
243
|
-
}
|
|
244
|
-
} catch {
|
|
245
|
-
// Silent fail
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Environment Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads environment variables from ~/codedex/env/ so that all 35+ projects
|
|
5
|
+
* share a single source of truth for secrets. Portable via Google Drive.
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* ~/codedex/env/
|
|
9
|
+
* common.env — REDIS_URL, DATABASE_URL, shared across all projects
|
|
10
|
+
* llm.env — GEMINI_API_KEY, OPENAI_API_KEY (comma-separated or JSON arrays)
|
|
11
|
+
* stripe.env — STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
|
|
12
|
+
* <custom>.env — Any additional groupings
|
|
13
|
+
*
|
|
14
|
+
* @module env/loader
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Load all env files from ~/codedex/env/
|
|
19
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
20
|
+
* loadCentralEnv();
|
|
21
|
+
*
|
|
22
|
+
* // Now process.env has all the values
|
|
23
|
+
* console.log(process.env.GEMINI_API_KEY);
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* // Load specific files only
|
|
29
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
30
|
+
* loadCentralEnv({ files: ['llm.env', 'common.env'] });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* // Custom env directory (e.g., team-shared location)
|
|
36
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
37
|
+
* loadCentralEnv({ envDir: '/shared/team/env' });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
42
|
+
import { join } from 'path';
|
|
43
|
+
import { homedir } from 'os';
|
|
44
|
+
import { parse as dotenvParse } from 'dotenv';
|
|
45
|
+
|
|
46
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export interface LoadCentralEnvOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Path to the centralized env directory.
|
|
51
|
+
* Defaults to ~/codedex/env/
|
|
52
|
+
*
|
|
53
|
+
* Can also be set via CODEDEX_ENV_DIR environment variable.
|
|
54
|
+
*/
|
|
55
|
+
envDir?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Specific .env files to load (e.g., ['llm.env', 'common.env']).
|
|
59
|
+
* If omitted, loads ALL .env files in the directory.
|
|
60
|
+
*/
|
|
61
|
+
files?: string[];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* If true, central env values will NOT overwrite existing process.env values.
|
|
65
|
+
* Useful when a project needs to override a shared value locally.
|
|
66
|
+
* Default: false (central values take precedence over local .env)
|
|
67
|
+
*/
|
|
68
|
+
preserveExisting?: boolean;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* If true, silently continue when the env directory doesn't exist.
|
|
72
|
+
* If false, throw an error.
|
|
73
|
+
* Default: true (silent — for portability across dev environments)
|
|
74
|
+
*/
|
|
75
|
+
silent?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface LoadResult {
|
|
79
|
+
/** Whether the env directory was found and loaded */
|
|
80
|
+
loaded: boolean;
|
|
81
|
+
/** Path to the env directory that was used */
|
|
82
|
+
envDir: string;
|
|
83
|
+
/** List of .env files that were loaded */
|
|
84
|
+
filesLoaded: string[];
|
|
85
|
+
/** Number of environment variables set */
|
|
86
|
+
varsSet: number;
|
|
87
|
+
/** Variables that were skipped because they already existed (when preserveExisting=true) */
|
|
88
|
+
varsSkipped: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Parser ──────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse a .env file content string into key-value pairs.
|
|
95
|
+
*
|
|
96
|
+
* Delegates to `dotenv.parse()` for battle-tested parsing that handles:
|
|
97
|
+
* KEY=value
|
|
98
|
+
* KEY="quoted value with spaces"
|
|
99
|
+
* KEY='single quoted'
|
|
100
|
+
* KEY=`backtick quoted`
|
|
101
|
+
* MULTILINE="line1\nline2" (real newlines inside double quotes)
|
|
102
|
+
* KEY=value # inline comment
|
|
103
|
+
* export KEY=value (bash-style)
|
|
104
|
+
* # full-line comments
|
|
105
|
+
* \n, \r, \t escape sequences
|
|
106
|
+
*
|
|
107
|
+
* Returns a Map<string, string> for compatibility with the rest of the loader.
|
|
108
|
+
*/
|
|
109
|
+
function parseEnvFile(content: string): Map<string, string> {
|
|
110
|
+
const parsed = dotenvParse(content);
|
|
111
|
+
return new Map(Object.entries(parsed));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Loader ──────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Default env directory path: ~/codedex/env/
|
|
118
|
+
*/
|
|
119
|
+
export function getDefaultEnvDir(): string {
|
|
120
|
+
return process.env.CODEDEX_ENV_DIR || join(homedir(), 'codedex', 'env');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load environment variables from the centralized env directory into process.env.
|
|
125
|
+
*
|
|
126
|
+
* Call this once at the top of your app's entry point, before any code
|
|
127
|
+
* that reads process.env.
|
|
128
|
+
*/
|
|
129
|
+
export function loadCentralEnv(options: LoadCentralEnvOptions = {}): LoadResult {
|
|
130
|
+
const envDir = options.envDir || getDefaultEnvDir();
|
|
131
|
+
const silent = options.silent !== false;
|
|
132
|
+
const preserveExisting = options.preserveExisting === true;
|
|
133
|
+
|
|
134
|
+
const result: LoadResult = {
|
|
135
|
+
loaded: false,
|
|
136
|
+
envDir,
|
|
137
|
+
filesLoaded: [],
|
|
138
|
+
varsSet: 0,
|
|
139
|
+
varsSkipped: [],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Check if directory exists
|
|
143
|
+
if (!existsSync(envDir)) {
|
|
144
|
+
if (!silent) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Centralized env directory not found: ${envDir}\n` +
|
|
147
|
+
`Create it with: mkdir -p ${envDir}\n` +
|
|
148
|
+
`Or set CODEDEX_ENV_DIR to point to your env directory.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Determine which files to load
|
|
155
|
+
let filesToLoad: string[];
|
|
156
|
+
if (options.files) {
|
|
157
|
+
filesToLoad = options.files;
|
|
158
|
+
} else {
|
|
159
|
+
try {
|
|
160
|
+
filesToLoad = readdirSync(envDir)
|
|
161
|
+
.filter(f => f.endsWith('.env'))
|
|
162
|
+
.sort(); // Alphabetical order for deterministic loading
|
|
163
|
+
} catch {
|
|
164
|
+
if (!silent) throw new Error(`Cannot read env directory: ${envDir}`);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Load each file
|
|
170
|
+
for (const fileName of filesToLoad) {
|
|
171
|
+
const filePath = join(envDir, fileName);
|
|
172
|
+
|
|
173
|
+
if (!existsSync(filePath)) {
|
|
174
|
+
if (!silent) {
|
|
175
|
+
throw new Error(`Env file not found: ${filePath}`);
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
182
|
+
const vars = parseEnvFile(content);
|
|
183
|
+
|
|
184
|
+
for (const [key, value] of vars) {
|
|
185
|
+
if (preserveExisting && process.env[key] !== undefined) {
|
|
186
|
+
result.varsSkipped.push(key);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
process.env[key] = value;
|
|
190
|
+
result.varsSet++;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
result.filesLoaded.push(fileName);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (!silent) throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
result.loaded = result.filesLoaded.length > 0;
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Read a single value from the centralized env without loading everything.
|
|
205
|
+
* Useful for one-off lookups.
|
|
206
|
+
*/
|
|
207
|
+
export function getCentralEnvVar(key: string, options?: { envDir?: string }): string | undefined {
|
|
208
|
+
const envDir = options?.envDir || getDefaultEnvDir();
|
|
209
|
+
|
|
210
|
+
if (!existsSync(envDir)) return undefined;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const files = readdirSync(envDir).filter(f => f.endsWith('.env')).sort();
|
|
214
|
+
|
|
215
|
+
for (const fileName of files) {
|
|
216
|
+
const content = readFileSync(join(envDir, fileName), 'utf-8');
|
|
217
|
+
const vars = parseEnvFile(content);
|
|
218
|
+
if (vars.has(key)) return vars.get(key);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// Silent fail
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|