ultraenv 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 +2058 -0
- package/bin/ultraenv.mjs +3 -0
- package/dist/chunk-2USZPWLZ.js +288 -0
- package/dist/chunk-3UV2QNJL.js +270 -0
- package/dist/chunk-3VYXPTYV.js +179 -0
- package/dist/chunk-4XUYMRK5.js +366 -0
- package/dist/chunk-5G2DU52U.js +189 -0
- package/dist/chunk-6KS56D6E.js +172 -0
- package/dist/chunk-AWN6ADV7.js +328 -0
- package/dist/chunk-CHVO6NWI.js +203 -0
- package/dist/chunk-CIFMBJ4H.js +3975 -0
- package/dist/chunk-GC7RXHLA.js +253 -0
- package/dist/chunk-HFXQGJY3.js +445 -0
- package/dist/chunk-IGFVP24Q.js +91 -0
- package/dist/chunk-IKPTKALB.js +78 -0
- package/dist/chunk-JB7RKV3C.js +66 -0
- package/dist/chunk-MNVFG7H4.js +611 -0
- package/dist/chunk-MSXMESFP.js +1910 -0
- package/dist/chunk-N5PAV4NM.js +127 -0
- package/dist/chunk-NBOABPHM.js +158 -0
- package/dist/chunk-OMAOROL4.js +49 -0
- package/dist/chunk-R7PZRSZ7.js +105 -0
- package/dist/chunk-TE7HPLA6.js +73 -0
- package/dist/chunk-TMT5KCO3.js +101 -0
- package/dist/chunk-UEWYFN6A.js +189 -0
- package/dist/chunk-WMHN5RW2.js +128 -0
- package/dist/chunk-XC65ORJ5.js +70 -0
- package/dist/chunk-YMMP4VQL.js +118 -0
- package/dist/chunk-YN2KGTCB.js +33 -0
- package/dist/chunk-YTICOB5M.js +65 -0
- package/dist/chunk-YVWLXFUT.js +107 -0
- package/dist/ci-check-sync-VBMSVWIV.js +48 -0
- package/dist/ci-scan-24MT5XGS.js +41 -0
- package/dist/ci-setup-C2NKEFRD.js +135 -0
- package/dist/ci-validate-7AW24LSQ.js +57 -0
- package/dist/cli/index.cjs +9217 -0
- package/dist/cli/index.d.cts +9 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +339 -0
- package/dist/comparator-RDKX3OI7.js +13 -0
- package/dist/completion-MW35C2XO.js +168 -0
- package/dist/config-O5YRQP5Z.js +13 -0
- package/dist/debug-PTPXAF3K.js +131 -0
- package/dist/declaration-LEME4AFZ.js +10 -0
- package/dist/doctor-FZAUPKHS.js +129 -0
- package/dist/envs-compare-5K3HESX5.js +49 -0
- package/dist/envs-create-2XXHXMGA.js +58 -0
- package/dist/envs-list-NQM5252B.js +59 -0
- package/dist/envs-switch-6L2AQYID.js +50 -0
- package/dist/envs-validate-FL73Q76T.js +89 -0
- package/dist/fs-VH7ATUS3.js +31 -0
- package/dist/generator-LFZBMZZS.js +14 -0
- package/dist/git-BZS4DPAI.js +30 -0
- package/dist/help-3XJBXEHE.js +121 -0
- package/dist/index.cjs +12907 -0
- package/dist/index.d.cts +2562 -0
- package/dist/index.d.ts +2562 -0
- package/dist/index.js +3212 -0
- package/dist/init-Y7JQ2KYJ.js +146 -0
- package/dist/install-hook-SKXIV6NV.js +111 -0
- package/dist/json-schema-I26YNQBH.js +10 -0
- package/dist/key-manager-O3G55WPU.js +25 -0
- package/dist/middleware/express.cjs +103 -0
- package/dist/middleware/express.d.cts +115 -0
- package/dist/middleware/express.d.ts +115 -0
- package/dist/middleware/express.js +8 -0
- package/dist/middleware/fastify.cjs +91 -0
- package/dist/middleware/fastify.d.cts +111 -0
- package/dist/middleware/fastify.d.ts +111 -0
- package/dist/middleware/fastify.js +8 -0
- package/dist/module-IDIZPP4M.js +10 -0
- package/dist/protect-NCWPM6VC.js +161 -0
- package/dist/scan-TRLY36TT.js +58 -0
- package/dist/schema/index.cjs +4074 -0
- package/dist/schema/index.d.cts +1244 -0
- package/dist/schema/index.d.ts +1244 -0
- package/dist/schema/index.js +152 -0
- package/dist/sync-TMHMTLH2.js +186 -0
- package/dist/typegen-SQOSXBWM.js +80 -0
- package/dist/validate-IOAM5HWS.js +100 -0
- package/dist/vault-decrypt-U6HJZNBV.js +111 -0
- package/dist/vault-diff-B3ZOQTWI.js +132 -0
- package/dist/vault-encrypt-GUSLCSKS.js +112 -0
- package/dist/vault-init-GUBOTOUL.js +106 -0
- package/dist/vault-rekey-DAHT7JCN.js +132 -0
- package/dist/vault-status-GDLRU2OK.js +90 -0
- package/dist/vault-verify-CD76FJSF.js +102 -0
- package/package.json +106 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeIntegrity,
|
|
3
|
+
computeVaultChecksum,
|
|
4
|
+
verifyIntegrity,
|
|
5
|
+
verifyVaultChecksum
|
|
6
|
+
} from "./chunk-N5PAV4NM.js";
|
|
7
|
+
import {
|
|
8
|
+
compareEnvironments,
|
|
9
|
+
formatComparison
|
|
10
|
+
} from "./chunk-3UV2QNJL.js";
|
|
11
|
+
import {
|
|
12
|
+
discoverEnvironments,
|
|
13
|
+
getActiveEnvironment,
|
|
14
|
+
listEnvironments,
|
|
15
|
+
switchEnvironment,
|
|
16
|
+
validateAllEnvironments
|
|
17
|
+
} from "./chunk-GC7RXHLA.js";
|
|
18
|
+
import {
|
|
19
|
+
createEnvironment,
|
|
20
|
+
duplicateEnvironment,
|
|
21
|
+
removeEnvironment
|
|
22
|
+
} from "./chunk-AWN6ADV7.js";
|
|
23
|
+
import {
|
|
24
|
+
addCustomPattern,
|
|
25
|
+
detectHighEntropyStrings,
|
|
26
|
+
formatScanResult,
|
|
27
|
+
isHighEntropy,
|
|
28
|
+
matchPatterns,
|
|
29
|
+
removeCustomPattern,
|
|
30
|
+
resetPatterns,
|
|
31
|
+
scan,
|
|
32
|
+
scanDiff,
|
|
33
|
+
scanFiles,
|
|
34
|
+
scanGitHistory,
|
|
35
|
+
scanLineForEntropy,
|
|
36
|
+
scanStagedFiles,
|
|
37
|
+
shannonEntropy
|
|
38
|
+
} from "./chunk-MSXMESFP.js";
|
|
39
|
+
import {
|
|
40
|
+
getEnvironmentData,
|
|
41
|
+
getVaultEnvironments,
|
|
42
|
+
parseVaultFile,
|
|
43
|
+
readVaultFile,
|
|
44
|
+
serializeVaultFile,
|
|
45
|
+
writeVaultFile
|
|
46
|
+
} from "./chunk-NBOABPHM.js";
|
|
47
|
+
import "./chunk-R7PZRSZ7.js";
|
|
48
|
+
import {
|
|
49
|
+
generateExampleContent,
|
|
50
|
+
generateExampleFile,
|
|
51
|
+
needsUpdate
|
|
52
|
+
} from "./chunk-CHVO6NWI.js";
|
|
53
|
+
import {
|
|
54
|
+
maskValue
|
|
55
|
+
} from "./chunk-TE7HPLA6.js";
|
|
56
|
+
import {
|
|
57
|
+
compareSync,
|
|
58
|
+
compareValues
|
|
59
|
+
} from "./chunk-YVWLXFUT.js";
|
|
60
|
+
import {
|
|
61
|
+
generateDeclaration
|
|
62
|
+
} from "./chunk-IGFVP24Q.js";
|
|
63
|
+
import {
|
|
64
|
+
generateModule
|
|
65
|
+
} from "./chunk-YMMP4VQL.js";
|
|
66
|
+
import {
|
|
67
|
+
generateJsonSchema
|
|
68
|
+
} from "./chunk-6KS56D6E.js";
|
|
69
|
+
import {
|
|
70
|
+
expandVariables,
|
|
71
|
+
load,
|
|
72
|
+
loadSync,
|
|
73
|
+
loadWithResult,
|
|
74
|
+
loadWithResultSync,
|
|
75
|
+
mergeCascade,
|
|
76
|
+
resolveCascade
|
|
77
|
+
} from "./chunk-MNVFG7H4.js";
|
|
78
|
+
import {
|
|
79
|
+
parseEnvFile
|
|
80
|
+
} from "./chunk-HFXQGJY3.js";
|
|
81
|
+
import {
|
|
82
|
+
healthCheckRoute,
|
|
83
|
+
ultraenvMiddleware
|
|
84
|
+
} from "./chunk-IKPTKALB.js";
|
|
85
|
+
import {
|
|
86
|
+
createUltraenvPlugin,
|
|
87
|
+
ultraenvPlugin
|
|
88
|
+
} from "./chunk-JB7RKV3C.js";
|
|
89
|
+
import {
|
|
90
|
+
defineEnv,
|
|
91
|
+
t,
|
|
92
|
+
tryDefineEnv,
|
|
93
|
+
validate
|
|
94
|
+
} from "./chunk-CIFMBJ4H.js";
|
|
95
|
+
import {
|
|
96
|
+
findConfig,
|
|
97
|
+
loadConfig
|
|
98
|
+
} from "./chunk-4XUYMRK5.js";
|
|
99
|
+
import {
|
|
100
|
+
copyFile,
|
|
101
|
+
ensureDir,
|
|
102
|
+
exists,
|
|
103
|
+
findUp,
|
|
104
|
+
isDirectory,
|
|
105
|
+
isFile,
|
|
106
|
+
listFiles,
|
|
107
|
+
readFile,
|
|
108
|
+
readFileSync,
|
|
109
|
+
removeFile,
|
|
110
|
+
writeFile
|
|
111
|
+
} from "./chunk-3VYXPTYV.js";
|
|
112
|
+
import {
|
|
113
|
+
deriveEnvironmentKey,
|
|
114
|
+
formatKey,
|
|
115
|
+
generateKeysFile,
|
|
116
|
+
generateMasterKey,
|
|
117
|
+
isValidKeyFormat,
|
|
118
|
+
maskKey,
|
|
119
|
+
parseKey,
|
|
120
|
+
parseKeysFile,
|
|
121
|
+
rotateKey
|
|
122
|
+
} from "./chunk-UEWYFN6A.js";
|
|
123
|
+
import {
|
|
124
|
+
decryptEnvironment,
|
|
125
|
+
decryptValue,
|
|
126
|
+
encryptEnvironment,
|
|
127
|
+
encryptValue,
|
|
128
|
+
isEncryptedValue
|
|
129
|
+
} from "./chunk-2USZPWLZ.js";
|
|
130
|
+
import {
|
|
131
|
+
VERSION
|
|
132
|
+
} from "./chunk-XC65ORJ5.js";
|
|
133
|
+
import {
|
|
134
|
+
ConfigError,
|
|
135
|
+
EncryptionError,
|
|
136
|
+
FileSystemError,
|
|
137
|
+
InterpolationError,
|
|
138
|
+
ParseError,
|
|
139
|
+
ScanError,
|
|
140
|
+
UltraenvError,
|
|
141
|
+
ValidationError,
|
|
142
|
+
VaultError,
|
|
143
|
+
isUltraenvError
|
|
144
|
+
} from "./chunk-5G2DU52U.js";
|
|
145
|
+
|
|
146
|
+
// src/core/watcher.ts
|
|
147
|
+
import { watch } from "fs";
|
|
148
|
+
import { existsSync } from "fs";
|
|
149
|
+
import { resolve, join } from "path";
|
|
150
|
+
var DEFAULT_WATCH_OPTIONS = {
|
|
151
|
+
files: [".env", ".env.local", ".env.development", ".env.production", ".env.staging"],
|
|
152
|
+
recursive: false,
|
|
153
|
+
debounceMs: 100,
|
|
154
|
+
initial: false,
|
|
155
|
+
pollIntervalMs: 0,
|
|
156
|
+
ignore: []
|
|
157
|
+
};
|
|
158
|
+
var EnvFileWatcher = class {
|
|
159
|
+
state;
|
|
160
|
+
constructor(options) {
|
|
161
|
+
this.state = {
|
|
162
|
+
nativeWatchers: /* @__PURE__ */ new Map(),
|
|
163
|
+
debounceTimers: /* @__PURE__ */ new Map(),
|
|
164
|
+
callbacks: /* @__PURE__ */ new Map(),
|
|
165
|
+
active: false,
|
|
166
|
+
cascadeResult: null,
|
|
167
|
+
watchOptions: { ...DEFAULT_WATCH_OPTIONS, ...options?.watchOptions },
|
|
168
|
+
cascadeOptions: options?.cascadeOptions ?? {}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Start watching all .env files in the cascade.
|
|
173
|
+
* If `initial` is true in WatchOptions, emits a 'ready' event immediately.
|
|
174
|
+
*/
|
|
175
|
+
start() {
|
|
176
|
+
if (this.state.active) return;
|
|
177
|
+
this.state.active = true;
|
|
178
|
+
this.state.cascadeResult = resolveCascade(this.state.cascadeOptions);
|
|
179
|
+
const filesToWatch = this.state.cascadeResult.existingFiles;
|
|
180
|
+
this.watchDirectory(this.state.cascadeResult.envDir);
|
|
181
|
+
for (const entry of filesToWatch) {
|
|
182
|
+
this.watchFile(entry.absolutePath);
|
|
183
|
+
}
|
|
184
|
+
if (this.state.watchOptions.initial) {
|
|
185
|
+
this.emitEvent({
|
|
186
|
+
type: "change",
|
|
187
|
+
path: "",
|
|
188
|
+
timestamp: Date.now()
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Stop watching all files and clean up resources.
|
|
194
|
+
*/
|
|
195
|
+
stop() {
|
|
196
|
+
if (!this.state.active) return;
|
|
197
|
+
this.state.active = false;
|
|
198
|
+
for (const [, nativeWatcher] of this.state.nativeWatchers) {
|
|
199
|
+
try {
|
|
200
|
+
nativeWatcher.close();
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
this.state.nativeWatchers.clear();
|
|
205
|
+
for (const [, timer] of this.state.debounceTimers) {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
}
|
|
208
|
+
this.state.debounceTimers.clear();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Whether the watcher is currently active.
|
|
212
|
+
*/
|
|
213
|
+
get active() {
|
|
214
|
+
return this.state.active;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Register a callback for a specific event type.
|
|
218
|
+
*
|
|
219
|
+
* @param event - The event type: 'change', 'error', or 'ready'
|
|
220
|
+
* @param callback - The callback to invoke when the event occurs
|
|
221
|
+
* @returns This watcher instance (for chaining)
|
|
222
|
+
*/
|
|
223
|
+
on(event, callback) {
|
|
224
|
+
if (!this.state.callbacks.has(event)) {
|
|
225
|
+
this.state.callbacks.set(event, /* @__PURE__ */ new Set());
|
|
226
|
+
}
|
|
227
|
+
this.state.callbacks.get(event).add(callback);
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Remove a previously registered callback.
|
|
232
|
+
*
|
|
233
|
+
* @param event - The event type
|
|
234
|
+
* @param callback - The callback to remove
|
|
235
|
+
* @returns This watcher instance (for chaining)
|
|
236
|
+
*/
|
|
237
|
+
off(event, callback) {
|
|
238
|
+
const callbacks = this.state.callbacks.get(event);
|
|
239
|
+
if (callbacks !== void 0) {
|
|
240
|
+
callbacks.delete(callback);
|
|
241
|
+
}
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Stop watching and release all resources (alias for stop()).
|
|
246
|
+
*/
|
|
247
|
+
close() {
|
|
248
|
+
this.stop();
|
|
249
|
+
}
|
|
250
|
+
// -------------------------------------------------------------------------
|
|
251
|
+
// Private Methods
|
|
252
|
+
// -------------------------------------------------------------------------
|
|
253
|
+
/**
|
|
254
|
+
* Watch a specific file for changes.
|
|
255
|
+
*/
|
|
256
|
+
watchFile(filePath) {
|
|
257
|
+
if (!existsSync(filePath)) return;
|
|
258
|
+
if (this.state.nativeWatchers.has(filePath)) return;
|
|
259
|
+
if (this.shouldIgnore(filePath)) return;
|
|
260
|
+
try {
|
|
261
|
+
const nativeWatcher = watch(filePath, {
|
|
262
|
+
persistent: true
|
|
263
|
+
}, (eventType, filename) => {
|
|
264
|
+
this.handleFileEvent(eventType, filePath, filename);
|
|
265
|
+
});
|
|
266
|
+
nativeWatcher.on("error", () => {
|
|
267
|
+
this.emitChangeEvent(filePath);
|
|
268
|
+
this.state.nativeWatchers.delete(filePath);
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
if (this.state.active && existsSync(filePath)) {
|
|
271
|
+
this.watchFile(filePath);
|
|
272
|
+
}
|
|
273
|
+
}, 1e3);
|
|
274
|
+
});
|
|
275
|
+
this.state.nativeWatchers.set(filePath, nativeWatcher);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw new FileSystemError(`Failed to watch file "${filePath}"`, {
|
|
278
|
+
path: filePath,
|
|
279
|
+
operation: "watch",
|
|
280
|
+
cause: error instanceof Error ? error : void 0
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Watch a directory for file additions/removals.
|
|
286
|
+
*/
|
|
287
|
+
watchDirectory(dirPath) {
|
|
288
|
+
const dirWatchKey = `__dir__:${dirPath}`;
|
|
289
|
+
if (this.state.nativeWatchers.has(dirWatchKey)) return;
|
|
290
|
+
try {
|
|
291
|
+
const nativeWatcher = watch(dirPath, {
|
|
292
|
+
persistent: true
|
|
293
|
+
}, (eventType, filename) => {
|
|
294
|
+
if (filename === null) return;
|
|
295
|
+
const fullPath = resolve(join(dirPath, filename));
|
|
296
|
+
if (isEnvFileName(filename) && !this.shouldIgnore(fullPath)) {
|
|
297
|
+
if (eventType === "rename") {
|
|
298
|
+
if (existsSync(fullPath) && !this.state.nativeWatchers.has(fullPath)) {
|
|
299
|
+
this.watchFile(fullPath);
|
|
300
|
+
this.debouncedEmit(fullPath, "add");
|
|
301
|
+
} else if (!existsSync(fullPath) && this.state.nativeWatchers.has(fullPath)) {
|
|
302
|
+
const watcher = this.state.nativeWatchers.get(fullPath);
|
|
303
|
+
if (watcher !== void 0) {
|
|
304
|
+
watcher.close();
|
|
305
|
+
this.state.nativeWatchers.delete(fullPath);
|
|
306
|
+
}
|
|
307
|
+
this.debouncedEmit(fullPath, "unlink");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
nativeWatcher.on("error", () => {
|
|
313
|
+
this.state.nativeWatchers.delete(dirWatchKey);
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
if (this.state.active) {
|
|
316
|
+
this.watchDirectory(dirPath);
|
|
317
|
+
}
|
|
318
|
+
}, 5e3);
|
|
319
|
+
});
|
|
320
|
+
this.state.nativeWatchers.set(dirWatchKey, nativeWatcher);
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Handle a file system event from a file watcher.
|
|
326
|
+
*/
|
|
327
|
+
handleFileEvent(eventType, filePath, _filename) {
|
|
328
|
+
if (eventType === "change" || eventType === "rename") {
|
|
329
|
+
if (existsSync(filePath)) {
|
|
330
|
+
this.debouncedEmit(filePath, "change");
|
|
331
|
+
} else {
|
|
332
|
+
this.debouncedEmit(filePath, "unlink");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Emit a change event after debouncing.
|
|
338
|
+
*/
|
|
339
|
+
debouncedEmit(filePath, type) {
|
|
340
|
+
const debounceMs = this.state.watchOptions.debounceMs;
|
|
341
|
+
const existingTimer = this.state.debounceTimers.get(filePath);
|
|
342
|
+
if (existingTimer !== void 0) {
|
|
343
|
+
clearTimeout(existingTimer);
|
|
344
|
+
}
|
|
345
|
+
const timer = setTimeout(() => {
|
|
346
|
+
this.state.debounceTimers.delete(filePath);
|
|
347
|
+
this.emitEvent({
|
|
348
|
+
type,
|
|
349
|
+
path: filePath,
|
|
350
|
+
timestamp: Date.now()
|
|
351
|
+
});
|
|
352
|
+
}, debounceMs);
|
|
353
|
+
this.state.debounceTimers.set(filePath, timer);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Emit a change event (used by error recovery).
|
|
357
|
+
*/
|
|
358
|
+
emitChangeEvent(filePath) {
|
|
359
|
+
this.debouncedEmit(filePath, "change");
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Emit an event to all registered change callbacks.
|
|
363
|
+
*/
|
|
364
|
+
emitEvent(event) {
|
|
365
|
+
const changeCallbacks = this.state.callbacks.get("change");
|
|
366
|
+
if (changeCallbacks !== void 0) {
|
|
367
|
+
for (const cb of changeCallbacks) {
|
|
368
|
+
try {
|
|
369
|
+
cb(event);
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (event.type === "unlink") {
|
|
375
|
+
const errorCallbacks = this.state.callbacks.get("error");
|
|
376
|
+
if (errorCallbacks !== void 0) {
|
|
377
|
+
for (const cb of errorCallbacks) {
|
|
378
|
+
try {
|
|
379
|
+
cb(event);
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Check if a file path should be ignored based on ignore patterns.
|
|
388
|
+
*/
|
|
389
|
+
shouldIgnore(filePath) {
|
|
390
|
+
const patterns = this.state.watchOptions.ignore;
|
|
391
|
+
const fileName = filePath.split("/").pop() ?? filePath;
|
|
392
|
+
for (const pattern of patterns) {
|
|
393
|
+
if (pattern === fileName) return true;
|
|
394
|
+
if (pattern.startsWith("*") && fileName.endsWith(pattern.slice(1))) return true;
|
|
395
|
+
if (pattern.endsWith("*") && fileName.startsWith(pattern.slice(0, -1))) return true;
|
|
396
|
+
if (pattern.includes("*")) {
|
|
397
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
398
|
+
if (regex.test(fileName)) return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function createWatcher(options) {
|
|
405
|
+
return new EnvFileWatcher(options);
|
|
406
|
+
}
|
|
407
|
+
function isEnvFileName(name) {
|
|
408
|
+
if (typeof name !== "string") return false;
|
|
409
|
+
return name === ".env" || name.startsWith(".env.");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/vault/secure-memory.ts
|
|
413
|
+
import { randomBytes, timingSafeEqual as cryptoTimingSafeEqual } from "crypto";
|
|
414
|
+
var SecureBuffer = class _SecureBuffer {
|
|
415
|
+
_buffer;
|
|
416
|
+
_length;
|
|
417
|
+
_zeroed;
|
|
418
|
+
/** Track all live SecureBuffers for FinalizationRegistry cleanup */
|
|
419
|
+
static registry = new FinalizationRegistry((buf) => {
|
|
420
|
+
buf.fill(0);
|
|
421
|
+
});
|
|
422
|
+
/** Counter for generating unique token identifiers */
|
|
423
|
+
static _instanceCount = 0;
|
|
424
|
+
/**
|
|
425
|
+
* Create a new SecureBuffer.
|
|
426
|
+
*
|
|
427
|
+
* @param size - The size of the buffer in bytes.
|
|
428
|
+
* @throws RangeError if size is not a positive integer.
|
|
429
|
+
*/
|
|
430
|
+
constructor(size) {
|
|
431
|
+
if (size < 1 || !Number.isInteger(size)) {
|
|
432
|
+
throw new RangeError(
|
|
433
|
+
`SecureBuffer: size must be a positive integer, got ${size}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
this._buffer = Buffer.alloc(size);
|
|
437
|
+
this._length = size;
|
|
438
|
+
this._zeroed = false;
|
|
439
|
+
_SecureBuffer._instanceCount++;
|
|
440
|
+
_SecureBuffer.registry.register(this, this._buffer, this);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Create a SecureBuffer from an existing Buffer.
|
|
444
|
+
* The source buffer is NOT modified — a copy is made.
|
|
445
|
+
*
|
|
446
|
+
* @param source - The source buffer to copy.
|
|
447
|
+
* @returns A new SecureBuffer containing a copy of the source data.
|
|
448
|
+
*/
|
|
449
|
+
static from(source) {
|
|
450
|
+
const buf = new _SecureBuffer(source.length);
|
|
451
|
+
Buffer.from(source).copy(buf._buffer);
|
|
452
|
+
buf._zeroed = false;
|
|
453
|
+
return buf;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Create a SecureBuffer from a string.
|
|
457
|
+
*
|
|
458
|
+
* @param str - The string to encode.
|
|
459
|
+
* @param encoding - The character encoding (default: 'utf-8').
|
|
460
|
+
* @returns A new SecureBuffer containing the encoded string.
|
|
461
|
+
*/
|
|
462
|
+
static fromString(str, encoding = "utf-8") {
|
|
463
|
+
const buf = new _SecureBuffer(Buffer.byteLength(str, encoding));
|
|
464
|
+
buf._buffer.write(str, encoding);
|
|
465
|
+
buf._zeroed = false;
|
|
466
|
+
return buf;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* The size of the buffer in bytes.
|
|
470
|
+
*/
|
|
471
|
+
get length() {
|
|
472
|
+
return this._length;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Whether the buffer has been zeroed.
|
|
476
|
+
*/
|
|
477
|
+
get isZeroed() {
|
|
478
|
+
return this._zeroed;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Fill the buffer with the specified byte value.
|
|
482
|
+
*
|
|
483
|
+
* @param value - The byte value to fill with (0-255).
|
|
484
|
+
* @returns This SecureBuffer for chaining.
|
|
485
|
+
*/
|
|
486
|
+
fill(value) {
|
|
487
|
+
if (value < 0 || value > 255 || !Number.isInteger(value)) {
|
|
488
|
+
throw new RangeError(
|
|
489
|
+
`SecureBuffer.fill: value must be an integer 0-255, got ${value}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
this._buffer.fill(value);
|
|
493
|
+
this._zeroed = value === 0;
|
|
494
|
+
return this;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Fill the buffer with random bytes.
|
|
498
|
+
*
|
|
499
|
+
* @returns This SecureBuffer for chaining.
|
|
500
|
+
*/
|
|
501
|
+
fillRandom() {
|
|
502
|
+
randomBytes(this._length).copy(this._buffer);
|
|
503
|
+
this._zeroed = false;
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Zero out all bytes in the buffer.
|
|
508
|
+
*
|
|
509
|
+
* This should be called as soon as the sensitive data is no longer needed.
|
|
510
|
+
* The buffer will also be zeroed automatically on garbage collection, but
|
|
511
|
+
* explicit zeroing provides faster cleanup for time-sensitive operations.
|
|
512
|
+
*
|
|
513
|
+
* @returns This SecureBuffer for chaining.
|
|
514
|
+
*/
|
|
515
|
+
zero() {
|
|
516
|
+
this._buffer.fill(0);
|
|
517
|
+
this._zeroed = true;
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Get a read-only copy of the underlying buffer.
|
|
522
|
+
*
|
|
523
|
+
* **Warning**: This returns a COPY of the buffer contents.
|
|
524
|
+
* The caller is responsible for zeroing the returned buffer when done.
|
|
525
|
+
* Use toString() when possible to avoid managing raw buffers.
|
|
526
|
+
*
|
|
527
|
+
* @returns A copy of the buffer contents.
|
|
528
|
+
*/
|
|
529
|
+
getBuffer() {
|
|
530
|
+
if (this._zeroed) {
|
|
531
|
+
return Buffer.alloc(this._length);
|
|
532
|
+
}
|
|
533
|
+
return Buffer.from(this._buffer);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Get a single byte at the specified index.
|
|
537
|
+
*
|
|
538
|
+
* @param index - The byte index.
|
|
539
|
+
* @returns The byte value (0-255).
|
|
540
|
+
*/
|
|
541
|
+
getByte(index) {
|
|
542
|
+
if (index < 0 || index >= this._length) {
|
|
543
|
+
throw new RangeError(
|
|
544
|
+
`SecureBuffer.getByte: index ${index} out of bounds (length: ${this._length})`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return this._buffer[index];
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Convert the buffer contents to a string.
|
|
551
|
+
*
|
|
552
|
+
* @param encoding - The character encoding (default: 'utf-8').
|
|
553
|
+
* @returns The decoded string.
|
|
554
|
+
*/
|
|
555
|
+
toString(encoding = "utf-8") {
|
|
556
|
+
if (this._zeroed) {
|
|
557
|
+
return "";
|
|
558
|
+
}
|
|
559
|
+
return this._buffer.toString(encoding);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Convert to hex string.
|
|
563
|
+
*
|
|
564
|
+
* @returns Lowercase hex string.
|
|
565
|
+
*/
|
|
566
|
+
toHex() {
|
|
567
|
+
if (this._zeroed) {
|
|
568
|
+
return "0".repeat(this._length * 2);
|
|
569
|
+
}
|
|
570
|
+
return this._buffer.toString("hex");
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Convert to base64 string.
|
|
574
|
+
*
|
|
575
|
+
* @returns Base64-encoded string.
|
|
576
|
+
*/
|
|
577
|
+
toBase64() {
|
|
578
|
+
if (this._zeroed) {
|
|
579
|
+
return "";
|
|
580
|
+
}
|
|
581
|
+
return this._buffer.toString("base64");
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* JSON serialization — never exposes the buffer contents.
|
|
585
|
+
* Returns a placeholder string to prevent accidental logging of secrets.
|
|
586
|
+
*
|
|
587
|
+
* @returns A safe placeholder string.
|
|
588
|
+
*/
|
|
589
|
+
toJSON() {
|
|
590
|
+
return "[SecureBuffer]";
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Custom inspect for console.log — never exposes contents.
|
|
594
|
+
*/
|
|
595
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
596
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")](_depth, _options) {
|
|
597
|
+
return `[SecureBuffer: ${this._length} bytes${this._zeroed ? " (zeroed)" : ""}]`;
|
|
598
|
+
}
|
|
599
|
+
/* v8 ignore stop */
|
|
600
|
+
/**
|
|
601
|
+
* Manual cleanup — call when the SecureBuffer is no longer needed.
|
|
602
|
+
* Zeros the buffer and unregisters from the FinalizationRegistry.
|
|
603
|
+
*
|
|
604
|
+
* After calling dispose(), the buffer cannot be used.
|
|
605
|
+
*/
|
|
606
|
+
dispose() {
|
|
607
|
+
this._buffer.fill(0);
|
|
608
|
+
this._zeroed = true;
|
|
609
|
+
_SecureBuffer.registry.unregister(this);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
var SecureString = class {
|
|
613
|
+
_buffer;
|
|
614
|
+
_disposed;
|
|
615
|
+
/**
|
|
616
|
+
* Create a new SecureString.
|
|
617
|
+
*
|
|
618
|
+
* **Internal**: Use the `createSecureString()` factory function instead
|
|
619
|
+
* of calling this constructor directly.
|
|
620
|
+
*
|
|
621
|
+
* @param buffer - The SecureBuffer backing this string.
|
|
622
|
+
* @internal
|
|
623
|
+
*/
|
|
624
|
+
constructor(buffer) {
|
|
625
|
+
this._buffer = buffer;
|
|
626
|
+
this._disposed = false;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* The string value.
|
|
630
|
+
* Returns empty string if the backing buffer has been zeroed/disposed.
|
|
631
|
+
*/
|
|
632
|
+
get value() {
|
|
633
|
+
if (this._disposed) return "";
|
|
634
|
+
return this._buffer.toString("utf-8");
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* The length of the string in characters.
|
|
638
|
+
*/
|
|
639
|
+
get length() {
|
|
640
|
+
return this.value.length;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Whether this SecureString has been disposed.
|
|
644
|
+
*/
|
|
645
|
+
get disposed() {
|
|
646
|
+
return this._disposed;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Zero the backing buffer and mark as disposed.
|
|
650
|
+
* After calling dispose(), value will always return ''.
|
|
651
|
+
*/
|
|
652
|
+
dispose() {
|
|
653
|
+
this._buffer.zero();
|
|
654
|
+
this._buffer.dispose();
|
|
655
|
+
this._disposed = true;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* JSON serialization — never exposes the string contents.
|
|
659
|
+
*/
|
|
660
|
+
toJSON() {
|
|
661
|
+
return "[SecureString]";
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Custom inspect for console.log.
|
|
665
|
+
*/
|
|
666
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
667
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")](_depth, _options) {
|
|
668
|
+
return `[SecureString: ${this._disposed ? "disposed" : `${this.length} chars`}]`;
|
|
669
|
+
}
|
|
670
|
+
/* v8 ignore stop */
|
|
671
|
+
};
|
|
672
|
+
function createSecureString(value) {
|
|
673
|
+
const buffer = SecureBuffer.fromString(value, "utf-8");
|
|
674
|
+
return new SecureString(buffer);
|
|
675
|
+
}
|
|
676
|
+
function createSecureStringFromBuffer(buffer, encoding = "utf-8") {
|
|
677
|
+
const secureBuf = SecureBuffer.from(buffer);
|
|
678
|
+
const strBuf = SecureBuffer.fromString(buffer.toString(encoding), encoding);
|
|
679
|
+
secureBuf.zero();
|
|
680
|
+
return new SecureString(strBuf);
|
|
681
|
+
}
|
|
682
|
+
function wipeString(str) {
|
|
683
|
+
try {
|
|
684
|
+
const chars = str.split("");
|
|
685
|
+
for (let i = 0; i < chars.length; i++) {
|
|
686
|
+
chars[i] = "\0";
|
|
687
|
+
}
|
|
688
|
+
const _wiped = chars.join("");
|
|
689
|
+
void _wiped;
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function secureCompare(a, b) {
|
|
694
|
+
if (a === b) return true;
|
|
695
|
+
const bufA = typeof a === "string" ? Buffer.from(a, "utf-8") : a;
|
|
696
|
+
const bufB = typeof b === "string" ? Buffer.from(b, "utf-8") : b;
|
|
697
|
+
if (bufA.length !== bufB.length) {
|
|
698
|
+
const maxLen = Math.max(bufA.length, bufB.length);
|
|
699
|
+
const padA = Buffer.alloc(maxLen, 0);
|
|
700
|
+
const padB = Buffer.alloc(maxLen, 0);
|
|
701
|
+
bufA.copy(padA, maxLen - bufA.length);
|
|
702
|
+
bufB.copy(padB, maxLen - bufB.length);
|
|
703
|
+
try {
|
|
704
|
+
cryptoTimingSafeEqual(padA, padB);
|
|
705
|
+
return false;
|
|
706
|
+
} catch {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
return cryptoTimingSafeEqual(bufA, bufB);
|
|
712
|
+
} catch {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/sync/watcher.ts
|
|
718
|
+
import { watch as watch2 } from "fs";
|
|
719
|
+
import { existsSync as existsSync2 } from "fs";
|
|
720
|
+
var SyncWatcher = class {
|
|
721
|
+
nativeWatcher = null;
|
|
722
|
+
debounceTimer = null;
|
|
723
|
+
callbacks = /* @__PURE__ */ new Map();
|
|
724
|
+
_active = false;
|
|
725
|
+
options;
|
|
726
|
+
constructor(options) {
|
|
727
|
+
this.options = options;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Start watching the .env file for changes.
|
|
731
|
+
*/
|
|
732
|
+
start() {
|
|
733
|
+
if (this._active) return;
|
|
734
|
+
const envPath = this.options.envPath;
|
|
735
|
+
if (!existsSync2(envPath)) {
|
|
736
|
+
this.emitError(new Error(`File not found: "${envPath}"`));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
this._active = true;
|
|
740
|
+
try {
|
|
741
|
+
this.nativeWatcher = watch2(envPath, { persistent: true }, (_eventType, _filename) => {
|
|
742
|
+
this.debouncedSync();
|
|
743
|
+
});
|
|
744
|
+
this.nativeWatcher.on("error", (error) => {
|
|
745
|
+
this.emitError(error);
|
|
746
|
+
this.nativeWatcher = null;
|
|
747
|
+
setTimeout(() => {
|
|
748
|
+
if (this._active && existsSync2(envPath)) {
|
|
749
|
+
this.start();
|
|
750
|
+
}
|
|
751
|
+
}, 1e3);
|
|
752
|
+
});
|
|
753
|
+
this.emitEvent({
|
|
754
|
+
type: "change",
|
|
755
|
+
path: envPath,
|
|
756
|
+
timestamp: Date.now()
|
|
757
|
+
});
|
|
758
|
+
} catch (error) {
|
|
759
|
+
this._active = false;
|
|
760
|
+
this.emitError(error instanceof Error ? error : new Error(String(error)));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Stop watching and release all resources.
|
|
765
|
+
*/
|
|
766
|
+
stop() {
|
|
767
|
+
if (!this._active) return;
|
|
768
|
+
this._active = false;
|
|
769
|
+
if (this.nativeWatcher !== null) {
|
|
770
|
+
try {
|
|
771
|
+
this.nativeWatcher.close();
|
|
772
|
+
} catch {
|
|
773
|
+
}
|
|
774
|
+
this.nativeWatcher = null;
|
|
775
|
+
}
|
|
776
|
+
if (this.debounceTimer !== null) {
|
|
777
|
+
clearTimeout(this.debounceTimer);
|
|
778
|
+
this.debounceTimer = null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Whether the watcher is currently active.
|
|
783
|
+
*/
|
|
784
|
+
get active() {
|
|
785
|
+
return this._active;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Register a callback for a specific event type.
|
|
789
|
+
*/
|
|
790
|
+
on(event, callback) {
|
|
791
|
+
if (!this.callbacks.has(event)) {
|
|
792
|
+
this.callbacks.set(event, /* @__PURE__ */ new Set());
|
|
793
|
+
}
|
|
794
|
+
this.callbacks.get(event).add(callback);
|
|
795
|
+
return this;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Remove a previously registered callback.
|
|
799
|
+
*/
|
|
800
|
+
off(event, callback) {
|
|
801
|
+
const cbs = this.callbacks.get(event);
|
|
802
|
+
if (cbs !== void 0) {
|
|
803
|
+
cbs.delete(callback);
|
|
804
|
+
}
|
|
805
|
+
return this;
|
|
806
|
+
}
|
|
807
|
+
// -------------------------------------------------------------------------
|
|
808
|
+
// Private Methods
|
|
809
|
+
// -------------------------------------------------------------------------
|
|
810
|
+
/**
|
|
811
|
+
* Debounce sync operations to avoid excessive file writes.
|
|
812
|
+
*/
|
|
813
|
+
debouncedSync() {
|
|
814
|
+
const debounceMs = this.options.debounceMs ?? 300;
|
|
815
|
+
if (this.debounceTimer !== null) {
|
|
816
|
+
clearTimeout(this.debounceTimer);
|
|
817
|
+
}
|
|
818
|
+
this.debounceTimer = setTimeout(() => {
|
|
819
|
+
this.debounceTimer = null;
|
|
820
|
+
void this.performSync();
|
|
821
|
+
}, debounceMs);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Perform the actual sync: regenerate .env.example if needed.
|
|
825
|
+
*/
|
|
826
|
+
async performSync() {
|
|
827
|
+
const result = {
|
|
828
|
+
success: false,
|
|
829
|
+
envPath: this.options.envPath,
|
|
830
|
+
examplePath: this.options.examplePath,
|
|
831
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
832
|
+
};
|
|
833
|
+
try {
|
|
834
|
+
const needs = await needsUpdate(
|
|
835
|
+
this.options.envPath,
|
|
836
|
+
this.options.examplePath,
|
|
837
|
+
{
|
|
838
|
+
schemaPath: void 0,
|
|
839
|
+
includeDescriptions: this.options.includeDescriptions,
|
|
840
|
+
includeTypes: this.options.includeTypes,
|
|
841
|
+
includeDefaults: this.options.includeDefaults
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
if (!needs) {
|
|
845
|
+
result.success = true;
|
|
846
|
+
if (this.options.onSync !== void 0) {
|
|
847
|
+
this.options.onSync(result);
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
await generateExampleFile(
|
|
852
|
+
this.options.envPath,
|
|
853
|
+
this.options.examplePath,
|
|
854
|
+
{
|
|
855
|
+
schemaPath: void 0,
|
|
856
|
+
includeDescriptions: this.options.includeDescriptions,
|
|
857
|
+
includeTypes: this.options.includeTypes,
|
|
858
|
+
includeDefaults: this.options.includeDefaults
|
|
859
|
+
}
|
|
860
|
+
);
|
|
861
|
+
result.success = true;
|
|
862
|
+
this.emitEvent({
|
|
863
|
+
type: "change",
|
|
864
|
+
path: this.options.examplePath,
|
|
865
|
+
timestamp: Date.now()
|
|
866
|
+
});
|
|
867
|
+
} catch (error) {
|
|
868
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
869
|
+
this.emitError(error instanceof Error ? error : new Error(String(error)));
|
|
870
|
+
}
|
|
871
|
+
if (this.options.onSync !== void 0) {
|
|
872
|
+
this.options.onSync(result);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Emit a change event to all registered change callbacks.
|
|
877
|
+
*/
|
|
878
|
+
emitEvent(event) {
|
|
879
|
+
const changeCallbacks = this.callbacks.get("change");
|
|
880
|
+
if (changeCallbacks !== void 0) {
|
|
881
|
+
for (const cb of changeCallbacks) {
|
|
882
|
+
try {
|
|
883
|
+
cb(event);
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const readyCallbacks = this.callbacks.get("ready");
|
|
889
|
+
if (readyCallbacks !== void 0 && event.type === "change") {
|
|
890
|
+
for (const cb of readyCallbacks) {
|
|
891
|
+
try {
|
|
892
|
+
cb(event);
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Emit an error event.
|
|
900
|
+
*/
|
|
901
|
+
emitError(_error) {
|
|
902
|
+
const errorCallbacks = this.callbacks.get("error");
|
|
903
|
+
if (errorCallbacks !== void 0) {
|
|
904
|
+
const event = {
|
|
905
|
+
type: "change",
|
|
906
|
+
path: this.options.envPath,
|
|
907
|
+
timestamp: Date.now()
|
|
908
|
+
};
|
|
909
|
+
for (const cb of errorCallbacks) {
|
|
910
|
+
try {
|
|
911
|
+
cb(event);
|
|
912
|
+
} catch {
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
function createSyncWatcher(options) {
|
|
919
|
+
return new SyncWatcher(options);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/typegen/watcher.ts
|
|
923
|
+
import { watch as watch3 } from "fs";
|
|
924
|
+
import { existsSync as existsSync3 } from "fs";
|
|
925
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
926
|
+
var TypegenWatcher = class {
|
|
927
|
+
nativeWatchers = /* @__PURE__ */ new Map();
|
|
928
|
+
debounceTimer = null;
|
|
929
|
+
callbacks = /* @__PURE__ */ new Map();
|
|
930
|
+
_active = false;
|
|
931
|
+
options;
|
|
932
|
+
constructor(options) {
|
|
933
|
+
this.options = options;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Start watching the configured files.
|
|
937
|
+
*/
|
|
938
|
+
start() {
|
|
939
|
+
if (this._active) return;
|
|
940
|
+
this._active = true;
|
|
941
|
+
if (this.options.schemaPath !== void 0) {
|
|
942
|
+
this.watchFile(this.options.schemaPath);
|
|
943
|
+
}
|
|
944
|
+
if (this.options.envPath !== void 0) {
|
|
945
|
+
this.watchFile(this.options.envPath);
|
|
946
|
+
}
|
|
947
|
+
const outputDir = dirname(resolve2(this.options.outputPath));
|
|
948
|
+
this.watchDirectory(outputDir);
|
|
949
|
+
this.emitChange("");
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Stop watching and release all resources.
|
|
953
|
+
*/
|
|
954
|
+
stop() {
|
|
955
|
+
if (!this._active) return;
|
|
956
|
+
this._active = false;
|
|
957
|
+
for (const [, nativeWatcher] of this.nativeWatchers) {
|
|
958
|
+
try {
|
|
959
|
+
nativeWatcher.close();
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
this.nativeWatchers.clear();
|
|
964
|
+
if (this.debounceTimer !== null) {
|
|
965
|
+
clearTimeout(this.debounceTimer);
|
|
966
|
+
this.debounceTimer = null;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Whether the watcher is currently active.
|
|
971
|
+
*/
|
|
972
|
+
get active() {
|
|
973
|
+
return this._active;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Register a callback for a specific event type.
|
|
977
|
+
*/
|
|
978
|
+
on(event, callback) {
|
|
979
|
+
if (!this.callbacks.has(event)) {
|
|
980
|
+
this.callbacks.set(event, /* @__PURE__ */ new Set());
|
|
981
|
+
}
|
|
982
|
+
this.callbacks.get(event).add(callback);
|
|
983
|
+
return this;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Remove a previously registered callback.
|
|
987
|
+
*/
|
|
988
|
+
off(event, callback) {
|
|
989
|
+
const cbs = this.callbacks.get(event);
|
|
990
|
+
if (cbs !== void 0) {
|
|
991
|
+
cbs.delete(callback);
|
|
992
|
+
}
|
|
993
|
+
return this;
|
|
994
|
+
}
|
|
995
|
+
// -------------------------------------------------------------------------
|
|
996
|
+
// Private Methods
|
|
997
|
+
// -------------------------------------------------------------------------
|
|
998
|
+
/**
|
|
999
|
+
* Watch a specific file for changes.
|
|
1000
|
+
*/
|
|
1001
|
+
watchFile(filePath) {
|
|
1002
|
+
if (!existsSync3(filePath)) return;
|
|
1003
|
+
try {
|
|
1004
|
+
const nativeWatcher = watch3(filePath, { persistent: true }, () => {
|
|
1005
|
+
this.debouncedGenerate();
|
|
1006
|
+
});
|
|
1007
|
+
nativeWatcher.on("error", () => {
|
|
1008
|
+
this.nativeWatchers.delete(filePath);
|
|
1009
|
+
setTimeout(() => {
|
|
1010
|
+
if (this._active && existsSync3(filePath)) {
|
|
1011
|
+
this.watchFile(filePath);
|
|
1012
|
+
}
|
|
1013
|
+
}, 1e3);
|
|
1014
|
+
});
|
|
1015
|
+
this.nativeWatchers.set(filePath, nativeWatcher);
|
|
1016
|
+
} catch {
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Watch a directory for changes.
|
|
1021
|
+
*/
|
|
1022
|
+
watchDirectory(dirPath) {
|
|
1023
|
+
const dirKey = `__dir__:${dirPath}`;
|
|
1024
|
+
if (this.nativeWatchers.has(dirKey)) return;
|
|
1025
|
+
try {
|
|
1026
|
+
const nativeWatcher = watch3(dirPath, { persistent: true }, () => {
|
|
1027
|
+
this.debouncedGenerate();
|
|
1028
|
+
});
|
|
1029
|
+
nativeWatcher.on("error", () => {
|
|
1030
|
+
this.nativeWatchers.delete(dirKey);
|
|
1031
|
+
setTimeout(() => {
|
|
1032
|
+
if (this._active) {
|
|
1033
|
+
this.watchDirectory(dirPath);
|
|
1034
|
+
}
|
|
1035
|
+
}, 5e3);
|
|
1036
|
+
});
|
|
1037
|
+
this.nativeWatchers.set(dirKey, nativeWatcher);
|
|
1038
|
+
} catch {
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Debounce generation to avoid excessive rewrites.
|
|
1043
|
+
*/
|
|
1044
|
+
debouncedGenerate() {
|
|
1045
|
+
const debounceMs = this.options.debounceMs ?? 300;
|
|
1046
|
+
if (this.debounceTimer !== null) {
|
|
1047
|
+
clearTimeout(this.debounceTimer);
|
|
1048
|
+
}
|
|
1049
|
+
this.debounceTimer = setTimeout(() => {
|
|
1050
|
+
this.debounceTimer = null;
|
|
1051
|
+
void this.performGenerate();
|
|
1052
|
+
}, debounceMs);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Perform the actual type generation.
|
|
1056
|
+
*/
|
|
1057
|
+
async performGenerate() {
|
|
1058
|
+
const format = this.options.format ?? "declaration";
|
|
1059
|
+
const result = {
|
|
1060
|
+
success: false,
|
|
1061
|
+
format,
|
|
1062
|
+
outputPaths: [],
|
|
1063
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1064
|
+
};
|
|
1065
|
+
try {
|
|
1066
|
+
let vars = {};
|
|
1067
|
+
if (this.options.envPath !== void 0 && existsSync3(this.options.envPath)) {
|
|
1068
|
+
const envContent = await readFile(this.options.envPath);
|
|
1069
|
+
const parsed = parseEnvFile(envContent, this.options.envPath);
|
|
1070
|
+
for (const envVar of parsed.vars) {
|
|
1071
|
+
vars[envVar.key] = envVar.value;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const outputPath = this.options.outputPath;
|
|
1075
|
+
const outputPaths = [];
|
|
1076
|
+
if (format === "declaration" || format === "all") {
|
|
1077
|
+
const declPath = format === "all" ? outputPath.replace(/\.\w+$/, ".d.ts") || "ultraenv.d.ts" : outputPath;
|
|
1078
|
+
await generateDeclaration(vars, this.options.schema, declPath);
|
|
1079
|
+
outputPaths.push(declPath);
|
|
1080
|
+
}
|
|
1081
|
+
if (format === "module" || format === "all") {
|
|
1082
|
+
const modulePath = format === "all" ? outputPath.replace(/\.\w+$/, ".env.ts") || "ultraenv.env.ts" : outputPath;
|
|
1083
|
+
if (this.options.schema !== void 0) {
|
|
1084
|
+
await generateModule(this.options.schema, modulePath);
|
|
1085
|
+
}
|
|
1086
|
+
outputPaths.push(modulePath);
|
|
1087
|
+
}
|
|
1088
|
+
if (format === "json-schema" || format === "all") {
|
|
1089
|
+
const jsonPath = format === "all" ? outputPath.replace(/\.\w+$/, ".schema.json") || "ultraenv.schema.json" : outputPath;
|
|
1090
|
+
if (this.options.schema !== void 0) {
|
|
1091
|
+
await generateJsonSchema(this.options.schema, jsonPath);
|
|
1092
|
+
}
|
|
1093
|
+
outputPaths.push(jsonPath);
|
|
1094
|
+
}
|
|
1095
|
+
result.success = true;
|
|
1096
|
+
result.outputPaths = outputPaths;
|
|
1097
|
+
this.emitChange(outputPaths[0] ?? outputPath);
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
1100
|
+
this.emitError(error instanceof Error ? error : new Error(String(error)));
|
|
1101
|
+
}
|
|
1102
|
+
if (this.options.onGenerate !== void 0) {
|
|
1103
|
+
this.options.onGenerate(result);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Emit a change event to all registered callbacks.
|
|
1108
|
+
*/
|
|
1109
|
+
emitChange(filePath) {
|
|
1110
|
+
const event = {
|
|
1111
|
+
type: "change",
|
|
1112
|
+
path: filePath,
|
|
1113
|
+
timestamp: Date.now()
|
|
1114
|
+
};
|
|
1115
|
+
const changeCallbacks = this.callbacks.get("change");
|
|
1116
|
+
if (changeCallbacks !== void 0) {
|
|
1117
|
+
for (const cb of changeCallbacks) {
|
|
1118
|
+
try {
|
|
1119
|
+
cb(event);
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const readyCallbacks = this.callbacks.get("ready");
|
|
1125
|
+
if (readyCallbacks !== void 0) {
|
|
1126
|
+
for (const cb of readyCallbacks) {
|
|
1127
|
+
try {
|
|
1128
|
+
cb(event);
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Emit an error event.
|
|
1136
|
+
*/
|
|
1137
|
+
emitError(_error) {
|
|
1138
|
+
const event = {
|
|
1139
|
+
type: "change",
|
|
1140
|
+
path: "",
|
|
1141
|
+
timestamp: Date.now()
|
|
1142
|
+
};
|
|
1143
|
+
const errorCallbacks = this.callbacks.get("error");
|
|
1144
|
+
if (errorCallbacks !== void 0) {
|
|
1145
|
+
for (const cb of errorCallbacks) {
|
|
1146
|
+
try {
|
|
1147
|
+
cb(event);
|
|
1148
|
+
} catch {
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
function createTypegenWatcher(options) {
|
|
1155
|
+
return new TypegenWatcher(options);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/presets/nextjs.ts
|
|
1159
|
+
var nextjsSchema = {
|
|
1160
|
+
// ── Public (client-exposed) Variables ──────────────────────────────
|
|
1161
|
+
NEXT_PUBLIC_API_URL: {
|
|
1162
|
+
type: "string",
|
|
1163
|
+
format: "url",
|
|
1164
|
+
description: "Public API base URL exposed to the client bundle"
|
|
1165
|
+
},
|
|
1166
|
+
NEXT_PUBLIC_SITE_URL: {
|
|
1167
|
+
type: "string",
|
|
1168
|
+
format: "url",
|
|
1169
|
+
description: "Canonical site URL (used for SEO, OG tags, etc.)"
|
|
1170
|
+
},
|
|
1171
|
+
NEXT_PUBLIC_VERCEL_URL: {
|
|
1172
|
+
type: "string",
|
|
1173
|
+
format: "url",
|
|
1174
|
+
optional: true,
|
|
1175
|
+
description: "Vercel deployment URL (auto-set by Vercel)"
|
|
1176
|
+
},
|
|
1177
|
+
NEXT_PUBLIC_GA_ID: {
|
|
1178
|
+
type: "string",
|
|
1179
|
+
optional: true,
|
|
1180
|
+
description: "Google Analytics measurement ID (e.g., G-XXXXXXXXXX)"
|
|
1181
|
+
},
|
|
1182
|
+
NEXT_PUBLIC_SENTRY_DSN: {
|
|
1183
|
+
type: "string",
|
|
1184
|
+
optional: true,
|
|
1185
|
+
format: "url",
|
|
1186
|
+
description: "Sentry DSN for client-side error reporting"
|
|
1187
|
+
},
|
|
1188
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: {
|
|
1189
|
+
type: "string",
|
|
1190
|
+
optional: true,
|
|
1191
|
+
description: "Stripe publishable key for client-side usage"
|
|
1192
|
+
},
|
|
1193
|
+
// ── Server-Only Variables ─────────────────────────────────────────
|
|
1194
|
+
DATABASE_URL: {
|
|
1195
|
+
type: "string",
|
|
1196
|
+
format: "url",
|
|
1197
|
+
description: "Primary database connection URL (server-only)"
|
|
1198
|
+
},
|
|
1199
|
+
DATABASE_URL_UNPOOLED: {
|
|
1200
|
+
type: "string",
|
|
1201
|
+
optional: true,
|
|
1202
|
+
format: "url",
|
|
1203
|
+
description: "Unpooled database connection for migrations (server-only)"
|
|
1204
|
+
},
|
|
1205
|
+
DIRECT_URL: {
|
|
1206
|
+
type: "string",
|
|
1207
|
+
optional: true,
|
|
1208
|
+
description: "Direct database URL for Prisma migrations"
|
|
1209
|
+
},
|
|
1210
|
+
NODE_ENV: {
|
|
1211
|
+
type: "enum",
|
|
1212
|
+
values: ["development", "production", "test"],
|
|
1213
|
+
description: "Node.js environment mode",
|
|
1214
|
+
default: "development"
|
|
1215
|
+
},
|
|
1216
|
+
NEXT_PUBLIC_NODE_ENV: {
|
|
1217
|
+
type: "enum",
|
|
1218
|
+
values: ["development", "production", "test"],
|
|
1219
|
+
optional: true,
|
|
1220
|
+
description: "Node.js environment mode exposed to client (usually set by Next.js)"
|
|
1221
|
+
},
|
|
1222
|
+
NEXT_TELEMETRY_DISABLED: {
|
|
1223
|
+
type: "boolean",
|
|
1224
|
+
optional: true,
|
|
1225
|
+
description: "Disable Next.js telemetry collection"
|
|
1226
|
+
},
|
|
1227
|
+
NEXT_PUBLIC_APP_ENV: {
|
|
1228
|
+
type: "string",
|
|
1229
|
+
optional: true,
|
|
1230
|
+
description: "Application environment name exposed to the client"
|
|
1231
|
+
},
|
|
1232
|
+
// ── Authentication ────────────────────────────────────────────────
|
|
1233
|
+
NEXTAUTH_URL: {
|
|
1234
|
+
type: "string",
|
|
1235
|
+
format: "url",
|
|
1236
|
+
optional: true,
|
|
1237
|
+
description: "NextAuth.js callback URL (server-only)"
|
|
1238
|
+
},
|
|
1239
|
+
NEXTAUTH_SECRET: {
|
|
1240
|
+
type: "string",
|
|
1241
|
+
optional: true,
|
|
1242
|
+
description: "NextAuth.js secret for signing tokens (server-only)"
|
|
1243
|
+
},
|
|
1244
|
+
NEXTAUTH_CLIENT_ID: {
|
|
1245
|
+
type: "string",
|
|
1246
|
+
optional: true,
|
|
1247
|
+
description: "OAuth client ID for NextAuth.js (server-only)"
|
|
1248
|
+
},
|
|
1249
|
+
NEXTAUTH_CLIENT_SECRET: {
|
|
1250
|
+
type: "string",
|
|
1251
|
+
optional: true,
|
|
1252
|
+
description: "OAuth client secret for NextAuth.js (server-only)"
|
|
1253
|
+
},
|
|
1254
|
+
// ── Image Optimization ────────────────────────────────────────────
|
|
1255
|
+
NEXT_PUBLIC_IMAGE_DOMAINS: {
|
|
1256
|
+
type: "array",
|
|
1257
|
+
optional: true,
|
|
1258
|
+
separator: ",",
|
|
1259
|
+
trimItems: true,
|
|
1260
|
+
description: "Allowed image optimization domains (comma-separated)"
|
|
1261
|
+
},
|
|
1262
|
+
NEXT_PUBLIC_IMAGE_REMOTE_PATTERNS: {
|
|
1263
|
+
type: "json",
|
|
1264
|
+
optional: true,
|
|
1265
|
+
description: "Remote image patterns for next/image (JSON array)"
|
|
1266
|
+
},
|
|
1267
|
+
// ── Feature Flags ─────────────────────────────────────────────────
|
|
1268
|
+
NEXT_PUBLIC_ENABLE_ANALYTICS: {
|
|
1269
|
+
type: "boolean",
|
|
1270
|
+
optional: true,
|
|
1271
|
+
description: "Enable client-side analytics tracking"
|
|
1272
|
+
},
|
|
1273
|
+
NEXT_PUBLIC_ENABLE_MOCK_MODE: {
|
|
1274
|
+
type: "boolean",
|
|
1275
|
+
optional: true,
|
|
1276
|
+
description: "Enable mock API mode for development"
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
var NEXTJS_FILES = [
|
|
1280
|
+
".env" /* Env */,
|
|
1281
|
+
".env.local" /* EnvLocal */,
|
|
1282
|
+
".env.development" /* EnvDevelopment */,
|
|
1283
|
+
".env.development.local" /* EnvDevelopmentLocal */,
|
|
1284
|
+
".env.test" /* EnvTest */,
|
|
1285
|
+
".env.test.local" /* EnvTestLocal */,
|
|
1286
|
+
".env.production" /* EnvProduction */,
|
|
1287
|
+
".env.production.local" /* EnvProductionLocal */
|
|
1288
|
+
];
|
|
1289
|
+
var nextjsPreset = {
|
|
1290
|
+
id: "nextjs",
|
|
1291
|
+
name: "Next.js",
|
|
1292
|
+
description: "Configuration preset for Next.js applications with client/server variable separation",
|
|
1293
|
+
schema: nextjsSchema,
|
|
1294
|
+
files: NEXTJS_FILES,
|
|
1295
|
+
tags: ["framework", "react", "ssr", "fullstack"]
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
// src/presets/vite.ts
|
|
1299
|
+
var VITE_MODES = [
|
|
1300
|
+
"development",
|
|
1301
|
+
"production",
|
|
1302
|
+
"test"
|
|
1303
|
+
];
|
|
1304
|
+
var viteSchema = {
|
|
1305
|
+
// ── Public (client-exposed) Variables ──────────────────────────────
|
|
1306
|
+
VITE_API_URL: {
|
|
1307
|
+
type: "string",
|
|
1308
|
+
format: "url",
|
|
1309
|
+
description: "Public API base URL exposed via import.meta.env"
|
|
1310
|
+
},
|
|
1311
|
+
VITE_APP_TITLE: {
|
|
1312
|
+
type: "string",
|
|
1313
|
+
optional: true,
|
|
1314
|
+
description: "Application title for the HTML document"
|
|
1315
|
+
},
|
|
1316
|
+
VITE_APP_BASE_URL: {
|
|
1317
|
+
type: "string",
|
|
1318
|
+
optional: true,
|
|
1319
|
+
description: "Base URL for the application (used in router)"
|
|
1320
|
+
},
|
|
1321
|
+
VITE_ENABLE_DEVTOOLS: {
|
|
1322
|
+
type: "boolean",
|
|
1323
|
+
optional: true,
|
|
1324
|
+
description: "Enable Vue/React devtools in development"
|
|
1325
|
+
},
|
|
1326
|
+
VITE_SENTRY_DSN: {
|
|
1327
|
+
type: "string",
|
|
1328
|
+
optional: true,
|
|
1329
|
+
format: "url",
|
|
1330
|
+
description: "Sentry DSN for client-side error reporting"
|
|
1331
|
+
},
|
|
1332
|
+
VITE_GA_ID: {
|
|
1333
|
+
type: "string",
|
|
1334
|
+
optional: true,
|
|
1335
|
+
description: "Google Analytics measurement ID"
|
|
1336
|
+
},
|
|
1337
|
+
// ── Mode-Based Variables ──────────────────────────────────────────
|
|
1338
|
+
MODE: {
|
|
1339
|
+
type: "enum",
|
|
1340
|
+
values: VITE_MODES,
|
|
1341
|
+
description: "Vite mode (development | production | test)",
|
|
1342
|
+
default: "development"
|
|
1343
|
+
},
|
|
1344
|
+
VITE_MODE: {
|
|
1345
|
+
type: "enum",
|
|
1346
|
+
values: VITE_MODES,
|
|
1347
|
+
optional: true,
|
|
1348
|
+
description: "Vite mode exposed to client code via import.meta.env.MODE"
|
|
1349
|
+
},
|
|
1350
|
+
DEV: {
|
|
1351
|
+
type: "boolean",
|
|
1352
|
+
optional: true,
|
|
1353
|
+
description: "Whether the app is running in dev mode (set by Vite)"
|
|
1354
|
+
},
|
|
1355
|
+
PROD: {
|
|
1356
|
+
type: "boolean",
|
|
1357
|
+
optional: true,
|
|
1358
|
+
description: "Whether the app is running in production mode (set by Vite)"
|
|
1359
|
+
},
|
|
1360
|
+
SSR: {
|
|
1361
|
+
type: "boolean",
|
|
1362
|
+
optional: true,
|
|
1363
|
+
description: "Whether the app is running in SSR mode (set by Vite SSR plugin)"
|
|
1364
|
+
},
|
|
1365
|
+
// ── Server-Only Variables ─────────────────────────────────────────
|
|
1366
|
+
NODE_ENV: {
|
|
1367
|
+
type: "enum",
|
|
1368
|
+
values: VITE_MODES,
|
|
1369
|
+
description: "Node.js environment mode",
|
|
1370
|
+
default: "development"
|
|
1371
|
+
},
|
|
1372
|
+
PORT: {
|
|
1373
|
+
type: "number",
|
|
1374
|
+
optional: true,
|
|
1375
|
+
positive: true,
|
|
1376
|
+
description: "Dev server port (server-only)",
|
|
1377
|
+
default: 5173
|
|
1378
|
+
},
|
|
1379
|
+
HOST: {
|
|
1380
|
+
type: "string",
|
|
1381
|
+
optional: true,
|
|
1382
|
+
description: "Dev server hostname (server-only)"
|
|
1383
|
+
},
|
|
1384
|
+
DATABASE_URL: {
|
|
1385
|
+
type: "string",
|
|
1386
|
+
optional: true,
|
|
1387
|
+
format: "url",
|
|
1388
|
+
description: "Database connection URL (server-only)"
|
|
1389
|
+
},
|
|
1390
|
+
API_SECRET: {
|
|
1391
|
+
type: "string",
|
|
1392
|
+
optional: true,
|
|
1393
|
+
description: "API secret key for backend authentication (server-only)"
|
|
1394
|
+
},
|
|
1395
|
+
JWT_SECRET: {
|
|
1396
|
+
type: "string",
|
|
1397
|
+
optional: true,
|
|
1398
|
+
description: "JWT signing secret (server-only)"
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
var VITE_FILES = [
|
|
1402
|
+
".env" /* Env */,
|
|
1403
|
+
".env.local" /* EnvLocal */,
|
|
1404
|
+
".env.development" /* EnvDevelopment */,
|
|
1405
|
+
".env.development.local" /* EnvDevelopmentLocal */,
|
|
1406
|
+
".env.test" /* EnvTest */,
|
|
1407
|
+
".env.test.local" /* EnvTestLocal */,
|
|
1408
|
+
".env.production" /* EnvProduction */,
|
|
1409
|
+
".env.production.local" /* EnvProductionLocal */
|
|
1410
|
+
];
|
|
1411
|
+
var vitePreset = {
|
|
1412
|
+
id: "vite",
|
|
1413
|
+
name: "Vite",
|
|
1414
|
+
description: "Configuration preset for Vite-powered applications with mode-based env loading",
|
|
1415
|
+
schema: viteSchema,
|
|
1416
|
+
files: VITE_FILES,
|
|
1417
|
+
tags: ["build-tool", "frontend", "spa"]
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
// src/presets/nuxt.ts
|
|
1421
|
+
var nuxtSchema = {
|
|
1422
|
+
// ── Nuxt Runtime Config (exposed to client with NUXT_PUBLIC_ prefix) ────
|
|
1423
|
+
NUXT_PUBLIC_API_BASE: {
|
|
1424
|
+
type: "string",
|
|
1425
|
+
optional: true,
|
|
1426
|
+
description: "Public API base URL (exposed to client via useRuntimeConfig)"
|
|
1427
|
+
},
|
|
1428
|
+
NUXT_PUBLIC_SITE_URL: {
|
|
1429
|
+
type: "string",
|
|
1430
|
+
format: "url",
|
|
1431
|
+
optional: true,
|
|
1432
|
+
description: "Canonical site URL (exposed to client)"
|
|
1433
|
+
},
|
|
1434
|
+
NUXT_PUBLIC_APP_NAME: {
|
|
1435
|
+
type: "string",
|
|
1436
|
+
optional: true,
|
|
1437
|
+
description: "Application name (exposed to client)"
|
|
1438
|
+
},
|
|
1439
|
+
NUXT_PUBLIC_GA_ID: {
|
|
1440
|
+
type: "string",
|
|
1441
|
+
optional: true,
|
|
1442
|
+
description: "Google Analytics measurement ID (exposed to client)"
|
|
1443
|
+
},
|
|
1444
|
+
NUXT_PUBLIC_SENTRY_DSN: {
|
|
1445
|
+
type: "string",
|
|
1446
|
+
optional: true,
|
|
1447
|
+
format: "url",
|
|
1448
|
+
description: "Sentry DSN for client-side error reporting"
|
|
1449
|
+
},
|
|
1450
|
+
// ── Nuxt Private Runtime Config (server-only) ─────────────────────
|
|
1451
|
+
NUXT_API_SECRET: {
|
|
1452
|
+
type: "string",
|
|
1453
|
+
optional: true,
|
|
1454
|
+
description: "API secret key (server-only runtime config)"
|
|
1455
|
+
},
|
|
1456
|
+
NUXT_SESSION_SECRET: {
|
|
1457
|
+
type: "string",
|
|
1458
|
+
optional: true,
|
|
1459
|
+
description: "Session encryption secret (server-only)"
|
|
1460
|
+
},
|
|
1461
|
+
NUXT_OAUTH_GITHUB_CLIENT_ID: {
|
|
1462
|
+
type: "string",
|
|
1463
|
+
optional: true,
|
|
1464
|
+
description: "GitHub OAuth client ID (server-only)"
|
|
1465
|
+
},
|
|
1466
|
+
NUXT_OAUTH_GITHUB_CLIENT_SECRET: {
|
|
1467
|
+
type: "string",
|
|
1468
|
+
optional: true,
|
|
1469
|
+
description: "GitHub OAuth client secret (server-only)"
|
|
1470
|
+
},
|
|
1471
|
+
NUXT_DATABASE_URL: {
|
|
1472
|
+
type: "string",
|
|
1473
|
+
optional: true,
|
|
1474
|
+
format: "url",
|
|
1475
|
+
description: "Database URL (server-only runtime config)"
|
|
1476
|
+
},
|
|
1477
|
+
// ── Nitro Server Variables ────────────────────────────────────────
|
|
1478
|
+
NITRO_PORT: {
|
|
1479
|
+
type: "number",
|
|
1480
|
+
optional: true,
|
|
1481
|
+
positive: true,
|
|
1482
|
+
description: "Nitro server port",
|
|
1483
|
+
default: 3e3
|
|
1484
|
+
},
|
|
1485
|
+
NITRO_HOST: {
|
|
1486
|
+
type: "string",
|
|
1487
|
+
optional: true,
|
|
1488
|
+
description: "Nitro server hostname"
|
|
1489
|
+
},
|
|
1490
|
+
NITRO_PRESET: {
|
|
1491
|
+
type: "string",
|
|
1492
|
+
optional: true,
|
|
1493
|
+
description: "Nitro deployment preset (e.g., node, vercel, netlify)"
|
|
1494
|
+
},
|
|
1495
|
+
NITRO_BODY_SIZE_LIMIT: {
|
|
1496
|
+
type: "string",
|
|
1497
|
+
optional: true,
|
|
1498
|
+
description: 'Nitro request body size limit (e.g., "16mb")'
|
|
1499
|
+
},
|
|
1500
|
+
// ── Core Variables ────────────────────────────────────────────────
|
|
1501
|
+
NODE_ENV: {
|
|
1502
|
+
type: "enum",
|
|
1503
|
+
values: ["development", "production", "test"],
|
|
1504
|
+
description: "Node.js environment mode",
|
|
1505
|
+
default: "development"
|
|
1506
|
+
},
|
|
1507
|
+
NUXT_APP_ENV: {
|
|
1508
|
+
type: "string",
|
|
1509
|
+
optional: true,
|
|
1510
|
+
description: "Application environment identifier"
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
var NUXT_FILES = [
|
|
1514
|
+
".env" /* Env */,
|
|
1515
|
+
".env.local" /* EnvLocal */,
|
|
1516
|
+
".env.development" /* EnvDevelopment */,
|
|
1517
|
+
".env.development.local" /* EnvDevelopmentLocal */,
|
|
1518
|
+
".env.test" /* EnvTest */,
|
|
1519
|
+
".env.test.local" /* EnvTestLocal */,
|
|
1520
|
+
".env.production" /* EnvProduction */,
|
|
1521
|
+
".env.production.local" /* EnvProductionLocal */,
|
|
1522
|
+
".env.staging" /* EnvStaging */,
|
|
1523
|
+
".env.staging.local" /* EnvStagingLocal */
|
|
1524
|
+
];
|
|
1525
|
+
var nuxtPreset = {
|
|
1526
|
+
id: "nuxt",
|
|
1527
|
+
name: "Nuxt",
|
|
1528
|
+
description: "Configuration preset for Nuxt 3 with NUXT/NITRO runtime config separation",
|
|
1529
|
+
schema: nuxtSchema,
|
|
1530
|
+
files: NUXT_FILES,
|
|
1531
|
+
tags: ["framework", "vue", "ssr", "fullstack"]
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
// src/presets/remix.ts
|
|
1535
|
+
var remixSchema = {
|
|
1536
|
+
// ── Core Variables ────────────────────────────────────────────────
|
|
1537
|
+
NODE_ENV: {
|
|
1538
|
+
type: "enum",
|
|
1539
|
+
values: ["development", "production", "test"],
|
|
1540
|
+
description: "Node.js environment mode",
|
|
1541
|
+
default: "development"
|
|
1542
|
+
},
|
|
1543
|
+
PORT: {
|
|
1544
|
+
type: "number",
|
|
1545
|
+
optional: true,
|
|
1546
|
+
positive: true,
|
|
1547
|
+
description: "Remix dev server port",
|
|
1548
|
+
default: 3e3
|
|
1549
|
+
},
|
|
1550
|
+
HOST: {
|
|
1551
|
+
type: "string",
|
|
1552
|
+
optional: true,
|
|
1553
|
+
description: "Remix dev server hostname"
|
|
1554
|
+
},
|
|
1555
|
+
// ── Session Management ────────────────────────────────────────────
|
|
1556
|
+
SESSION_SECRET: {
|
|
1557
|
+
type: "string",
|
|
1558
|
+
description: "Secret for signing Remix sessions (required)"
|
|
1559
|
+
},
|
|
1560
|
+
SESSION_MAX_AGE: {
|
|
1561
|
+
type: "number",
|
|
1562
|
+
optional: true,
|
|
1563
|
+
positive: true,
|
|
1564
|
+
description: "Session maximum age in seconds"
|
|
1565
|
+
},
|
|
1566
|
+
// ── Database ──────────────────────────────────────────────────────
|
|
1567
|
+
DATABASE_URL: {
|
|
1568
|
+
type: "string",
|
|
1569
|
+
optional: true,
|
|
1570
|
+
format: "url",
|
|
1571
|
+
description: "Primary database connection URL (server-only)"
|
|
1572
|
+
},
|
|
1573
|
+
DATABASE_POOL_SIZE: {
|
|
1574
|
+
type: "number",
|
|
1575
|
+
optional: true,
|
|
1576
|
+
positive: true,
|
|
1577
|
+
description: "Database connection pool size"
|
|
1578
|
+
},
|
|
1579
|
+
// ── Authentication ────────────────────────────────────────────────
|
|
1580
|
+
AUTH_SECRET: {
|
|
1581
|
+
type: "string",
|
|
1582
|
+
optional: true,
|
|
1583
|
+
description: "Authentication secret for Remix Auth (server-only)"
|
|
1584
|
+
},
|
|
1585
|
+
AUTH_REDIRECT_URL: {
|
|
1586
|
+
type: "string",
|
|
1587
|
+
optional: true,
|
|
1588
|
+
format: "url",
|
|
1589
|
+
description: "Post-authentication redirect URL (server-only)"
|
|
1590
|
+
},
|
|
1591
|
+
// ── API & External Services ───────────────────────────────────────
|
|
1592
|
+
API_BASE_URL: {
|
|
1593
|
+
type: "string",
|
|
1594
|
+
optional: true,
|
|
1595
|
+
format: "url",
|
|
1596
|
+
description: "Backend API base URL (server-only)"
|
|
1597
|
+
},
|
|
1598
|
+
STRIPE_SECRET_KEY: {
|
|
1599
|
+
type: "string",
|
|
1600
|
+
optional: true,
|
|
1601
|
+
description: "Stripe secret key (server-only)"
|
|
1602
|
+
},
|
|
1603
|
+
STRIPE_WEBHOOK_SECRET: {
|
|
1604
|
+
type: "string",
|
|
1605
|
+
optional: true,
|
|
1606
|
+
description: "Stripe webhook signing secret (server-only)"
|
|
1607
|
+
},
|
|
1608
|
+
SENTRY_DSN: {
|
|
1609
|
+
type: "string",
|
|
1610
|
+
optional: true,
|
|
1611
|
+
format: "url",
|
|
1612
|
+
description: "Sentry DSN for server-side error reporting"
|
|
1613
|
+
},
|
|
1614
|
+
// ── Remix-Specific ───────────────────────────────────────────────
|
|
1615
|
+
REMIX_DEV_ORIGIN: {
|
|
1616
|
+
type: "string",
|
|
1617
|
+
optional: true,
|
|
1618
|
+
format: "url",
|
|
1619
|
+
description: "Dev server origin URL for HMR"
|
|
1620
|
+
},
|
|
1621
|
+
REMIX_PUBLIC_PATH: {
|
|
1622
|
+
type: "string",
|
|
1623
|
+
optional: true,
|
|
1624
|
+
description: "Public asset path prefix"
|
|
1625
|
+
},
|
|
1626
|
+
BASE_URL: {
|
|
1627
|
+
type: "string",
|
|
1628
|
+
format: "url",
|
|
1629
|
+
optional: true,
|
|
1630
|
+
description: "Application base URL"
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
var REMIX_FILES = [
|
|
1634
|
+
".env" /* Env */,
|
|
1635
|
+
".env.local" /* EnvLocal */,
|
|
1636
|
+
".env.development" /* EnvDevelopment */,
|
|
1637
|
+
".env.development.local" /* EnvDevelopmentLocal */,
|
|
1638
|
+
".env.test" /* EnvTest */,
|
|
1639
|
+
".env.test.local" /* EnvTestLocal */,
|
|
1640
|
+
".env.production" /* EnvProduction */,
|
|
1641
|
+
".env.production.local" /* EnvProductionLocal */
|
|
1642
|
+
];
|
|
1643
|
+
var remixPreset = {
|
|
1644
|
+
id: "remix",
|
|
1645
|
+
name: "Remix",
|
|
1646
|
+
description: "Configuration preset for Remix with server-side-only env variable enforcement",
|
|
1647
|
+
schema: remixSchema,
|
|
1648
|
+
files: REMIX_FILES,
|
|
1649
|
+
tags: ["framework", "react", "ssr", "fullstack"]
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
// src/presets/sveltekit.ts
|
|
1653
|
+
var sveltekitSchema = {
|
|
1654
|
+
// ── Public Variables (exposed to client) ──────────────────────────
|
|
1655
|
+
PUBLIC_API_URL: {
|
|
1656
|
+
type: "string",
|
|
1657
|
+
format: "url",
|
|
1658
|
+
description: "Public API base URL (exposed to client via import.meta.env)"
|
|
1659
|
+
},
|
|
1660
|
+
PUBLIC_SITE_URL: {
|
|
1661
|
+
type: "string",
|
|
1662
|
+
format: "url",
|
|
1663
|
+
description: "Canonical site URL (exposed to client)"
|
|
1664
|
+
},
|
|
1665
|
+
PUBLIC_APP_NAME: {
|
|
1666
|
+
type: "string",
|
|
1667
|
+
optional: true,
|
|
1668
|
+
description: "Application name (exposed to client)"
|
|
1669
|
+
},
|
|
1670
|
+
PUBLIC_GA_ID: {
|
|
1671
|
+
type: "string",
|
|
1672
|
+
optional: true,
|
|
1673
|
+
description: "Google Analytics measurement ID (exposed to client)"
|
|
1674
|
+
},
|
|
1675
|
+
PUBLIC_SENTRY_DSN: {
|
|
1676
|
+
type: "string",
|
|
1677
|
+
optional: true,
|
|
1678
|
+
format: "url",
|
|
1679
|
+
description: "Sentry DSN for client-side error reporting"
|
|
1680
|
+
},
|
|
1681
|
+
PUBLIC_STRIPE_PUBLISHABLE_KEY: {
|
|
1682
|
+
type: "string",
|
|
1683
|
+
optional: true,
|
|
1684
|
+
description: "Stripe publishable key (exposed to client)"
|
|
1685
|
+
},
|
|
1686
|
+
PUBLICturnstileSiteKey: {
|
|
1687
|
+
type: "string",
|
|
1688
|
+
optional: true,
|
|
1689
|
+
description: "Cloudflare Turnstile site key (exposed to client)"
|
|
1690
|
+
},
|
|
1691
|
+
// ── Server-Only Private Variables ─────────────────────────────────
|
|
1692
|
+
DATABASE_URL: {
|
|
1693
|
+
type: "string",
|
|
1694
|
+
optional: true,
|
|
1695
|
+
format: "url",
|
|
1696
|
+
description: "Database connection URL (server-only, $env/static/private)"
|
|
1697
|
+
},
|
|
1698
|
+
PRIVATE_API_SECRET: {
|
|
1699
|
+
type: "string",
|
|
1700
|
+
optional: true,
|
|
1701
|
+
description: "Private API secret (server-only)"
|
|
1702
|
+
},
|
|
1703
|
+
PRIVATE_SESSION_SECRET: {
|
|
1704
|
+
type: "string",
|
|
1705
|
+
optional: true,
|
|
1706
|
+
description: "Session encryption secret (server-only)"
|
|
1707
|
+
},
|
|
1708
|
+
PRIVATE_OAUTH_GITHUB_CLIENT_ID: {
|
|
1709
|
+
type: "string",
|
|
1710
|
+
optional: true,
|
|
1711
|
+
description: "GitHub OAuth client ID (server-only)"
|
|
1712
|
+
},
|
|
1713
|
+
PRIVATE_OAUTH_GITHUB_CLIENT_SECRET: {
|
|
1714
|
+
type: "string",
|
|
1715
|
+
optional: true,
|
|
1716
|
+
description: "GitHub OAuth client secret (server-only)"
|
|
1717
|
+
},
|
|
1718
|
+
PRIVATE_COOKIE_SECRET: {
|
|
1719
|
+
type: "string",
|
|
1720
|
+
optional: true,
|
|
1721
|
+
description: "Cookie signing secret (server-only)"
|
|
1722
|
+
},
|
|
1723
|
+
JWT_SECRET: {
|
|
1724
|
+
type: "string",
|
|
1725
|
+
optional: true,
|
|
1726
|
+
description: "JWT signing secret (server-only)"
|
|
1727
|
+
},
|
|
1728
|
+
STRIPE_SECRET_KEY: {
|
|
1729
|
+
type: "string",
|
|
1730
|
+
optional: true,
|
|
1731
|
+
description: "Stripe secret key (server-only)"
|
|
1732
|
+
},
|
|
1733
|
+
// ── Core Variables ────────────────────────────────────────────────
|
|
1734
|
+
ORIGIN: {
|
|
1735
|
+
type: "string",
|
|
1736
|
+
format: "url",
|
|
1737
|
+
optional: true,
|
|
1738
|
+
description: "Server origin URL (used by SvelteKit adapter)"
|
|
1739
|
+
},
|
|
1740
|
+
PORT: {
|
|
1741
|
+
type: "number",
|
|
1742
|
+
optional: true,
|
|
1743
|
+
positive: true,
|
|
1744
|
+
description: "Dev server port",
|
|
1745
|
+
default: 5173
|
|
1746
|
+
},
|
|
1747
|
+
HOST: {
|
|
1748
|
+
type: "string",
|
|
1749
|
+
optional: true,
|
|
1750
|
+
description: "Dev server hostname"
|
|
1751
|
+
},
|
|
1752
|
+
NODE_ENV: {
|
|
1753
|
+
type: "enum",
|
|
1754
|
+
values: ["development", "production", "test"],
|
|
1755
|
+
description: "Node.js environment mode",
|
|
1756
|
+
default: "development"
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
var SVELTEKIT_FILES = [
|
|
1760
|
+
".env" /* Env */,
|
|
1761
|
+
".env.local" /* EnvLocal */,
|
|
1762
|
+
".env.development" /* EnvDevelopment */,
|
|
1763
|
+
".env.development.local" /* EnvDevelopmentLocal */,
|
|
1764
|
+
".env.test" /* EnvTest */,
|
|
1765
|
+
".env.test.local" /* EnvTestLocal */,
|
|
1766
|
+
".env.production" /* EnvProduction */,
|
|
1767
|
+
".env.production.local" /* EnvProductionLocal */
|
|
1768
|
+
];
|
|
1769
|
+
var sveltekitPreset = {
|
|
1770
|
+
id: "sveltekit",
|
|
1771
|
+
name: "SvelteKit",
|
|
1772
|
+
description: "Configuration preset for SvelteKit with PUBLIC_ prefix convention",
|
|
1773
|
+
schema: sveltekitSchema,
|
|
1774
|
+
files: SVELTEKIT_FILES,
|
|
1775
|
+
tags: ["framework", "svelte", "ssr", "fullstack"]
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1778
|
+
// src/presets/express.ts
|
|
1779
|
+
var expressSchema = {
|
|
1780
|
+
// ── Server Configuration ──────────────────────────────────────────
|
|
1781
|
+
PORT: {
|
|
1782
|
+
type: "number",
|
|
1783
|
+
positive: true,
|
|
1784
|
+
description: "HTTP server listen port",
|
|
1785
|
+
default: 3e3
|
|
1786
|
+
},
|
|
1787
|
+
HOST: {
|
|
1788
|
+
type: "string",
|
|
1789
|
+
description: "HTTP server listen hostname",
|
|
1790
|
+
default: "0.0.0.0"
|
|
1791
|
+
},
|
|
1792
|
+
NODE_ENV: {
|
|
1793
|
+
type: "enum",
|
|
1794
|
+
values: ["development", "production", "test"],
|
|
1795
|
+
description: "Node.js environment mode",
|
|
1796
|
+
default: "development"
|
|
1797
|
+
},
|
|
1798
|
+
TRUST_PROXY: {
|
|
1799
|
+
type: "string",
|
|
1800
|
+
optional: true,
|
|
1801
|
+
description: "Trust proxy setting (ip, ipsubnet, hostname, loopback, boolean)"
|
|
1802
|
+
},
|
|
1803
|
+
// ── CORS ──────────────────────────────────────────────────────────
|
|
1804
|
+
CORS_ORIGIN: {
|
|
1805
|
+
type: "string",
|
|
1806
|
+
optional: true,
|
|
1807
|
+
description: 'CORS allowed origins (comma-separated or "*")'
|
|
1808
|
+
},
|
|
1809
|
+
CORS_CREDENTIALS: {
|
|
1810
|
+
type: "boolean",
|
|
1811
|
+
optional: true,
|
|
1812
|
+
description: "Enable CORS credentials support"
|
|
1813
|
+
},
|
|
1814
|
+
CORS_METHODS: {
|
|
1815
|
+
type: "array",
|
|
1816
|
+
optional: true,
|
|
1817
|
+
separator: ",",
|
|
1818
|
+
trimItems: true,
|
|
1819
|
+
description: "CORS allowed methods (comma-separated)"
|
|
1820
|
+
},
|
|
1821
|
+
CORS_HEADERS: {
|
|
1822
|
+
type: "array",
|
|
1823
|
+
optional: true,
|
|
1824
|
+
separator: ",",
|
|
1825
|
+
trimItems: true,
|
|
1826
|
+
description: "CORS allowed headers (comma-separated)"
|
|
1827
|
+
},
|
|
1828
|
+
// ── Rate Limiting ─────────────────────────────────────────────────
|
|
1829
|
+
RATE_LIMIT_WINDOW_MS: {
|
|
1830
|
+
type: "number",
|
|
1831
|
+
optional: true,
|
|
1832
|
+
positive: true,
|
|
1833
|
+
description: "Rate limit window in milliseconds"
|
|
1834
|
+
},
|
|
1835
|
+
RATE_LIMIT_MAX: {
|
|
1836
|
+
type: "number",
|
|
1837
|
+
optional: true,
|
|
1838
|
+
positive: true,
|
|
1839
|
+
description: "Maximum requests per rate limit window"
|
|
1840
|
+
},
|
|
1841
|
+
// ── Body Parsing ─────────────────────────────────────────────────
|
|
1842
|
+
BODY_SIZE_LIMIT: {
|
|
1843
|
+
type: "string",
|
|
1844
|
+
optional: true,
|
|
1845
|
+
description: 'Maximum request body size (e.g., "10mb")'
|
|
1846
|
+
},
|
|
1847
|
+
// ── Database ──────────────────────────────────────────────────────
|
|
1848
|
+
DATABASE_URL: {
|
|
1849
|
+
type: "string",
|
|
1850
|
+
optional: true,
|
|
1851
|
+
format: "url",
|
|
1852
|
+
description: "Primary database connection URL"
|
|
1853
|
+
},
|
|
1854
|
+
DATABASE_POOL_SIZE: {
|
|
1855
|
+
type: "number",
|
|
1856
|
+
optional: true,
|
|
1857
|
+
positive: true,
|
|
1858
|
+
description: "Database connection pool size"
|
|
1859
|
+
},
|
|
1860
|
+
// ── Authentication ────────────────────────────────────────────────
|
|
1861
|
+
JWT_SECRET: {
|
|
1862
|
+
type: "string",
|
|
1863
|
+
optional: true,
|
|
1864
|
+
description: "JWT signing secret"
|
|
1865
|
+
},
|
|
1866
|
+
JWT_EXPIRES_IN: {
|
|
1867
|
+
type: "string",
|
|
1868
|
+
optional: true,
|
|
1869
|
+
description: 'JWT token expiration (e.g., "7d", "24h")'
|
|
1870
|
+
},
|
|
1871
|
+
SESSION_SECRET: {
|
|
1872
|
+
type: "string",
|
|
1873
|
+
optional: true,
|
|
1874
|
+
description: "Express session secret"
|
|
1875
|
+
},
|
|
1876
|
+
COOKIE_SECRET: {
|
|
1877
|
+
type: "string",
|
|
1878
|
+
optional: true,
|
|
1879
|
+
description: "Cookie signing secret"
|
|
1880
|
+
},
|
|
1881
|
+
// ── Logging ───────────────────────────────────────────────────────
|
|
1882
|
+
LOG_LEVEL: {
|
|
1883
|
+
type: "enum",
|
|
1884
|
+
values: ["fatal", "error", "warn", "info", "debug", "trace", "silent"],
|
|
1885
|
+
optional: true,
|
|
1886
|
+
description: "Application log level",
|
|
1887
|
+
default: "info"
|
|
1888
|
+
},
|
|
1889
|
+
LOG_FORMAT: {
|
|
1890
|
+
type: "enum",
|
|
1891
|
+
values: ["json", "text", "combined", "dev"],
|
|
1892
|
+
optional: true,
|
|
1893
|
+
description: "Log output format",
|
|
1894
|
+
default: "dev"
|
|
1895
|
+
},
|
|
1896
|
+
// ── Security ──────────────────────────────────────────────────────
|
|
1897
|
+
HELMET_ENABLED: {
|
|
1898
|
+
type: "boolean",
|
|
1899
|
+
optional: true,
|
|
1900
|
+
description: "Enable Helmet security middleware",
|
|
1901
|
+
default: true
|
|
1902
|
+
},
|
|
1903
|
+
HSTS_MAX_AGE: {
|
|
1904
|
+
type: "number",
|
|
1905
|
+
optional: true,
|
|
1906
|
+
nonNegative: true,
|
|
1907
|
+
description: "HTTP Strict Transport Security max age in seconds"
|
|
1908
|
+
},
|
|
1909
|
+
CSP_DIRECTIVES: {
|
|
1910
|
+
type: "json",
|
|
1911
|
+
optional: true,
|
|
1912
|
+
description: "Content Security Policy directives (JSON object)"
|
|
1913
|
+
},
|
|
1914
|
+
// ── Compression ───────────────────────────────────────────────────
|
|
1915
|
+
COMPRESSION_ENABLED: {
|
|
1916
|
+
type: "boolean",
|
|
1917
|
+
optional: true,
|
|
1918
|
+
description: "Enable response compression",
|
|
1919
|
+
default: true
|
|
1920
|
+
},
|
|
1921
|
+
// ── Health Check ──────────────────────────────────────────────────
|
|
1922
|
+
HEALTH_CHECK_PATH: {
|
|
1923
|
+
type: "string",
|
|
1924
|
+
optional: true,
|
|
1925
|
+
description: "Health check endpoint path",
|
|
1926
|
+
default: "/health"
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
var EXPRESS_FILES = [
|
|
1930
|
+
".env" /* Env */,
|
|
1931
|
+
".env.local" /* EnvLocal */,
|
|
1932
|
+
".env.development" /* EnvDevelopment */,
|
|
1933
|
+
".env.development.local" /* EnvDevelopmentLocal */,
|
|
1934
|
+
".env.test" /* EnvTest */,
|
|
1935
|
+
".env.test.local" /* EnvTestLocal */,
|
|
1936
|
+
".env.production" /* EnvProduction */,
|
|
1937
|
+
".env.production.local" /* EnvProductionLocal */
|
|
1938
|
+
];
|
|
1939
|
+
var expressPreset = {
|
|
1940
|
+
id: "express",
|
|
1941
|
+
name: "Express",
|
|
1942
|
+
description: "Configuration preset for Express.js backend applications",
|
|
1943
|
+
schema: expressSchema,
|
|
1944
|
+
files: EXPRESS_FILES,
|
|
1945
|
+
tags: ["framework", "backend", "http", "node"]
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1948
|
+
// src/presets/fastify.ts
|
|
1949
|
+
var fastifySchema = {
|
|
1950
|
+
// ── Server Configuration ──────────────────────────────────────────
|
|
1951
|
+
PORT: {
|
|
1952
|
+
type: "number",
|
|
1953
|
+
positive: true,
|
|
1954
|
+
description: "Fastify server listen port",
|
|
1955
|
+
default: 3e3
|
|
1956
|
+
},
|
|
1957
|
+
HOST: {
|
|
1958
|
+
type: "string",
|
|
1959
|
+
description: "Fastify server listen hostname",
|
|
1960
|
+
default: "0.0.0.0"
|
|
1961
|
+
},
|
|
1962
|
+
FASTIFY_ADDRESS: {
|
|
1963
|
+
type: "string",
|
|
1964
|
+
optional: true,
|
|
1965
|
+
description: "Fastify listen address (alias for HOST)"
|
|
1966
|
+
},
|
|
1967
|
+
FASTIFY_PORT: {
|
|
1968
|
+
type: "number",
|
|
1969
|
+
optional: true,
|
|
1970
|
+
positive: true,
|
|
1971
|
+
description: "Fastify listen port (alias for PORT)"
|
|
1972
|
+
},
|
|
1973
|
+
NODE_ENV: {
|
|
1974
|
+
type: "enum",
|
|
1975
|
+
values: ["development", "production", "test"],
|
|
1976
|
+
description: "Node.js environment mode",
|
|
1977
|
+
default: "development"
|
|
1978
|
+
},
|
|
1979
|
+
PREFIX: {
|
|
1980
|
+
type: "string",
|
|
1981
|
+
optional: true,
|
|
1982
|
+
description: 'URL prefix for all routes (e.g., "/api/v1")'
|
|
1983
|
+
},
|
|
1984
|
+
// ── Fastify Core Options ──────────────────────────────────────────
|
|
1985
|
+
FASTIFY_BODY_LIMIT: {
|
|
1986
|
+
type: "number",
|
|
1987
|
+
optional: true,
|
|
1988
|
+
positive: true,
|
|
1989
|
+
description: "Maximum request body size in bytes"
|
|
1990
|
+
},
|
|
1991
|
+
FASTIFY_REQUEST_TIMEOUT: {
|
|
1992
|
+
type: "number",
|
|
1993
|
+
optional: true,
|
|
1994
|
+
positive: true,
|
|
1995
|
+
description: "Request timeout in milliseconds"
|
|
1996
|
+
},
|
|
1997
|
+
FASTIFY_KEEP_ALIVE_TIMEOUT: {
|
|
1998
|
+
type: "number",
|
|
1999
|
+
optional: true,
|
|
2000
|
+
positive: true,
|
|
2001
|
+
description: "Keep-alive timeout in milliseconds"
|
|
2002
|
+
},
|
|
2003
|
+
FASTIFY_MAX_HEADER_SIZE: {
|
|
2004
|
+
type: "number",
|
|
2005
|
+
optional: true,
|
|
2006
|
+
positive: true,
|
|
2007
|
+
description: "Maximum HTTP header size in bytes"
|
|
2008
|
+
},
|
|
2009
|
+
FASTIFY_HTTP2_ENABLED: {
|
|
2010
|
+
type: "boolean",
|
|
2011
|
+
optional: true,
|
|
2012
|
+
description: "Enable HTTP/2 support"
|
|
2013
|
+
},
|
|
2014
|
+
FASTIFY_HTTPS_ENABLED: {
|
|
2015
|
+
type: "boolean",
|
|
2016
|
+
optional: true,
|
|
2017
|
+
description: "Enable HTTPS (requires TLS cert/key)"
|
|
2018
|
+
},
|
|
2019
|
+
FASTIFY_HTTPS_CERT: {
|
|
2020
|
+
type: "string",
|
|
2021
|
+
optional: true,
|
|
2022
|
+
description: "Path to TLS certificate file"
|
|
2023
|
+
},
|
|
2024
|
+
FASTIFY_HTTPS_KEY: {
|
|
2025
|
+
type: "string",
|
|
2026
|
+
optional: true,
|
|
2027
|
+
description: "Path to TLS private key file"
|
|
2028
|
+
},
|
|
2029
|
+
// ── CORS ──────────────────────────────────────────────────────────
|
|
2030
|
+
CORS_ORIGIN: {
|
|
2031
|
+
type: "string",
|
|
2032
|
+
optional: true,
|
|
2033
|
+
description: "CORS allowed origins"
|
|
2034
|
+
},
|
|
2035
|
+
CORS_METHODS: {
|
|
2036
|
+
type: "array",
|
|
2037
|
+
optional: true,
|
|
2038
|
+
separator: ",",
|
|
2039
|
+
trimItems: true,
|
|
2040
|
+
description: "CORS allowed methods"
|
|
2041
|
+
},
|
|
2042
|
+
// ── Database ──────────────────────────────────────────────────────
|
|
2043
|
+
DATABASE_URL: {
|
|
2044
|
+
type: "string",
|
|
2045
|
+
optional: true,
|
|
2046
|
+
format: "url",
|
|
2047
|
+
description: "Primary database connection URL"
|
|
2048
|
+
},
|
|
2049
|
+
// ── Authentication ────────────────────────────────────────────────
|
|
2050
|
+
JWT_SECRET: {
|
|
2051
|
+
type: "string",
|
|
2052
|
+
optional: true,
|
|
2053
|
+
description: "JWT signing secret"
|
|
2054
|
+
},
|
|
2055
|
+
FASTIFY_JWT_SECRET: {
|
|
2056
|
+
type: "string",
|
|
2057
|
+
optional: true,
|
|
2058
|
+
description: "JWT secret (Fastify JWT plugin specific)"
|
|
2059
|
+
},
|
|
2060
|
+
// ── Logging (Fastify uses Pino) ──────────────────────────────────
|
|
2061
|
+
LOG_LEVEL: {
|
|
2062
|
+
type: "enum",
|
|
2063
|
+
values: ["fatal", "error", "warn", "info", "debug", "trace", "silent"],
|
|
2064
|
+
optional: true,
|
|
2065
|
+
description: "Pino log level",
|
|
2066
|
+
default: "info"
|
|
2067
|
+
},
|
|
2068
|
+
LOG_PRETTY_PRINT: {
|
|
2069
|
+
type: "boolean",
|
|
2070
|
+
optional: true,
|
|
2071
|
+
description: "Enable Pino pretty printing (development only)"
|
|
2072
|
+
},
|
|
2073
|
+
FASTIFY_LOGGER: {
|
|
2074
|
+
type: "boolean",
|
|
2075
|
+
optional: true,
|
|
2076
|
+
description: "Enable Fastify built-in Pino logger",
|
|
2077
|
+
default: true
|
|
2078
|
+
},
|
|
2079
|
+
// ── Rate Limiting ─────────────────────────────────────────────────
|
|
2080
|
+
RATE_LIMIT_MAX: {
|
|
2081
|
+
type: "number",
|
|
2082
|
+
optional: true,
|
|
2083
|
+
positive: true,
|
|
2084
|
+
description: "Max requests per time window"
|
|
2085
|
+
},
|
|
2086
|
+
RATE_LIMIT_TIME_WINDOW: {
|
|
2087
|
+
type: "string",
|
|
2088
|
+
optional: true,
|
|
2089
|
+
description: 'Rate limit time window (e.g., "1 minute")'
|
|
2090
|
+
},
|
|
2091
|
+
// ── Health Check ──────────────────────────────────────────────────
|
|
2092
|
+
HEALTH_CHECK_PATH: {
|
|
2093
|
+
type: "string",
|
|
2094
|
+
optional: true,
|
|
2095
|
+
description: "Health check endpoint path",
|
|
2096
|
+
default: "/health"
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
var FASTIFY_FILES = [
|
|
2100
|
+
".env" /* Env */,
|
|
2101
|
+
".env.local" /* EnvLocal */,
|
|
2102
|
+
".env.development" /* EnvDevelopment */,
|
|
2103
|
+
".env.development.local" /* EnvDevelopmentLocal */,
|
|
2104
|
+
".env.test" /* EnvTest */,
|
|
2105
|
+
".env.test.local" /* EnvTestLocal */,
|
|
2106
|
+
".env.production" /* EnvProduction */,
|
|
2107
|
+
".env.production.local" /* EnvProductionLocal */
|
|
2108
|
+
];
|
|
2109
|
+
var fastifyPreset = {
|
|
2110
|
+
id: "fastify",
|
|
2111
|
+
name: "Fastify",
|
|
2112
|
+
description: "Configuration preset for Fastify HTTP framework with schema-based configuration",
|
|
2113
|
+
schema: fastifySchema,
|
|
2114
|
+
files: FASTIFY_FILES,
|
|
2115
|
+
tags: ["framework", "backend", "http", "node", "performance"]
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
// src/presets/docker.ts
|
|
2119
|
+
var dockerSchema = {
|
|
2120
|
+
// ── Docker Engine ─────────────────────────────────────────────────
|
|
2121
|
+
DOCKER_HOST: {
|
|
2122
|
+
type: "string",
|
|
2123
|
+
optional: true,
|
|
2124
|
+
description: "Docker daemon socket URL"
|
|
2125
|
+
},
|
|
2126
|
+
DOCKER_TLS_VERIFY: {
|
|
2127
|
+
type: "boolean",
|
|
2128
|
+
optional: true,
|
|
2129
|
+
description: "Enable TLS verification for Docker daemon"
|
|
2130
|
+
},
|
|
2131
|
+
DOCKER_CERT_PATH: {
|
|
2132
|
+
type: "string",
|
|
2133
|
+
optional: true,
|
|
2134
|
+
description: "Path to Docker TLS certificates"
|
|
2135
|
+
},
|
|
2136
|
+
DOCKER_API_VERSION: {
|
|
2137
|
+
type: "string",
|
|
2138
|
+
optional: true,
|
|
2139
|
+
description: "Docker API version"
|
|
2140
|
+
},
|
|
2141
|
+
DOCKER_CONTEXT: {
|
|
2142
|
+
type: "string",
|
|
2143
|
+
optional: true,
|
|
2144
|
+
description: "Docker context name"
|
|
2145
|
+
},
|
|
2146
|
+
// ── Docker Compose ────────────────────────────────────────────────
|
|
2147
|
+
COMPOSE_FILE: {
|
|
2148
|
+
type: "string",
|
|
2149
|
+
optional: true,
|
|
2150
|
+
description: "Docker Compose file path (default: docker-compose.yml)"
|
|
2151
|
+
},
|
|
2152
|
+
COMPOSE_PROJECT_NAME: {
|
|
2153
|
+
type: "string",
|
|
2154
|
+
optional: true,
|
|
2155
|
+
description: "Docker Compose project name"
|
|
2156
|
+
},
|
|
2157
|
+
COMPOSE_PROFILES: {
|
|
2158
|
+
type: "array",
|
|
2159
|
+
optional: true,
|
|
2160
|
+
separator: ",",
|
|
2161
|
+
trimItems: true,
|
|
2162
|
+
description: "Docker Compose active profiles"
|
|
2163
|
+
},
|
|
2164
|
+
COMPOSE_DOCKER_CLI_BUILD: {
|
|
2165
|
+
type: "number",
|
|
2166
|
+
optional: true,
|
|
2167
|
+
description: "Use Docker CLI for builds (0 or 1)"
|
|
2168
|
+
},
|
|
2169
|
+
// ── Common Container Services ─────────────────────────────────────
|
|
2170
|
+
// PostgreSQL
|
|
2171
|
+
POSTGRES_USER: {
|
|
2172
|
+
type: "string",
|
|
2173
|
+
optional: true,
|
|
2174
|
+
description: "PostgreSQL superuser name"
|
|
2175
|
+
},
|
|
2176
|
+
POSTGRES_PASSWORD: {
|
|
2177
|
+
type: "string",
|
|
2178
|
+
optional: true,
|
|
2179
|
+
description: "PostgreSQL superuser password"
|
|
2180
|
+
},
|
|
2181
|
+
POSTGRES_DB: {
|
|
2182
|
+
type: "string",
|
|
2183
|
+
optional: true,
|
|
2184
|
+
description: "PostgreSQL default database name"
|
|
2185
|
+
},
|
|
2186
|
+
POSTGRES_HOST: {
|
|
2187
|
+
type: "string",
|
|
2188
|
+
optional: true,
|
|
2189
|
+
description: "PostgreSQL host (within Docker network)",
|
|
2190
|
+
default: "postgres"
|
|
2191
|
+
},
|
|
2192
|
+
POSTGRES_PORT: {
|
|
2193
|
+
type: "number",
|
|
2194
|
+
optional: true,
|
|
2195
|
+
positive: true,
|
|
2196
|
+
description: "PostgreSQL port",
|
|
2197
|
+
default: 5432
|
|
2198
|
+
},
|
|
2199
|
+
// Redis
|
|
2200
|
+
REDIS_HOST: {
|
|
2201
|
+
type: "string",
|
|
2202
|
+
optional: true,
|
|
2203
|
+
description: "Redis host (within Docker network)",
|
|
2204
|
+
default: "redis"
|
|
2205
|
+
},
|
|
2206
|
+
REDIS_PORT: {
|
|
2207
|
+
type: "number",
|
|
2208
|
+
optional: true,
|
|
2209
|
+
positive: true,
|
|
2210
|
+
description: "Redis port",
|
|
2211
|
+
default: 6379
|
|
2212
|
+
},
|
|
2213
|
+
REDIS_PASSWORD: {
|
|
2214
|
+
type: "string",
|
|
2215
|
+
optional: true,
|
|
2216
|
+
description: "Redis password"
|
|
2217
|
+
},
|
|
2218
|
+
// MySQL
|
|
2219
|
+
MYSQL_ROOT_PASSWORD: {
|
|
2220
|
+
type: "string",
|
|
2221
|
+
optional: true,
|
|
2222
|
+
description: "MySQL root password"
|
|
2223
|
+
},
|
|
2224
|
+
MYSQL_DATABASE: {
|
|
2225
|
+
type: "string",
|
|
2226
|
+
optional: true,
|
|
2227
|
+
description: "MySQL default database name"
|
|
2228
|
+
},
|
|
2229
|
+
MYSQL_USER: {
|
|
2230
|
+
type: "string",
|
|
2231
|
+
optional: true,
|
|
2232
|
+
description: "MySQL user name"
|
|
2233
|
+
},
|
|
2234
|
+
MYSQL_PASSWORD: {
|
|
2235
|
+
type: "string",
|
|
2236
|
+
optional: true,
|
|
2237
|
+
description: "MySQL user password"
|
|
2238
|
+
},
|
|
2239
|
+
MYSQL_HOST: {
|
|
2240
|
+
type: "string",
|
|
2241
|
+
optional: true,
|
|
2242
|
+
description: "MySQL host (within Docker network)",
|
|
2243
|
+
default: "mysql"
|
|
2244
|
+
},
|
|
2245
|
+
MYSQL_PORT: {
|
|
2246
|
+
type: "number",
|
|
2247
|
+
optional: true,
|
|
2248
|
+
positive: true,
|
|
2249
|
+
description: "MySQL port",
|
|
2250
|
+
default: 3306
|
|
2251
|
+
},
|
|
2252
|
+
// MongoDB
|
|
2253
|
+
MONGO_INITDB_ROOT_USERNAME: {
|
|
2254
|
+
type: "string",
|
|
2255
|
+
optional: true,
|
|
2256
|
+
description: "MongoDB root username"
|
|
2257
|
+
},
|
|
2258
|
+
MONGO_INITDB_ROOT_PASSWORD: {
|
|
2259
|
+
type: "string",
|
|
2260
|
+
optional: true,
|
|
2261
|
+
description: "MongoDB root password"
|
|
2262
|
+
},
|
|
2263
|
+
MONGO_HOST: {
|
|
2264
|
+
type: "string",
|
|
2265
|
+
optional: true,
|
|
2266
|
+
description: "MongoDB host (within Docker network)",
|
|
2267
|
+
default: "mongodb"
|
|
2268
|
+
},
|
|
2269
|
+
MONGO_PORT: {
|
|
2270
|
+
type: "number",
|
|
2271
|
+
optional: true,
|
|
2272
|
+
positive: true,
|
|
2273
|
+
description: "MongoDB port",
|
|
2274
|
+
default: 27017
|
|
2275
|
+
},
|
|
2276
|
+
// ── Networking ────────────────────────────────────────────────────
|
|
2277
|
+
APP_PORT: {
|
|
2278
|
+
type: "number",
|
|
2279
|
+
optional: true,
|
|
2280
|
+
positive: true,
|
|
2281
|
+
description: "Application container exposed port"
|
|
2282
|
+
},
|
|
2283
|
+
APP_HOST: {
|
|
2284
|
+
type: "string",
|
|
2285
|
+
optional: true,
|
|
2286
|
+
description: "Application container hostname"
|
|
2287
|
+
},
|
|
2288
|
+
VIRTUAL_HOST: {
|
|
2289
|
+
type: "string",
|
|
2290
|
+
optional: true,
|
|
2291
|
+
description: "Virtual host for nginx-proxy"
|
|
2292
|
+
},
|
|
2293
|
+
VIRTUAL_PORT: {
|
|
2294
|
+
type: "number",
|
|
2295
|
+
optional: true,
|
|
2296
|
+
positive: true,
|
|
2297
|
+
description: "Virtual port for nginx-proxy"
|
|
2298
|
+
},
|
|
2299
|
+
LETSENCRYPT_HOST: {
|
|
2300
|
+
type: "string",
|
|
2301
|
+
optional: true,
|
|
2302
|
+
description: "Let's Encrypt host(s) for nginx-proxy companion"
|
|
2303
|
+
},
|
|
2304
|
+
LETSENCRYPT_EMAIL: {
|
|
2305
|
+
type: "string",
|
|
2306
|
+
optional: true,
|
|
2307
|
+
format: "email",
|
|
2308
|
+
description: "Let's Encrypt notification email"
|
|
2309
|
+
},
|
|
2310
|
+
// ── Node.js App in Container ─────────────────────────────────────
|
|
2311
|
+
NODE_ENV: {
|
|
2312
|
+
type: "enum",
|
|
2313
|
+
values: ["development", "production", "test"],
|
|
2314
|
+
description: "Node.js environment mode",
|
|
2315
|
+
default: "production"
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
var DOCKER_FILES = [
|
|
2319
|
+
".env" /* Env */,
|
|
2320
|
+
".env.local" /* EnvLocal */,
|
|
2321
|
+
".env.production" /* EnvProduction */,
|
|
2322
|
+
".env.production.local" /* EnvProductionLocal */
|
|
2323
|
+
];
|
|
2324
|
+
var dockerPreset = {
|
|
2325
|
+
id: "docker",
|
|
2326
|
+
name: "Docker",
|
|
2327
|
+
description: "Configuration preset for Docker and Docker Compose with ARG/ENV awareness",
|
|
2328
|
+
schema: dockerSchema,
|
|
2329
|
+
files: DOCKER_FILES,
|
|
2330
|
+
tags: ["infrastructure", "containers", "devops"]
|
|
2331
|
+
};
|
|
2332
|
+
|
|
2333
|
+
// src/presets/aws-lambda.ts
|
|
2334
|
+
var awsLambdaSchema = {
|
|
2335
|
+
// ── AWS System Variables (set by Lambda runtime) ──────────────────
|
|
2336
|
+
AWS_REGION: {
|
|
2337
|
+
type: "string",
|
|
2338
|
+
optional: true,
|
|
2339
|
+
description: "AWS region for the Lambda function (auto-set by Lambda)"
|
|
2340
|
+
},
|
|
2341
|
+
AWS_DEFAULT_REGION: {
|
|
2342
|
+
type: "string",
|
|
2343
|
+
optional: true,
|
|
2344
|
+
description: "Default AWS region (usually same as AWS_REGION)"
|
|
2345
|
+
},
|
|
2346
|
+
AWS_ACCESS_KEY_ID: {
|
|
2347
|
+
type: "string",
|
|
2348
|
+
optional: true,
|
|
2349
|
+
description: "AWS access key ID (auto-set by Lambda execution role)"
|
|
2350
|
+
},
|
|
2351
|
+
AWS_SECRET_ACCESS_KEY: {
|
|
2352
|
+
type: "string",
|
|
2353
|
+
optional: true,
|
|
2354
|
+
description: "AWS secret access key (auto-set by Lambda execution role)"
|
|
2355
|
+
},
|
|
2356
|
+
AWS_SESSION_TOKEN: {
|
|
2357
|
+
type: "string",
|
|
2358
|
+
optional: true,
|
|
2359
|
+
description: "AWS session token for temporary credentials"
|
|
2360
|
+
},
|
|
2361
|
+
// ── Lambda Runtime Info (read-only, set by runtime) ───────────────
|
|
2362
|
+
AWS_LAMBDA_FUNCTION_NAME: {
|
|
2363
|
+
type: "string",
|
|
2364
|
+
optional: true,
|
|
2365
|
+
description: "Lambda function name (set by runtime)"
|
|
2366
|
+
},
|
|
2367
|
+
AWS_LAMBDA_FUNCTION_VERSION: {
|
|
2368
|
+
type: "string",
|
|
2369
|
+
optional: true,
|
|
2370
|
+
description: "Lambda function version (set by runtime)"
|
|
2371
|
+
},
|
|
2372
|
+
AWS_LAMBDA_FUNCTION_MEMORY_SIZE: {
|
|
2373
|
+
type: "number",
|
|
2374
|
+
optional: true,
|
|
2375
|
+
positive: true,
|
|
2376
|
+
description: "Allocated memory in MB (set by Lambda configuration)"
|
|
2377
|
+
},
|
|
2378
|
+
AWS_LAMBDA_FUNCTION_TIMEOUT: {
|
|
2379
|
+
type: "number",
|
|
2380
|
+
optional: true,
|
|
2381
|
+
positive: true,
|
|
2382
|
+
description: "Function timeout in seconds (set by Lambda configuration)"
|
|
2383
|
+
},
|
|
2384
|
+
AWS_LAMBDA_LOG_GROUP_NAME: {
|
|
2385
|
+
type: "string",
|
|
2386
|
+
optional: true,
|
|
2387
|
+
description: "CloudWatch log group name (set by runtime)"
|
|
2388
|
+
},
|
|
2389
|
+
AWS_LAMBDA_LOG_STREAM_NAME: {
|
|
2390
|
+
type: "string",
|
|
2391
|
+
optional: true,
|
|
2392
|
+
description: "CloudWatch log stream name (set by runtime)"
|
|
2393
|
+
},
|
|
2394
|
+
// ── Custom Application Variables ──────────────────────────────────
|
|
2395
|
+
NODE_ENV: {
|
|
2396
|
+
type: "enum",
|
|
2397
|
+
values: ["development", "production", "test"],
|
|
2398
|
+
description: "Node.js environment mode",
|
|
2399
|
+
default: "production"
|
|
2400
|
+
},
|
|
2401
|
+
APP_ENV: {
|
|
2402
|
+
type: "string",
|
|
2403
|
+
optional: true,
|
|
2404
|
+
description: "Application environment identifier (e.g., staging, prod)"
|
|
2405
|
+
},
|
|
2406
|
+
LOG_LEVEL: {
|
|
2407
|
+
type: "enum",
|
|
2408
|
+
values: ["fatal", "error", "warn", "info", "debug", "trace", "silent"],
|
|
2409
|
+
optional: true,
|
|
2410
|
+
description: "Application log level",
|
|
2411
|
+
default: "info"
|
|
2412
|
+
},
|
|
2413
|
+
// ── Database (common patterns) ────────────────────────────────────
|
|
2414
|
+
DATABASE_URL: {
|
|
2415
|
+
type: "string",
|
|
2416
|
+
optional: true,
|
|
2417
|
+
format: "url",
|
|
2418
|
+
description: "Database connection URL"
|
|
2419
|
+
},
|
|
2420
|
+
DB_HOST: {
|
|
2421
|
+
type: "string",
|
|
2422
|
+
optional: true,
|
|
2423
|
+
description: "Database host (within VPC)"
|
|
2424
|
+
},
|
|
2425
|
+
DB_PORT: {
|
|
2426
|
+
type: "number",
|
|
2427
|
+
optional: true,
|
|
2428
|
+
positive: true,
|
|
2429
|
+
description: "Database port"
|
|
2430
|
+
},
|
|
2431
|
+
DB_NAME: {
|
|
2432
|
+
type: "string",
|
|
2433
|
+
optional: true,
|
|
2434
|
+
description: "Database name"
|
|
2435
|
+
},
|
|
2436
|
+
DB_USER: {
|
|
2437
|
+
type: "string",
|
|
2438
|
+
optional: true,
|
|
2439
|
+
description: "Database user"
|
|
2440
|
+
},
|
|
2441
|
+
DB_PASSWORD: {
|
|
2442
|
+
type: "string",
|
|
2443
|
+
optional: true,
|
|
2444
|
+
description: "Database password"
|
|
2445
|
+
},
|
|
2446
|
+
// ── Common AWS Service Variables ──────────────────────────────────
|
|
2447
|
+
S3_BUCKET_NAME: {
|
|
2448
|
+
type: "string",
|
|
2449
|
+
optional: true,
|
|
2450
|
+
description: "Default S3 bucket name"
|
|
2451
|
+
},
|
|
2452
|
+
SQS_QUEUE_URL: {
|
|
2453
|
+
type: "string",
|
|
2454
|
+
optional: true,
|
|
2455
|
+
format: "url",
|
|
2456
|
+
description: "Default SQS queue URL"
|
|
2457
|
+
},
|
|
2458
|
+
SNS_TOPIC_ARN: {
|
|
2459
|
+
type: "string",
|
|
2460
|
+
optional: true,
|
|
2461
|
+
description: "Default SNS topic ARN"
|
|
2462
|
+
},
|
|
2463
|
+
DYNAMODB_TABLE_NAME: {
|
|
2464
|
+
type: "string",
|
|
2465
|
+
optional: true,
|
|
2466
|
+
description: "Default DynamoDB table name"
|
|
2467
|
+
},
|
|
2468
|
+
KMS_KEY_ID: {
|
|
2469
|
+
type: "string",
|
|
2470
|
+
optional: true,
|
|
2471
|
+
description: "Default KMS key ID for encryption"
|
|
2472
|
+
},
|
|
2473
|
+
COGNITO_USER_POOL_ID: {
|
|
2474
|
+
type: "string",
|
|
2475
|
+
optional: true,
|
|
2476
|
+
description: "Cognito user pool ID"
|
|
2477
|
+
},
|
|
2478
|
+
COGNITO_CLIENT_ID: {
|
|
2479
|
+
type: "string",
|
|
2480
|
+
optional: true,
|
|
2481
|
+
description: "Cognito app client ID"
|
|
2482
|
+
},
|
|
2483
|
+
// ── Secrets Manager / SSM Integration ─────────────────────────────
|
|
2484
|
+
SECRETS_MANAGER_PREFIX: {
|
|
2485
|
+
type: "string",
|
|
2486
|
+
optional: true,
|
|
2487
|
+
description: "AWS Secrets Manager path prefix for this function"
|
|
2488
|
+
},
|
|
2489
|
+
SSM_PARAMETER_PREFIX: {
|
|
2490
|
+
type: "string",
|
|
2491
|
+
optional: true,
|
|
2492
|
+
description: "AWS SSM Parameter Store path prefix for this function"
|
|
2493
|
+
},
|
|
2494
|
+
// ── Feature Flags ─────────────────────────────────────────────────
|
|
2495
|
+
FEATURE_FLAGS: {
|
|
2496
|
+
type: "json",
|
|
2497
|
+
optional: true,
|
|
2498
|
+
description: "Feature flags configuration (JSON object)"
|
|
2499
|
+
},
|
|
2500
|
+
ENABLE_DEBUG_MODE: {
|
|
2501
|
+
type: "boolean",
|
|
2502
|
+
optional: true,
|
|
2503
|
+
description: "Enable verbose debug logging"
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
var AWS_LAMBDA_FILES = [
|
|
2507
|
+
".env" /* Env */,
|
|
2508
|
+
".env.local" /* EnvLocal */,
|
|
2509
|
+
".env.production" /* EnvProduction */,
|
|
2510
|
+
".env.production.local" /* EnvProductionLocal */
|
|
2511
|
+
];
|
|
2512
|
+
var awsLambdaPreset = {
|
|
2513
|
+
id: "aws-lambda",
|
|
2514
|
+
name: "AWS Lambda",
|
|
2515
|
+
description: "Configuration preset for AWS Lambda with AWS system variable awareness",
|
|
2516
|
+
schema: awsLambdaSchema,
|
|
2517
|
+
files: AWS_LAMBDA_FILES,
|
|
2518
|
+
tags: ["serverless", "aws", "cloud", "infrastructure"]
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
// src/presets/index.ts
|
|
2522
|
+
var presets = {};
|
|
2523
|
+
function registerPreset(name, preset) {
|
|
2524
|
+
if (presets[name] !== void 0) {
|
|
2525
|
+
process.stderr.write(
|
|
2526
|
+
`[ultraenv] Warning: overwriting existing preset "${name}"
|
|
2527
|
+
`
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
presets[name] = preset;
|
|
2531
|
+
}
|
|
2532
|
+
function getPreset(name) {
|
|
2533
|
+
return presets[name];
|
|
2534
|
+
}
|
|
2535
|
+
function listPresets() {
|
|
2536
|
+
return Object.keys(presets);
|
|
2537
|
+
}
|
|
2538
|
+
function hasPreset(name) {
|
|
2539
|
+
return name in presets;
|
|
2540
|
+
}
|
|
2541
|
+
function unregisterPreset(name) {
|
|
2542
|
+
if (name in presets) {
|
|
2543
|
+
delete presets[name];
|
|
2544
|
+
return true;
|
|
2545
|
+
}
|
|
2546
|
+
return false;
|
|
2547
|
+
}
|
|
2548
|
+
function getAllPresets() {
|
|
2549
|
+
return { ...presets };
|
|
2550
|
+
}
|
|
2551
|
+
var BUILT_IN_PRESETS = [
|
|
2552
|
+
["nextjs", nextjsPreset],
|
|
2553
|
+
["vite", vitePreset],
|
|
2554
|
+
["nuxt", nuxtPreset],
|
|
2555
|
+
["remix", remixPreset],
|
|
2556
|
+
["sveltekit", sveltekitPreset],
|
|
2557
|
+
["express", expressPreset],
|
|
2558
|
+
["fastify", fastifyPreset],
|
|
2559
|
+
["docker", dockerPreset],
|
|
2560
|
+
["aws-lambda", awsLambdaPreset]
|
|
2561
|
+
];
|
|
2562
|
+
for (const [name, preset] of BUILT_IN_PRESETS) {
|
|
2563
|
+
registerPreset(name, preset);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// src/middleware/health.ts
|
|
2567
|
+
function healthCheck(options = {}) {
|
|
2568
|
+
const {
|
|
2569
|
+
source = process.env,
|
|
2570
|
+
loadedFiles = [],
|
|
2571
|
+
validCount,
|
|
2572
|
+
metadata = {}
|
|
2573
|
+
} = options;
|
|
2574
|
+
const envKeys = Object.entries(source).filter(
|
|
2575
|
+
([, value]) => value !== void 0 && value !== ""
|
|
2576
|
+
);
|
|
2577
|
+
const loaded = envKeys.length;
|
|
2578
|
+
const environment = detectEnvironment(source);
|
|
2579
|
+
const valid = validCount ?? loaded;
|
|
2580
|
+
const files = loadedFiles.length > 0 ? loadedFiles.map((f) => {
|
|
2581
|
+
const lastSlash = f.lastIndexOf("/");
|
|
2582
|
+
const lastBackslash = f.lastIndexOf("\\");
|
|
2583
|
+
const lastSep = Math.max(lastSlash, lastBackslash);
|
|
2584
|
+
return lastSep >= 0 ? f.slice(lastSep + 1) : f;
|
|
2585
|
+
}) : [];
|
|
2586
|
+
return {
|
|
2587
|
+
status: "ok",
|
|
2588
|
+
loaded,
|
|
2589
|
+
valid,
|
|
2590
|
+
environment,
|
|
2591
|
+
files,
|
|
2592
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2593
|
+
metadata
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
function liveCheck() {
|
|
2597
|
+
return {
|
|
2598
|
+
status: "ok",
|
|
2599
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
function readinessCheck(requiredVars, source) {
|
|
2603
|
+
const env = source ?? process.env;
|
|
2604
|
+
const missing = [];
|
|
2605
|
+
for (const varName of requiredVars) {
|
|
2606
|
+
const value = env[varName];
|
|
2607
|
+
if (value === void 0 || value === "") {
|
|
2608
|
+
missing.push(varName);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
return {
|
|
2612
|
+
status: missing.length === 0 ? "ok" : "error",
|
|
2613
|
+
ready: missing.length === 0,
|
|
2614
|
+
missing,
|
|
2615
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
function detectEnvironment(env) {
|
|
2619
|
+
const candidates = [
|
|
2620
|
+
"NODE_ENV",
|
|
2621
|
+
"APP_ENV",
|
|
2622
|
+
"ENVIRONMENT",
|
|
2623
|
+
"ENV",
|
|
2624
|
+
"STAGE"
|
|
2625
|
+
];
|
|
2626
|
+
for (const candidate of candidates) {
|
|
2627
|
+
const value = env[candidate];
|
|
2628
|
+
if (value !== void 0 && value !== "") {
|
|
2629
|
+
return value;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return "unknown";
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/utils/box.ts
|
|
2636
|
+
var DOUBLE = {
|
|
2637
|
+
topLeft: "\u2554",
|
|
2638
|
+
topRight: "\u2557",
|
|
2639
|
+
bottomLeft: "\u255A",
|
|
2640
|
+
bottomRight: "\u255D",
|
|
2641
|
+
horizontal: "\u2550",
|
|
2642
|
+
vertical: "\u2551",
|
|
2643
|
+
leftTee: "\u2560",
|
|
2644
|
+
rightTee: "\u2563"
|
|
2645
|
+
};
|
|
2646
|
+
function hRuleBottom(width, chars = DOUBLE) {
|
|
2647
|
+
return `${chars.bottomLeft}${chars.horizontal.repeat(width + 2)}${chars.bottomRight}`;
|
|
2648
|
+
}
|
|
2649
|
+
function boxLine(text, width, chars = DOUBLE) {
|
|
2650
|
+
const padded = text.length <= width ? text.padEnd(width) : text.slice(0, width);
|
|
2651
|
+
return `${chars.vertical} ${padded} ${chars.vertical}`;
|
|
2652
|
+
}
|
|
2653
|
+
function drawTitledBox(title, lines, chars = DOUBLE) {
|
|
2654
|
+
if (lines.length === 0) {
|
|
2655
|
+
return [hRuleTitled(title, 0, chars), boxLine("", 0, chars), hRuleBottom(0, chars)].join("\n");
|
|
2656
|
+
}
|
|
2657
|
+
const innerWidth = Math.max(
|
|
2658
|
+
...lines.map((line) => stripAnsi(line).length),
|
|
2659
|
+
title.length + 2,
|
|
2660
|
+
0
|
|
2661
|
+
);
|
|
2662
|
+
const result = [];
|
|
2663
|
+
result.push(hRuleTitled(title, innerWidth, chars));
|
|
2664
|
+
for (const line of lines) {
|
|
2665
|
+
const stripped = stripAnsi(line);
|
|
2666
|
+
const visibleLen = stripped.length;
|
|
2667
|
+
const rightPad = Math.max(0, innerWidth - visibleLen);
|
|
2668
|
+
result.push(`${chars.vertical} ${line}${" ".repeat(rightPad)} ${chars.vertical}`);
|
|
2669
|
+
}
|
|
2670
|
+
result.push(hRuleBottom(innerWidth, chars));
|
|
2671
|
+
return result.join("\n");
|
|
2672
|
+
}
|
|
2673
|
+
function hRuleTitled(title, width, chars = DOUBLE) {
|
|
2674
|
+
const paddedTitle = ` ${title} `;
|
|
2675
|
+
const totalWidth = width + 2;
|
|
2676
|
+
const remaining = Math.max(0, totalWidth - paddedTitle.length);
|
|
2677
|
+
const right = remaining % 2 === 0 ? chars.horizontal.repeat(remaining) : chars.horizontal.repeat(remaining) + " ";
|
|
2678
|
+
return `${chars.topLeft}${chars.horizontal}${paddedTitle}${right}${chars.topRight}`;
|
|
2679
|
+
}
|
|
2680
|
+
function stripAnsi(str) {
|
|
2681
|
+
return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// src/reporters/terminal.ts
|
|
2685
|
+
var ANSI = {
|
|
2686
|
+
reset: "\x1B[0m",
|
|
2687
|
+
bold: "\x1B[1m",
|
|
2688
|
+
dim: "\x1B[2m",
|
|
2689
|
+
red: "\x1B[31m",
|
|
2690
|
+
green: "\x1B[32m",
|
|
2691
|
+
yellow: "\x1B[33m",
|
|
2692
|
+
blue: "\x1B[34m",
|
|
2693
|
+
magenta: "\x1B[35m",
|
|
2694
|
+
cyan: "\x1B[36m",
|
|
2695
|
+
white: "\x1B[37m",
|
|
2696
|
+
gray: "\x1B[90m",
|
|
2697
|
+
bgRed: "\x1B[41m",
|
|
2698
|
+
bgGreen: "\x1B[42m",
|
|
2699
|
+
bgYellow: "\x1B[43m"
|
|
2700
|
+
};
|
|
2701
|
+
var DEFAULT_BOX = {
|
|
2702
|
+
topLeft: "\u2554",
|
|
2703
|
+
topRight: "\u2557",
|
|
2704
|
+
bottomLeft: "\u255A",
|
|
2705
|
+
bottomRight: "\u255D",
|
|
2706
|
+
horizontal: "\u2550",
|
|
2707
|
+
vertical: "\u2551",
|
|
2708
|
+
leftTee: "\u2560",
|
|
2709
|
+
rightTee: "\u2563"
|
|
2710
|
+
};
|
|
2711
|
+
function reportValidation(result, options = {}) {
|
|
2712
|
+
const { color = true, boxStyle = DEFAULT_BOX } = options;
|
|
2713
|
+
const c = color ? ANSI : noColorANSI();
|
|
2714
|
+
if (result.valid) {
|
|
2715
|
+
const lines = [];
|
|
2716
|
+
lines.push(`${c.green}${c.bold} \u2713${c.reset} All ${Object.keys(result.validated).length} variables passed validation`);
|
|
2717
|
+
if (result.warnings.length > 0) {
|
|
2718
|
+
lines.push("");
|
|
2719
|
+
for (const warning of result.warnings) {
|
|
2720
|
+
lines.push(`${c.yellow} \u26A0${c.reset} ${warning.field}: ${warning.message}`);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
return lines.join("\n");
|
|
2724
|
+
}
|
|
2725
|
+
const errorLines = [];
|
|
2726
|
+
for (const error of result.errors) {
|
|
2727
|
+
const fieldDisplay = `${c.red}${c.bold}${error.field}${c.reset}`;
|
|
2728
|
+
const messageDisplay = error.message;
|
|
2729
|
+
const expectedDisplay = `${c.dim}expected: ${error.expected}${c.reset}`;
|
|
2730
|
+
const valueDisplay = error.value !== void 0 && error.value !== "" ? `${c.dim}value: ${maskValue2(error.value)}${c.reset}` : `${c.dim}value: (empty)${c.reset}`;
|
|
2731
|
+
const hintDisplay = error.hint !== void 0 ? `${c.cyan}hint: ${error.hint}${c.reset}` : "";
|
|
2732
|
+
const sourceDisplay = error.source !== void 0 ? `${c.gray}source: ${error.source}${error.source !== void 0 && error.lineNumber !== void 0 ? `:${error.lineNumber}` : ""}${c.reset}` : "";
|
|
2733
|
+
errorLines.push(`${fieldDisplay} ${messageDisplay}`);
|
|
2734
|
+
errorLines.push(` ${expectedDisplay}`);
|
|
2735
|
+
errorLines.push(` ${valueDisplay}`);
|
|
2736
|
+
if (hintDisplay) errorLines.push(` ${hintDisplay}`);
|
|
2737
|
+
if (sourceDisplay) errorLines.push(` ${sourceDisplay}`);
|
|
2738
|
+
}
|
|
2739
|
+
const title = `${c.bgRed}${c.white}${c.bold} Validation Failed (${result.errors.length} error${result.errors.length === 1 ? "" : "s"}) ${c.reset}`;
|
|
2740
|
+
const box = drawTitledBox(title, errorLines, boxStyle);
|
|
2741
|
+
const output = [box];
|
|
2742
|
+
if (result.warnings.length > 0) {
|
|
2743
|
+
output.push("");
|
|
2744
|
+
const warningLines = [];
|
|
2745
|
+
for (const warning of result.warnings) {
|
|
2746
|
+
warningLines.push(`${c.yellow}\u26A0${c.reset} ${warning.field}: ${warning.message} ${c.dim}[${warning.code}]${c.reset}`);
|
|
2747
|
+
}
|
|
2748
|
+
output.push(warningLines.join("\n"));
|
|
2749
|
+
}
|
|
2750
|
+
if (result.unknown.length > 0) {
|
|
2751
|
+
output.push("");
|
|
2752
|
+
output.push(`${c.gray}?${c.reset} Unknown variables: ${result.unknown.map((u) => `${c.dim}${u}${c.reset}`).join(", ")}`);
|
|
2753
|
+
}
|
|
2754
|
+
return output.join("\n");
|
|
2755
|
+
}
|
|
2756
|
+
function reportError(error, options = {}) {
|
|
2757
|
+
const { color = true, boxStyle = DEFAULT_BOX } = options;
|
|
2758
|
+
const c = color ? ANSI : noColorANSI();
|
|
2759
|
+
const code = error.code ?? "ULTRAENV_ERROR";
|
|
2760
|
+
const lines = [
|
|
2761
|
+
`${c.red}${c.bold}Error [${code}]${c.reset}`,
|
|
2762
|
+
` ${error.message}`
|
|
2763
|
+
];
|
|
2764
|
+
if (error.hint !== void 0) {
|
|
2765
|
+
lines.push("");
|
|
2766
|
+
lines.push(`${c.cyan} \u{1F4A1} ${error.hint}${c.reset}`);
|
|
2767
|
+
}
|
|
2768
|
+
const title = `${c.bgRed}${c.white}${c.bold} ${code} ${c.reset}`;
|
|
2769
|
+
return drawTitledBox(title, lines, boxStyle);
|
|
2770
|
+
}
|
|
2771
|
+
function reportSuccess(message, options = {}) {
|
|
2772
|
+
const { color = true } = options;
|
|
2773
|
+
const c = color ? ANSI : noColorANSI();
|
|
2774
|
+
return `${c.green}${c.bold} \u2713${c.reset} ${message}`;
|
|
2775
|
+
}
|
|
2776
|
+
function reportWarning(warning, options = {}) {
|
|
2777
|
+
const { color = true } = options;
|
|
2778
|
+
const c = color ? ANSI : noColorANSI();
|
|
2779
|
+
const parts = [
|
|
2780
|
+
`${c.yellow} \u26A0${c.reset} ${c.bold}${warning.field}${c.reset}: ${warning.message}`
|
|
2781
|
+
];
|
|
2782
|
+
if (warning.value !== void 0 && warning.value !== "") {
|
|
2783
|
+
parts.push(`${c.dim} value: ${maskValue2(warning.value)}${c.reset}`);
|
|
2784
|
+
}
|
|
2785
|
+
if (warning.code !== void 0) {
|
|
2786
|
+
parts.push(`${c.dim} code: ${warning.code}${c.reset}`);
|
|
2787
|
+
}
|
|
2788
|
+
return parts.join("\n");
|
|
2789
|
+
}
|
|
2790
|
+
function reportInfo(message, options = {}) {
|
|
2791
|
+
const { color = true } = options;
|
|
2792
|
+
const c = color ? ANSI : noColorANSI();
|
|
2793
|
+
return `${c.blue} \u2139${c.reset} ${message}`;
|
|
2794
|
+
}
|
|
2795
|
+
function reportScanResult(result, options = {}) {
|
|
2796
|
+
const { color = true, boxStyle = DEFAULT_BOX } = options;
|
|
2797
|
+
const c = color ? ANSI : noColorANSI();
|
|
2798
|
+
if (!result.found) {
|
|
2799
|
+
return `${c.green}${c.bold} \u2713${c.reset} No secrets detected. Scanned ${result.filesScanned.length} file(s) in ${result.scanTimeMs}ms.`;
|
|
2800
|
+
}
|
|
2801
|
+
const output = [];
|
|
2802
|
+
const summaryLines = [
|
|
2803
|
+
`${c.red}${c.bold}${result.secrets.length} secret(s) detected${c.reset} across ${result.secrets.length > 0 ? new Set(result.secrets.map((s) => s.file)).size : 0} file(s)`,
|
|
2804
|
+
`Scanned: ${result.filesScanned.length} files | Skipped: ${result.filesSkipped.length} | Time: ${result.scanTimeMs}ms`
|
|
2805
|
+
];
|
|
2806
|
+
const summaryTitle = `${c.bgRed}${c.white}${c.bold} Secret Scan Results ${c.reset}`;
|
|
2807
|
+
output.push(drawTitledBox(summaryTitle, summaryLines, boxStyle));
|
|
2808
|
+
output.push("");
|
|
2809
|
+
const header = [
|
|
2810
|
+
`${c.bold}Severity${c.reset}`,
|
|
2811
|
+
`${c.bold}Type${c.reset}`,
|
|
2812
|
+
`${c.bold}File${c.reset}`,
|
|
2813
|
+
`${c.bold}Line${c.reset}`,
|
|
2814
|
+
`${c.bold}Value${c.reset}`
|
|
2815
|
+
];
|
|
2816
|
+
const rows = [header];
|
|
2817
|
+
for (const secret of result.secrets) {
|
|
2818
|
+
const severityColor = secret.pattern.severity === "critical" ? c.red : secret.pattern.severity === "high" ? c.yellow : c.dim;
|
|
2819
|
+
const severityIcon = secret.pattern.severity === "critical" ? "\u2717" : secret.pattern.severity === "high" ? "!" : "?";
|
|
2820
|
+
const lastSlash = secret.file.lastIndexOf("/");
|
|
2821
|
+
const lastBackslash = secret.file.lastIndexOf("\\");
|
|
2822
|
+
const lastSep = Math.max(lastSlash, lastBackslash);
|
|
2823
|
+
const fileName = lastSep >= 0 ? secret.file.slice(lastSep + 1) : secret.file;
|
|
2824
|
+
rows.push([
|
|
2825
|
+
`${severityColor}${severityIcon} ${secret.pattern.severity}${c.reset}`,
|
|
2826
|
+
secret.pattern.name,
|
|
2827
|
+
`${c.cyan}${fileName}${c.reset}`,
|
|
2828
|
+
`${c.dim}:${secret.line}:${secret.column}${c.reset}`,
|
|
2829
|
+
`${c.dim}${maskValue2(secret.value, 6, 4)}${c.reset}`
|
|
2830
|
+
]);
|
|
2831
|
+
}
|
|
2832
|
+
const table = formatTable(rows);
|
|
2833
|
+
output.push(table);
|
|
2834
|
+
return output.join("\n");
|
|
2835
|
+
}
|
|
2836
|
+
function formatTable(rows) {
|
|
2837
|
+
if (rows.length === 0) return "";
|
|
2838
|
+
function visibleWidth(s) {
|
|
2839
|
+
return s.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").length;
|
|
2840
|
+
}
|
|
2841
|
+
const colCount = Math.max(...rows.map((row) => row.length));
|
|
2842
|
+
const colWidths = [];
|
|
2843
|
+
for (let col = 0; col < colCount; col++) {
|
|
2844
|
+
let maxW = 0;
|
|
2845
|
+
for (const row of rows) {
|
|
2846
|
+
if (col < row.length) {
|
|
2847
|
+
const w = visibleWidth(row[col]);
|
|
2848
|
+
if (w > maxW) maxW = w;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
colWidths.push(maxW);
|
|
2852
|
+
}
|
|
2853
|
+
return rows.map(
|
|
2854
|
+
(row) => row.map((cell, col) => {
|
|
2855
|
+
const w = visibleWidth(cell);
|
|
2856
|
+
const padding = Math.max(0, (colWidths[col] ?? 0) - w);
|
|
2857
|
+
return cell + " ".repeat(padding);
|
|
2858
|
+
}).join(" ")
|
|
2859
|
+
).join("\n");
|
|
2860
|
+
}
|
|
2861
|
+
function maskValue2(value, visibleStart = 4, visibleEnd = 4) {
|
|
2862
|
+
if (value.length <= visibleStart + visibleEnd) {
|
|
2863
|
+
return "*".repeat(value.length);
|
|
2864
|
+
}
|
|
2865
|
+
const start = value.slice(0, visibleStart);
|
|
2866
|
+
const end = value.slice(-visibleEnd);
|
|
2867
|
+
const masked = "*".repeat(Math.max(4, value.length - visibleStart - visibleEnd));
|
|
2868
|
+
return `${start}${masked}${end}`;
|
|
2869
|
+
}
|
|
2870
|
+
function noColorANSI() {
|
|
2871
|
+
const obj = {};
|
|
2872
|
+
for (const [key, _value] of Object.entries(ANSI)) {
|
|
2873
|
+
obj[key] = "";
|
|
2874
|
+
}
|
|
2875
|
+
return obj;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// src/reporters/json.ts
|
|
2879
|
+
function reportValidation2(result, options = {}) {
|
|
2880
|
+
const { pretty = true, indent = 2 } = options;
|
|
2881
|
+
const output = {
|
|
2882
|
+
valid: result.valid,
|
|
2883
|
+
errorCount: result.errors.length,
|
|
2884
|
+
warningCount: result.warnings.length,
|
|
2885
|
+
validatedCount: Object.keys(result.validated).length,
|
|
2886
|
+
unknownCount: result.unknown.length
|
|
2887
|
+
};
|
|
2888
|
+
if (result.errors.length > 0) {
|
|
2889
|
+
output.errors = result.errors.map((error) => ({
|
|
2890
|
+
field: error.field,
|
|
2891
|
+
message: error.message,
|
|
2892
|
+
expected: error.expected,
|
|
2893
|
+
hint: error.hint,
|
|
2894
|
+
source: error.source ?? null,
|
|
2895
|
+
lineNumber: error.lineNumber ?? null
|
|
2896
|
+
}));
|
|
2897
|
+
}
|
|
2898
|
+
if (result.warnings.length > 0) {
|
|
2899
|
+
output.warnings = result.warnings.map((warning) => ({
|
|
2900
|
+
field: warning.field,
|
|
2901
|
+
message: warning.message,
|
|
2902
|
+
code: warning.code
|
|
2903
|
+
}));
|
|
2904
|
+
}
|
|
2905
|
+
if (result.unknown.length > 0) {
|
|
2906
|
+
output.unknown = result.unknown;
|
|
2907
|
+
}
|
|
2908
|
+
return serialize(output, pretty, indent);
|
|
2909
|
+
}
|
|
2910
|
+
function reportError2(error, options = {}) {
|
|
2911
|
+
const { pretty = true, indent = 2, includeStack = false } = options;
|
|
2912
|
+
const output = {
|
|
2913
|
+
name: error.name,
|
|
2914
|
+
code: error.code,
|
|
2915
|
+
message: error.message
|
|
2916
|
+
};
|
|
2917
|
+
if (error.hint !== void 0) {
|
|
2918
|
+
output.hint = error.hint;
|
|
2919
|
+
}
|
|
2920
|
+
if (includeStack && error.stack !== void 0) {
|
|
2921
|
+
output.stack = error.stack;
|
|
2922
|
+
}
|
|
2923
|
+
if (error.cause !== void 0) {
|
|
2924
|
+
output.cause = error.cause instanceof Error ? {
|
|
2925
|
+
name: error.cause.name,
|
|
2926
|
+
message: error.cause.message,
|
|
2927
|
+
...includeStack && error.cause.stack ? { stack: error.cause.stack } : {}
|
|
2928
|
+
} : String(error.cause);
|
|
2929
|
+
}
|
|
2930
|
+
return serialize(output, pretty, indent);
|
|
2931
|
+
}
|
|
2932
|
+
function reportScanResult2(result, options = {}) {
|
|
2933
|
+
const { pretty = true, indent = 2 } = options;
|
|
2934
|
+
const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
2935
|
+
for (const secret of result.secrets) {
|
|
2936
|
+
const severity = secret.pattern.severity;
|
|
2937
|
+
severityCounts[severity] = (severityCounts[severity] ?? 0) + 1;
|
|
2938
|
+
}
|
|
2939
|
+
const output = {
|
|
2940
|
+
found: result.found,
|
|
2941
|
+
totalSecrets: result.secrets.length,
|
|
2942
|
+
severityBreakdown: severityCounts,
|
|
2943
|
+
filesScanned: result.filesScanned.length,
|
|
2944
|
+
filesSkipped: result.filesSkipped.length,
|
|
2945
|
+
scanTimeMs: result.scanTimeMs,
|
|
2946
|
+
timestamp: result.timestamp
|
|
2947
|
+
};
|
|
2948
|
+
if (result.secrets.length > 0) {
|
|
2949
|
+
output.secrets = result.secrets.map((secret) => ({
|
|
2950
|
+
type: secret.pattern.name,
|
|
2951
|
+
severity: secret.pattern.severity,
|
|
2952
|
+
category: secret.pattern.category,
|
|
2953
|
+
file: secret.file,
|
|
2954
|
+
line: secret.line,
|
|
2955
|
+
column: secret.column,
|
|
2956
|
+
value: maskValue3(secret.value),
|
|
2957
|
+
confidence: secret.confidence,
|
|
2958
|
+
remediation: secret.pattern.remediation
|
|
2959
|
+
}));
|
|
2960
|
+
}
|
|
2961
|
+
return serialize(output, pretty, indent);
|
|
2962
|
+
}
|
|
2963
|
+
function serialize(obj, pretty, indent) {
|
|
2964
|
+
if (pretty) {
|
|
2965
|
+
return JSON.stringify(obj, null, indent);
|
|
2966
|
+
}
|
|
2967
|
+
return JSON.stringify(obj);
|
|
2968
|
+
}
|
|
2969
|
+
function maskValue3(value, visibleChars = 4) {
|
|
2970
|
+
if (value.length <= visibleChars + 4) {
|
|
2971
|
+
return "*".repeat(value.length);
|
|
2972
|
+
}
|
|
2973
|
+
const start = value.slice(0, visibleChars);
|
|
2974
|
+
const end = value.slice(-4);
|
|
2975
|
+
const masked = "*".repeat(Math.max(4, value.length - visibleChars - 4));
|
|
2976
|
+
return `${start}${masked}${end}`;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// src/reporters/sarif.ts
|
|
2980
|
+
function mapSeverity(severity) {
|
|
2981
|
+
switch (severity) {
|
|
2982
|
+
case "critical":
|
|
2983
|
+
return { level: "error", securitySeverity: "9.0" };
|
|
2984
|
+
case "high":
|
|
2985
|
+
return { level: "error", securitySeverity: "7.0" };
|
|
2986
|
+
case "medium":
|
|
2987
|
+
return { level: "warning", securitySeverity: "5.0" };
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
function reportScanResult3(result, options = {}) {
|
|
2991
|
+
const { toolVersion = "1.0.0" } = options;
|
|
2992
|
+
const ruleMap = /* @__PURE__ */ new Map();
|
|
2993
|
+
const rules = [];
|
|
2994
|
+
for (const secret of result.secrets) {
|
|
2995
|
+
const pattern = secret.pattern;
|
|
2996
|
+
if (ruleMap.has(pattern.id)) continue;
|
|
2997
|
+
const { securitySeverity } = mapSeverity(pattern.severity);
|
|
2998
|
+
const rule = {
|
|
2999
|
+
id: pattern.id,
|
|
3000
|
+
name: pattern.name,
|
|
3001
|
+
shortDescription: { text: pattern.name },
|
|
3002
|
+
fullDescription: { text: pattern.description },
|
|
3003
|
+
helpUri: `https://github.com/ultraenv/ultraenv/blob/main/docs/rules/${pattern.id}.md`,
|
|
3004
|
+
properties: {
|
|
3005
|
+
"security-severity": securitySeverity,
|
|
3006
|
+
tags: [`security`, `secret`, pattern.category]
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
3009
|
+
ruleMap.set(pattern.id, rule);
|
|
3010
|
+
rules.push(rule);
|
|
3011
|
+
}
|
|
3012
|
+
if (options.additionalRules !== void 0) {
|
|
3013
|
+
for (const rule of options.additionalRules) {
|
|
3014
|
+
if (!ruleMap.has(rule.id)) {
|
|
3015
|
+
ruleMap.set(rule.id, rule);
|
|
3016
|
+
rules.push(rule);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
const sarifResults = [];
|
|
3021
|
+
for (const secret of result.secrets) {
|
|
3022
|
+
const pattern = secret.pattern;
|
|
3023
|
+
const ruleIndex = rules.findIndex((r) => r.id === pattern.id);
|
|
3024
|
+
if (ruleIndex === -1) continue;
|
|
3025
|
+
const { level } = mapSeverity(pattern.severity);
|
|
3026
|
+
const sarifResult = {
|
|
3027
|
+
ruleId: pattern.id,
|
|
3028
|
+
ruleIndex,
|
|
3029
|
+
level,
|
|
3030
|
+
message: {
|
|
3031
|
+
text: `Potential secret detected: ${pattern.name}. ${pattern.description}`
|
|
3032
|
+
},
|
|
3033
|
+
locations: [
|
|
3034
|
+
{
|
|
3035
|
+
physicalLocation: {
|
|
3036
|
+
artifactLocation: {
|
|
3037
|
+
uri: secret.file
|
|
3038
|
+
},
|
|
3039
|
+
region: {
|
|
3040
|
+
startLine: secret.line,
|
|
3041
|
+
startColumn: secret.column
|
|
3042
|
+
}
|
|
3043
|
+
},
|
|
3044
|
+
message: {
|
|
3045
|
+
text: pattern.remediation
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
],
|
|
3049
|
+
properties: {
|
|
3050
|
+
confidence: secret.confidence,
|
|
3051
|
+
category: pattern.category
|
|
3052
|
+
}
|
|
3053
|
+
};
|
|
3054
|
+
sarifResults.push(sarifResult);
|
|
3055
|
+
}
|
|
3056
|
+
const document = {
|
|
3057
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
3058
|
+
version: "2.1.0",
|
|
3059
|
+
runs: [
|
|
3060
|
+
{
|
|
3061
|
+
tool: {
|
|
3062
|
+
driver: {
|
|
3063
|
+
name: "ultraenv",
|
|
3064
|
+
version: toolVersion,
|
|
3065
|
+
informationUri: "https://github.com/ultraenv/ultraenv",
|
|
3066
|
+
rules
|
|
3067
|
+
}
|
|
3068
|
+
},
|
|
3069
|
+
results: sarifResults,
|
|
3070
|
+
invocations: [
|
|
3071
|
+
{
|
|
3072
|
+
executionSuccessful: true,
|
|
3073
|
+
startTimeUtc: result.timestamp,
|
|
3074
|
+
endTimeUtc: (/* @__PURE__ */ new Date()).toISOString()
|
|
3075
|
+
}
|
|
3076
|
+
]
|
|
3077
|
+
}
|
|
3078
|
+
]
|
|
3079
|
+
};
|
|
3080
|
+
return JSON.stringify(document, null, 2);
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
// src/index.ts
|
|
3084
|
+
var index_default = { version: VERSION };
|
|
3085
|
+
export {
|
|
3086
|
+
ConfigError,
|
|
3087
|
+
EncryptionError,
|
|
3088
|
+
FileSystemError,
|
|
3089
|
+
InterpolationError,
|
|
3090
|
+
ParseError,
|
|
3091
|
+
ScanError,
|
|
3092
|
+
SecureBuffer,
|
|
3093
|
+
SecureString,
|
|
3094
|
+
UltraenvError,
|
|
3095
|
+
ValidationError as UltraenvValidationError,
|
|
3096
|
+
VERSION,
|
|
3097
|
+
VaultError,
|
|
3098
|
+
addCustomPattern,
|
|
3099
|
+
compareEnvironments,
|
|
3100
|
+
compareSync,
|
|
3101
|
+
compareValues,
|
|
3102
|
+
computeIntegrity,
|
|
3103
|
+
computeVaultChecksum,
|
|
3104
|
+
copyFile,
|
|
3105
|
+
createEnvironment,
|
|
3106
|
+
createSecureString,
|
|
3107
|
+
createSecureStringFromBuffer,
|
|
3108
|
+
createSyncWatcher,
|
|
3109
|
+
createTypegenWatcher,
|
|
3110
|
+
createUltraenvPlugin,
|
|
3111
|
+
createWatcher,
|
|
3112
|
+
decryptEnvironment,
|
|
3113
|
+
decryptValue,
|
|
3114
|
+
index_default as default,
|
|
3115
|
+
defineEnv,
|
|
3116
|
+
deriveEnvironmentKey,
|
|
3117
|
+
detectHighEntropyStrings,
|
|
3118
|
+
discoverEnvironments,
|
|
3119
|
+
duplicateEnvironment,
|
|
3120
|
+
encryptEnvironment,
|
|
3121
|
+
encryptValue,
|
|
3122
|
+
ensureDir,
|
|
3123
|
+
exists,
|
|
3124
|
+
expandVariables,
|
|
3125
|
+
findConfig,
|
|
3126
|
+
findUp,
|
|
3127
|
+
formatComparison,
|
|
3128
|
+
formatKey,
|
|
3129
|
+
formatScanResult,
|
|
3130
|
+
generateDeclaration,
|
|
3131
|
+
generateExampleContent,
|
|
3132
|
+
generateExampleFile,
|
|
3133
|
+
generateJsonSchema,
|
|
3134
|
+
generateKeysFile,
|
|
3135
|
+
generateMasterKey,
|
|
3136
|
+
generateModule,
|
|
3137
|
+
getActiveEnvironment,
|
|
3138
|
+
getAllPresets,
|
|
3139
|
+
getEnvironmentData,
|
|
3140
|
+
getPreset,
|
|
3141
|
+
getVaultEnvironments,
|
|
3142
|
+
hasPreset,
|
|
3143
|
+
healthCheck,
|
|
3144
|
+
healthCheckRoute,
|
|
3145
|
+
isDirectory,
|
|
3146
|
+
isEncryptedValue,
|
|
3147
|
+
isFile,
|
|
3148
|
+
isHighEntropy,
|
|
3149
|
+
isUltraenvError,
|
|
3150
|
+
isValidKeyFormat,
|
|
3151
|
+
listEnvironments,
|
|
3152
|
+
listFiles,
|
|
3153
|
+
listPresets,
|
|
3154
|
+
liveCheck,
|
|
3155
|
+
load,
|
|
3156
|
+
loadConfig,
|
|
3157
|
+
loadSync,
|
|
3158
|
+
loadWithResult,
|
|
3159
|
+
loadWithResultSync,
|
|
3160
|
+
maskKey,
|
|
3161
|
+
maskValue as maskSecretValue,
|
|
3162
|
+
matchPatterns,
|
|
3163
|
+
mergeCascade,
|
|
3164
|
+
needsUpdate,
|
|
3165
|
+
parseEnvFile,
|
|
3166
|
+
parseKey,
|
|
3167
|
+
parseKeysFile,
|
|
3168
|
+
parseVaultFile,
|
|
3169
|
+
readFile,
|
|
3170
|
+
readFileSync,
|
|
3171
|
+
readVaultFile,
|
|
3172
|
+
readinessCheck,
|
|
3173
|
+
registerPreset,
|
|
3174
|
+
removeCustomPattern,
|
|
3175
|
+
removeEnvironment,
|
|
3176
|
+
removeFile,
|
|
3177
|
+
reportError,
|
|
3178
|
+
reportError2 as reportErrorJson,
|
|
3179
|
+
reportInfo,
|
|
3180
|
+
reportScanResult2 as reportScanResultJson,
|
|
3181
|
+
reportScanResult3 as reportScanResultSarif,
|
|
3182
|
+
reportScanResult as reportScanResultTerminal,
|
|
3183
|
+
reportSuccess,
|
|
3184
|
+
reportValidation,
|
|
3185
|
+
reportValidation2 as reportValidationJson,
|
|
3186
|
+
reportWarning,
|
|
3187
|
+
resetPatterns,
|
|
3188
|
+
resolveCascade,
|
|
3189
|
+
rotateKey,
|
|
3190
|
+
scan,
|
|
3191
|
+
scanDiff,
|
|
3192
|
+
scanFiles,
|
|
3193
|
+
scanGitHistory,
|
|
3194
|
+
scanLineForEntropy,
|
|
3195
|
+
scanStagedFiles,
|
|
3196
|
+
secureCompare,
|
|
3197
|
+
serializeVaultFile,
|
|
3198
|
+
shannonEntropy,
|
|
3199
|
+
switchEnvironment,
|
|
3200
|
+
t,
|
|
3201
|
+
tryDefineEnv,
|
|
3202
|
+
ultraenvMiddleware,
|
|
3203
|
+
ultraenvPlugin,
|
|
3204
|
+
unregisterPreset,
|
|
3205
|
+
validate,
|
|
3206
|
+
validateAllEnvironments,
|
|
3207
|
+
verifyIntegrity,
|
|
3208
|
+
verifyVaultChecksum,
|
|
3209
|
+
wipeString,
|
|
3210
|
+
writeFile,
|
|
3211
|
+
writeVaultFile
|
|
3212
|
+
};
|