@spfn/core 0.1.0-alpha.88 → 0.2.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -466
- package/dist/boss-DI1r4kTS.d.ts +244 -0
- package/dist/cache/index.d.ts +13 -33
- package/dist/cache/index.js +14 -703
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +214 -17
- package/dist/codegen/index.js +231 -1420
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +1227 -0
- package/dist/config/index.js +273 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +741 -59
- package/dist/db/index.js +1063 -1226
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +658 -308
- package/dist/env/index.js +503 -928
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +87 -0
- package/dist/env/loader.js +70 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/errors/index.d.ts +417 -29
- package/dist/errors/index.js +359 -98
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +41 -0
- package/dist/event/index.js +131 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/sse/client.d.ts +82 -0
- package/dist/event/sse/client.js +115 -0
- package/dist/event/sse/client.js.map +1 -0
- package/dist/event/sse/index.d.ts +40 -0
- package/dist/event/sse/index.js +92 -0
- package/dist/event/sse/index.js.map +1 -0
- package/dist/job/index.d.ts +218 -0
- package/dist/job/index.js +410 -0
- package/dist/job/index.js.map +1 -0
- package/dist/logger/index.d.ts +20 -79
- package/dist/logger/index.js +82 -387
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +102 -20
- package/dist/middleware/index.js +51 -705
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +120 -0
- package/dist/nextjs/index.js +448 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +335 -262
- package/dist/nextjs/server.js +637 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/route/index.d.ts +879 -25
- package/dist/route/index.js +697 -1271
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +9 -0
- package/dist/route/types.js +3 -0
- package/dist/route/types.js.map +1 -0
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +345 -64
- package/dist/server/index.js +1174 -3233
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/types-BGl4QL1w.d.ts +77 -0
- package/dist/types-BOPTApC2.d.ts +245 -0
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +346 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +477 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +116 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +241 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +307 -0
- package/package.json +68 -48
- package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
- package/dist/client/index.d.ts +0 -358
- package/dist/client/index.js +0 -357
- package/dist/client/index.js.map +0 -1
- package/dist/client/nextjs/index.js +0 -371
- package/dist/client/nextjs/index.js.map +0 -1
- package/dist/codegen/generators/index.d.ts +0 -19
- package/dist/codegen/generators/index.js +0 -1404
- package/dist/codegen/generators/index.js.map +0 -1
- package/dist/database-errors-BNNmLTJE.d.ts +0 -86
- package/dist/events/index.d.ts +0 -183
- package/dist/events/index.js +0 -77
- package/dist/events/index.js.map +0 -1
- package/dist/index-DHiAqhKv.d.ts +0 -101
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -3674
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -121
- package/dist/types/index.js +0 -38
- package/dist/types/index.js.map +0 -1
- package/dist/types-BXibIEyj.d.ts +0 -60
package/dist/env/index.js
CHANGED
|
@@ -1,1010 +1,585 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, createWriteStream, statSync, readdirSync, renameSync, unlinkSync, accessSync, constants, writeFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
1
|
+
import { logger } from '@spfn/core/logger';
|
|
4
2
|
|
|
5
|
-
// src/env/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
info: 1,
|
|
11
|
-
warn: 2,
|
|
12
|
-
error: 3,
|
|
13
|
-
fatal: 4
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// src/logger/formatters.ts
|
|
17
|
-
var SENSITIVE_KEYS = [
|
|
18
|
-
"password",
|
|
19
|
-
"passwd",
|
|
20
|
-
"pwd",
|
|
21
|
-
"secret",
|
|
22
|
-
"token",
|
|
23
|
-
"apikey",
|
|
24
|
-
"api_key",
|
|
25
|
-
"accesstoken",
|
|
26
|
-
"access_token",
|
|
27
|
-
"refreshtoken",
|
|
28
|
-
"refresh_token",
|
|
29
|
-
"authorization",
|
|
30
|
-
"auth",
|
|
31
|
-
"cookie",
|
|
32
|
-
"session",
|
|
33
|
-
"sessionid",
|
|
34
|
-
"session_id",
|
|
35
|
-
"privatekey",
|
|
36
|
-
"private_key",
|
|
37
|
-
"creditcard",
|
|
38
|
-
"credit_card",
|
|
39
|
-
"cardnumber",
|
|
40
|
-
"card_number",
|
|
41
|
-
"cvv",
|
|
42
|
-
"ssn",
|
|
43
|
-
"pin"
|
|
44
|
-
];
|
|
45
|
-
var MASKED_VALUE = "***MASKED***";
|
|
46
|
-
function isSensitiveKey(key) {
|
|
47
|
-
const lowerKey = key.toLowerCase();
|
|
48
|
-
return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
|
|
49
|
-
}
|
|
50
|
-
function maskSensitiveData(data) {
|
|
51
|
-
if (data === null || data === void 0) {
|
|
52
|
-
return data;
|
|
53
|
-
}
|
|
54
|
-
if (Array.isArray(data)) {
|
|
55
|
-
return data.map((item) => maskSensitiveData(item));
|
|
56
|
-
}
|
|
57
|
-
if (typeof data === "object") {
|
|
58
|
-
const masked = {};
|
|
59
|
-
for (const [key, value] of Object.entries(data)) {
|
|
60
|
-
if (isSensitiveKey(key)) {
|
|
61
|
-
masked[key] = MASKED_VALUE;
|
|
62
|
-
} else if (typeof value === "object" && value !== null) {
|
|
63
|
-
masked[key] = maskSensitiveData(value);
|
|
64
|
-
} else {
|
|
65
|
-
masked[key] = value;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return masked;
|
|
69
|
-
}
|
|
70
|
-
return data;
|
|
71
|
-
}
|
|
72
|
-
var COLORS = {
|
|
73
|
-
reset: "\x1B[0m",
|
|
74
|
-
bright: "\x1B[1m",
|
|
75
|
-
dim: "\x1B[2m",
|
|
76
|
-
// 로그 레벨 컬러
|
|
77
|
-
debug: "\x1B[36m",
|
|
78
|
-
// cyan
|
|
79
|
-
info: "\x1B[32m",
|
|
80
|
-
// green
|
|
81
|
-
warn: "\x1B[33m",
|
|
82
|
-
// yellow
|
|
83
|
-
error: "\x1B[31m",
|
|
84
|
-
// red
|
|
85
|
-
fatal: "\x1B[35m",
|
|
86
|
-
// magenta
|
|
87
|
-
// 추가 컬러
|
|
88
|
-
gray: "\x1B[90m"
|
|
89
|
-
};
|
|
90
|
-
function formatTimestamp(date) {
|
|
91
|
-
return date.toISOString();
|
|
92
|
-
}
|
|
93
|
-
function formatTimestampHuman(date) {
|
|
94
|
-
const year = date.getFullYear();
|
|
95
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
96
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
97
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
98
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
99
|
-
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
100
|
-
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
101
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
102
|
-
}
|
|
103
|
-
function formatError(error) {
|
|
104
|
-
const lines = [];
|
|
105
|
-
lines.push(`${error.name}: ${error.message}`);
|
|
106
|
-
if (error.stack) {
|
|
107
|
-
const stackLines = error.stack.split("\n").slice(1);
|
|
108
|
-
lines.push(...stackLines);
|
|
109
|
-
}
|
|
110
|
-
return lines.join("\n");
|
|
111
|
-
}
|
|
112
|
-
function formatConsole(metadata, colorize = true) {
|
|
113
|
-
const parts = [];
|
|
114
|
-
const timestamp = formatTimestampHuman(metadata.timestamp);
|
|
115
|
-
if (colorize) {
|
|
116
|
-
parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
|
|
117
|
-
} else {
|
|
118
|
-
parts.push(`[${timestamp}]`);
|
|
119
|
-
}
|
|
120
|
-
if (metadata.module) {
|
|
121
|
-
if (colorize) {
|
|
122
|
-
parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
|
|
123
|
-
} else {
|
|
124
|
-
parts.push(`[module=${metadata.module}]`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
128
|
-
Object.entries(metadata.context).forEach(([key, value]) => {
|
|
129
|
-
let valueStr;
|
|
130
|
-
if (typeof value === "string") {
|
|
131
|
-
valueStr = value;
|
|
132
|
-
} else if (typeof value === "object" && value !== null) {
|
|
133
|
-
try {
|
|
134
|
-
valueStr = JSON.stringify(value);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
valueStr = "[circular]";
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
valueStr = String(value);
|
|
140
|
-
}
|
|
141
|
-
if (colorize) {
|
|
142
|
-
parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
|
|
143
|
-
} else {
|
|
144
|
-
parts.push(`[${key}=${valueStr}]`);
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
const levelStr = metadata.level.toUpperCase();
|
|
149
|
-
if (colorize) {
|
|
150
|
-
const color = COLORS[metadata.level];
|
|
151
|
-
parts.push(`${color}(${levelStr})${COLORS.reset}:`);
|
|
152
|
-
} else {
|
|
153
|
-
parts.push(`(${levelStr}):`);
|
|
154
|
-
}
|
|
155
|
-
if (colorize) {
|
|
156
|
-
parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
|
|
157
|
-
} else {
|
|
158
|
-
parts.push(metadata.message);
|
|
159
|
-
}
|
|
160
|
-
let output = parts.join(" ");
|
|
161
|
-
if (metadata.error) {
|
|
162
|
-
output += "\n" + formatError(metadata.error);
|
|
163
|
-
}
|
|
164
|
-
return output;
|
|
165
|
-
}
|
|
166
|
-
function formatJSON(metadata) {
|
|
167
|
-
const obj = {
|
|
168
|
-
timestamp: formatTimestamp(metadata.timestamp),
|
|
169
|
-
level: metadata.level,
|
|
170
|
-
message: metadata.message
|
|
171
|
-
};
|
|
172
|
-
if (metadata.module) {
|
|
173
|
-
obj.module = metadata.module;
|
|
174
|
-
}
|
|
175
|
-
if (metadata.context) {
|
|
176
|
-
obj.context = metadata.context;
|
|
177
|
-
}
|
|
178
|
-
if (metadata.error) {
|
|
179
|
-
obj.error = {
|
|
180
|
-
name: metadata.error.name,
|
|
181
|
-
message: metadata.error.message,
|
|
182
|
-
stack: metadata.error.stack
|
|
183
|
-
};
|
|
3
|
+
// src/env/validator.ts
|
|
4
|
+
function parseString(value) {
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (trimmed.length === 0) {
|
|
7
|
+
throw new Error("Value cannot be empty");
|
|
184
8
|
}
|
|
185
|
-
return
|
|
9
|
+
return trimmed;
|
|
186
10
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.config = config;
|
|
194
|
-
this.module = config.module;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Get current log level
|
|
198
|
-
*/
|
|
199
|
-
get level() {
|
|
200
|
-
return this.config.level;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Create child logger (per module)
|
|
204
|
-
*/
|
|
205
|
-
child(module) {
|
|
206
|
-
return new _Logger({
|
|
207
|
-
...this.config,
|
|
208
|
-
module
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Debug log
|
|
213
|
-
*/
|
|
214
|
-
debug(message, context) {
|
|
215
|
-
this.log("debug", message, void 0, context);
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Info log
|
|
219
|
-
*/
|
|
220
|
-
info(message, context) {
|
|
221
|
-
this.log("info", message, void 0, context);
|
|
222
|
-
}
|
|
223
|
-
warn(message, errorOrContext, context) {
|
|
224
|
-
if (errorOrContext instanceof Error) {
|
|
225
|
-
this.log("warn", message, errorOrContext, context);
|
|
226
|
-
} else {
|
|
227
|
-
this.log("warn", message, void 0, errorOrContext);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
error(message, errorOrContext, context) {
|
|
231
|
-
if (errorOrContext instanceof Error) {
|
|
232
|
-
this.log("error", message, errorOrContext, context);
|
|
233
|
-
} else {
|
|
234
|
-
this.log("error", message, void 0, errorOrContext);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
fatal(message, errorOrContext, context) {
|
|
238
|
-
if (errorOrContext instanceof Error) {
|
|
239
|
-
this.log("fatal", message, errorOrContext, context);
|
|
240
|
-
} else {
|
|
241
|
-
this.log("fatal", message, void 0, errorOrContext);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Log processing (internal)
|
|
246
|
-
*/
|
|
247
|
-
log(level, message, error, context) {
|
|
248
|
-
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const metadata = {
|
|
252
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
253
|
-
level,
|
|
254
|
-
message,
|
|
255
|
-
module: this.module,
|
|
256
|
-
error,
|
|
257
|
-
// Mask sensitive information in context to prevent credential leaks
|
|
258
|
-
context: context ? maskSensitiveData(context) : void 0
|
|
259
|
-
};
|
|
260
|
-
this.processTransports(metadata);
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Process Transports
|
|
264
|
-
*/
|
|
265
|
-
processTransports(metadata) {
|
|
266
|
-
const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
|
|
267
|
-
Promise.all(promises).catch((error) => {
|
|
268
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
269
|
-
process.stderr.write(`[Logger] Transport error: ${errorMessage}
|
|
270
|
-
`);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Transport log (error-safe)
|
|
275
|
-
*/
|
|
276
|
-
async safeTransportLog(transport, metadata) {
|
|
277
|
-
try {
|
|
278
|
-
await transport.log(metadata);
|
|
279
|
-
} catch (error) {
|
|
280
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
281
|
-
process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
|
|
282
|
-
`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Close all Transports
|
|
287
|
-
*/
|
|
288
|
-
async close() {
|
|
289
|
-
const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
|
|
290
|
-
await Promise.all(closePromises);
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// src/logger/transports/console.ts
|
|
295
|
-
var ConsoleTransport = class {
|
|
296
|
-
name = "console";
|
|
297
|
-
level;
|
|
298
|
-
enabled;
|
|
299
|
-
colorize;
|
|
300
|
-
constructor(config) {
|
|
301
|
-
this.level = config.level;
|
|
302
|
-
this.enabled = config.enabled;
|
|
303
|
-
this.colorize = config.colorize ?? true;
|
|
304
|
-
}
|
|
305
|
-
async log(metadata) {
|
|
306
|
-
if (!this.enabled) {
|
|
307
|
-
return;
|
|
11
|
+
function createStringParser(options = {}) {
|
|
12
|
+
return (value) => {
|
|
13
|
+
const { minLength, maxLength, pattern, trim = true } = options;
|
|
14
|
+
let result = trim ? value.trim() : value;
|
|
15
|
+
if (result.length === 0) {
|
|
16
|
+
throw new Error("Value cannot be empty");
|
|
308
17
|
}
|
|
309
|
-
if (
|
|
310
|
-
|
|
18
|
+
if (minLength !== void 0 && result.length < minLength) {
|
|
19
|
+
throw new Error(`Must be at least ${minLength} characters long (current: ${result.length})`);
|
|
311
20
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
console.error(message);
|
|
315
|
-
} else {
|
|
316
|
-
console.log(message);
|
|
21
|
+
if (maxLength !== void 0 && result.length > maxLength) {
|
|
22
|
+
throw new Error(`Must be at most ${maxLength} characters long (current: ${result.length})`);
|
|
317
23
|
}
|
|
318
|
-
|
|
319
|
-
};
|
|
320
|
-
var FileTransport = class {
|
|
321
|
-
name = "file";
|
|
322
|
-
level;
|
|
323
|
-
enabled;
|
|
324
|
-
logDir;
|
|
325
|
-
maxFileSize;
|
|
326
|
-
maxFiles;
|
|
327
|
-
currentStream = null;
|
|
328
|
-
currentFilename = null;
|
|
329
|
-
constructor(config) {
|
|
330
|
-
this.level = config.level;
|
|
331
|
-
this.enabled = config.enabled;
|
|
332
|
-
this.logDir = config.logDir;
|
|
333
|
-
this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
|
|
334
|
-
this.maxFiles = config.maxFiles ?? 10;
|
|
335
|
-
if (!existsSync(this.logDir)) {
|
|
336
|
-
mkdirSync(this.logDir, { recursive: true });
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
async log(metadata) {
|
|
340
|
-
if (!this.enabled) {
|
|
341
|
-
return;
|
|
24
|
+
if (pattern && !pattern.test(result)) {
|
|
25
|
+
throw new Error(`Must match pattern ${pattern}`);
|
|
342
26
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
await this.cleanOldFiles();
|
|
351
|
-
} else if (this.currentFilename) {
|
|
352
|
-
await this.checkAndRotateBySize();
|
|
353
|
-
}
|
|
354
|
-
if (this.currentStream) {
|
|
355
|
-
return new Promise((resolve, reject) => {
|
|
356
|
-
this.currentStream.write(message + "\n", "utf-8", (error) => {
|
|
357
|
-
if (error) {
|
|
358
|
-
process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
|
|
359
|
-
`);
|
|
360
|
-
reject(error);
|
|
361
|
-
} else {
|
|
362
|
-
resolve();
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* 스트림 교체 (날짜 변경 시)
|
|
370
|
-
*/
|
|
371
|
-
async rotateStream(filename) {
|
|
372
|
-
if (this.currentStream) {
|
|
373
|
-
await this.closeStream();
|
|
374
|
-
}
|
|
375
|
-
const filepath = join(this.logDir, filename);
|
|
376
|
-
this.currentStream = createWriteStream(filepath, {
|
|
377
|
-
flags: "a",
|
|
378
|
-
// append mode
|
|
379
|
-
encoding: "utf-8"
|
|
380
|
-
});
|
|
381
|
-
this.currentFilename = filename;
|
|
382
|
-
this.currentStream.on("error", (error) => {
|
|
383
|
-
process.stderr.write(`[FileTransport] Stream error: ${error.message}
|
|
384
|
-
`);
|
|
385
|
-
this.currentStream = null;
|
|
386
|
-
this.currentFilename = null;
|
|
387
|
-
});
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function parseBoolean(value) {
|
|
31
|
+
const normalized = value.toLowerCase().trim();
|
|
32
|
+
if (["true", "1", "yes"].includes(normalized)) {
|
|
33
|
+
return true;
|
|
388
34
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
*/
|
|
392
|
-
async closeStream() {
|
|
393
|
-
if (!this.currentStream) {
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
return new Promise((resolve, reject) => {
|
|
397
|
-
this.currentStream.end((error) => {
|
|
398
|
-
if (error) {
|
|
399
|
-
reject(error);
|
|
400
|
-
} else {
|
|
401
|
-
this.currentStream = null;
|
|
402
|
-
this.currentFilename = null;
|
|
403
|
-
resolve();
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
});
|
|
35
|
+
if (["false", "0", "no"].includes(normalized)) {
|
|
36
|
+
return false;
|
|
407
37
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (!existsSync(filepath)) {
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
try {
|
|
420
|
-
const stats = statSync(filepath);
|
|
421
|
-
if (stats.size >= this.maxFileSize) {
|
|
422
|
-
await this.rotateBySize();
|
|
423
|
-
}
|
|
424
|
-
} catch (error) {
|
|
425
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
426
|
-
process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage}
|
|
427
|
-
`);
|
|
428
|
-
}
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Must be a boolean value (true/false, 1/0, yes/no), got: ${value}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
function parseNumber(value, options = {}) {
|
|
43
|
+
const { min, max, integer = false } = options;
|
|
44
|
+
if (value.trim() === "") {
|
|
45
|
+
throw new Error("Value cannot be empty");
|
|
429
46
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
*/
|
|
434
|
-
async rotateBySize() {
|
|
435
|
-
if (!this.currentFilename) {
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
await this.closeStream();
|
|
439
|
-
const baseName = this.currentFilename.replace(/\.log$/, "");
|
|
440
|
-
const files = readdirSync(this.logDir);
|
|
441
|
-
const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse();
|
|
442
|
-
for (const file of relatedFiles) {
|
|
443
|
-
const match = file.match(/\.(\d+)\.log$/);
|
|
444
|
-
if (match) {
|
|
445
|
-
const oldNum = parseInt(match[1], 10);
|
|
446
|
-
const newNum = oldNum + 1;
|
|
447
|
-
const oldPath = join(this.logDir, file);
|
|
448
|
-
const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`);
|
|
449
|
-
try {
|
|
450
|
-
renameSync(oldPath, newPath2);
|
|
451
|
-
} catch (error) {
|
|
452
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
453
|
-
process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage}
|
|
454
|
-
`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
const currentPath = join(this.logDir, this.currentFilename);
|
|
459
|
-
const newPath = join(this.logDir, `${baseName}.1.log`);
|
|
460
|
-
try {
|
|
461
|
-
if (existsSync(currentPath)) {
|
|
462
|
-
renameSync(currentPath, newPath);
|
|
463
|
-
}
|
|
464
|
-
} catch (error) {
|
|
465
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
466
|
-
process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage}
|
|
467
|
-
`);
|
|
468
|
-
}
|
|
469
|
-
await this.rotateStream(this.currentFilename);
|
|
47
|
+
const num = Number(value);
|
|
48
|
+
if (isNaN(num)) {
|
|
49
|
+
throw new Error(`Must be a valid number, got: ${value}`);
|
|
470
50
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
* maxFiles 개수를 초과하는 로그 파일 삭제
|
|
474
|
-
*/
|
|
475
|
-
async cleanOldFiles() {
|
|
476
|
-
try {
|
|
477
|
-
if (!existsSync(this.logDir)) {
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const files = readdirSync(this.logDir);
|
|
481
|
-
const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => {
|
|
482
|
-
const filepath = join(this.logDir, file);
|
|
483
|
-
const stats = statSync(filepath);
|
|
484
|
-
return { file, mtime: stats.mtime };
|
|
485
|
-
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
486
|
-
if (logFiles.length > this.maxFiles) {
|
|
487
|
-
const filesToDelete = logFiles.slice(this.maxFiles);
|
|
488
|
-
for (const { file } of filesToDelete) {
|
|
489
|
-
const filepath = join(this.logDir, file);
|
|
490
|
-
try {
|
|
491
|
-
unlinkSync(filepath);
|
|
492
|
-
} catch (error) {
|
|
493
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
494
|
-
process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage}
|
|
495
|
-
`);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
} catch (error) {
|
|
500
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
501
|
-
process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
|
|
502
|
-
`);
|
|
503
|
-
}
|
|
51
|
+
if (integer && !Number.isInteger(num)) {
|
|
52
|
+
throw new Error(`Must be an integer, got: ${value}`);
|
|
504
53
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
*/
|
|
508
|
-
getLogFilename(date) {
|
|
509
|
-
const year = date.getFullYear();
|
|
510
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
511
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
512
|
-
return `${year}-${month}-${day}.log`;
|
|
54
|
+
if (min !== void 0 && num < min) {
|
|
55
|
+
throw new Error(`Must be at least ${min}, got: ${num}`);
|
|
513
56
|
}
|
|
514
|
-
|
|
515
|
-
|
|
57
|
+
if (max !== void 0 && num > max) {
|
|
58
|
+
throw new Error(`Must be at most ${max}, got: ${num}`);
|
|
516
59
|
}
|
|
517
|
-
|
|
518
|
-
function isFileLoggingEnabled() {
|
|
519
|
-
return process.env.LOGGER_FILE_ENABLED === "true";
|
|
60
|
+
return num;
|
|
520
61
|
}
|
|
521
|
-
function
|
|
522
|
-
|
|
523
|
-
const isDevelopment = process.env.NODE_ENV === "development";
|
|
524
|
-
if (isDevelopment) {
|
|
525
|
-
return "debug";
|
|
526
|
-
}
|
|
527
|
-
if (isProduction) {
|
|
528
|
-
return "info";
|
|
529
|
-
}
|
|
530
|
-
return "warn";
|
|
62
|
+
function createNumberParser(options = {}) {
|
|
63
|
+
return (value) => parseNumber(value, options);
|
|
531
64
|
}
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
return {
|
|
535
|
-
level: "debug",
|
|
536
|
-
enabled: true,
|
|
537
|
-
colorize: !isProduction
|
|
538
|
-
// Dev: colored output, Production: plain text
|
|
539
|
-
};
|
|
65
|
+
function parseInteger(value, options = {}) {
|
|
66
|
+
return parseNumber(value, { ...options, integer: true });
|
|
540
67
|
}
|
|
541
|
-
function
|
|
542
|
-
|
|
543
|
-
return {
|
|
544
|
-
level: "info",
|
|
545
|
-
enabled: isProduction,
|
|
546
|
-
// File logging in production only
|
|
547
|
-
logDir: process.env.LOG_DIR || "./logs",
|
|
548
|
-
maxFileSize: 10 * 1024 * 1024,
|
|
549
|
-
// 10MB
|
|
550
|
-
maxFiles: 10
|
|
551
|
-
};
|
|
68
|
+
function parseDecimal(value, options = {}) {
|
|
69
|
+
return parseNumber(value, { ...options, integer: false });
|
|
552
70
|
}
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
71
|
+
function parseUrl(value, options = {}) {
|
|
72
|
+
const { protocol = "any" } = options;
|
|
73
|
+
let url;
|
|
74
|
+
try {
|
|
75
|
+
url = new URL(value);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof TypeError) {
|
|
78
|
+
throw new Error(`Invalid URL: ${value}`);
|
|
560
79
|
}
|
|
80
|
+
throw error;
|
|
561
81
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
} catch {
|
|
565
|
-
throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
|
|
82
|
+
if (protocol === "http" && url.protocol !== "http:") {
|
|
83
|
+
throw new Error(`URL must use HTTP protocol, got ${url.protocol}`);
|
|
566
84
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
writeFileSync(testFile, "test", "utf-8");
|
|
570
|
-
unlinkSync(testFile);
|
|
571
|
-
} catch (error) {
|
|
572
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
573
|
-
throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
|
|
85
|
+
if (protocol === "https" && url.protocol !== "https:") {
|
|
86
|
+
throw new Error(`URL must use HTTPS protocol, got ${url.protocol}`);
|
|
574
87
|
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function createUrlParser(protocol = "any") {
|
|
91
|
+
return (value) => parseUrl(value, { protocol });
|
|
575
92
|
}
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
|
|
93
|
+
function parsePostgresUrl(value) {
|
|
94
|
+
let url;
|
|
95
|
+
try {
|
|
96
|
+
url = new URL(value);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof TypeError) {
|
|
99
|
+
throw new Error(`Invalid PostgreSQL URL: ${value}`);
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
579
102
|
}
|
|
580
|
-
|
|
581
|
-
if (!logDir) {
|
|
103
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
|
|
582
104
|
throw new Error(
|
|
583
|
-
|
|
105
|
+
`Must be a PostgreSQL URL (postgres:// or postgresql://), got ${url.protocol}`
|
|
584
106
|
);
|
|
585
107
|
}
|
|
586
|
-
|
|
108
|
+
return value;
|
|
587
109
|
}
|
|
588
|
-
function
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
110
|
+
function parseRedisUrl(value) {
|
|
111
|
+
let url;
|
|
112
|
+
try {
|
|
113
|
+
url = new URL(value);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof TypeError) {
|
|
116
|
+
throw new Error(`Invalid Redis URL: ${value}`);
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
592
119
|
}
|
|
593
|
-
if (
|
|
120
|
+
if (url.protocol !== "redis:" && url.protocol !== "rediss:") {
|
|
594
121
|
throw new Error(
|
|
595
|
-
`
|
|
122
|
+
`Must be a Redis URL (redis:// or rediss://), got ${url.protocol}`
|
|
596
123
|
);
|
|
597
124
|
}
|
|
125
|
+
return value;
|
|
598
126
|
}
|
|
599
|
-
function
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if (!smtpPort) missingFields.push("SMTP_PORT");
|
|
611
|
-
if (!emailFrom) missingFields.push("EMAIL_FROM");
|
|
612
|
-
if (!emailTo) missingFields.push("EMAIL_TO");
|
|
613
|
-
if (missingFields.length > 0) {
|
|
614
|
-
throw new Error(
|
|
615
|
-
`Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
|
|
616
|
-
);
|
|
127
|
+
function parseEnum(value, allowed, caseInsensitive = false) {
|
|
128
|
+
if (caseInsensitive) {
|
|
129
|
+
const normalizedValue = value.toLowerCase();
|
|
130
|
+
const normalizedAllowed = allowed.map((v) => v.toLowerCase());
|
|
131
|
+
const index = normalizedAllowed.indexOf(normalizedValue);
|
|
132
|
+
if (index === -1) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Must be one of [${allowed.join(", ")}], got: ${value}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return allowed[index];
|
|
617
138
|
}
|
|
618
|
-
|
|
619
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
139
|
+
if (!allowed.includes(value)) {
|
|
620
140
|
throw new Error(
|
|
621
|
-
`
|
|
141
|
+
`Must be one of [${allowed.join(", ")}], got: ${value}`
|
|
622
142
|
);
|
|
623
143
|
}
|
|
624
|
-
|
|
625
|
-
if (!emailRegex.test(emailFrom)) {
|
|
626
|
-
throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
|
|
627
|
-
}
|
|
628
|
-
const recipients = emailTo.split(",").map((e) => e.trim());
|
|
629
|
-
for (const email of recipients) {
|
|
630
|
-
if (!emailRegex.test(email)) {
|
|
631
|
-
throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
144
|
+
return value;
|
|
634
145
|
}
|
|
635
|
-
function
|
|
636
|
-
|
|
637
|
-
if (!nodeEnv) {
|
|
638
|
-
process.stderr.write(
|
|
639
|
-
"[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
|
|
640
|
-
);
|
|
641
|
-
}
|
|
146
|
+
function createEnumParser(allowed, caseInsensitive = false) {
|
|
147
|
+
return (value) => parseEnum(value, allowed, caseInsensitive);
|
|
642
148
|
}
|
|
643
|
-
function
|
|
149
|
+
function parseJson(value) {
|
|
644
150
|
try {
|
|
645
|
-
|
|
646
|
-
validateFileConfig();
|
|
647
|
-
validateSlackConfig();
|
|
648
|
-
validateEmailConfig();
|
|
151
|
+
return JSON.parse(value);
|
|
649
152
|
} catch (error) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
throw error;
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Invalid JSON: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
155
|
+
);
|
|
654
156
|
}
|
|
655
157
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
function initializeTransports() {
|
|
659
|
-
const transports = [];
|
|
660
|
-
const consoleConfig = getConsoleConfig();
|
|
661
|
-
transports.push(new ConsoleTransport(consoleConfig));
|
|
662
|
-
const fileConfig = getFileConfig();
|
|
663
|
-
if (fileConfig.enabled) {
|
|
664
|
-
transports.push(new FileTransport(fileConfig));
|
|
665
|
-
}
|
|
666
|
-
return transports;
|
|
667
|
-
}
|
|
668
|
-
function initializeLogger() {
|
|
669
|
-
validateConfig();
|
|
670
|
-
return new Logger({
|
|
671
|
-
level: getDefaultLogLevel(),
|
|
672
|
-
transports: initializeTransports()
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
var logger = initializeLogger();
|
|
676
|
-
|
|
677
|
-
// src/env/config.ts
|
|
678
|
-
var ENV_FILE_PRIORITY = [
|
|
679
|
-
".env",
|
|
680
|
-
// Base configuration (lowest priority)
|
|
681
|
-
".env.{NODE_ENV}",
|
|
682
|
-
// Environment-specific
|
|
683
|
-
".env.local",
|
|
684
|
-
// Local overrides (excluded in test)
|
|
685
|
-
".env.{NODE_ENV}.local"
|
|
686
|
-
// Local environment-specific (highest priority)
|
|
687
|
-
];
|
|
688
|
-
var TEST_ONLY_FILES = [
|
|
689
|
-
".env.test",
|
|
690
|
-
".env.test.local"
|
|
691
|
-
];
|
|
692
|
-
|
|
693
|
-
// src/env/loader.ts
|
|
694
|
-
var envLogger = logger.child("environment");
|
|
695
|
-
var environmentLoaded = false;
|
|
696
|
-
var cachedLoadResult;
|
|
697
|
-
function buildFileList(basePath, nodeEnv) {
|
|
698
|
-
const files = [];
|
|
699
|
-
if (!nodeEnv) {
|
|
700
|
-
files.push(join(basePath, ".env"));
|
|
701
|
-
files.push(join(basePath, ".env.local"));
|
|
702
|
-
return files;
|
|
703
|
-
}
|
|
704
|
-
for (const pattern of ENV_FILE_PRIORITY) {
|
|
705
|
-
const fileName = pattern.replace("{NODE_ENV}", nodeEnv);
|
|
706
|
-
if (nodeEnv === "test" && fileName === ".env.local") {
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
if (nodeEnv === "local" && pattern === ".env.local") {
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
if (nodeEnv !== "test" && TEST_ONLY_FILES.includes(fileName)) {
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
files.push(join(basePath, fileName));
|
|
716
|
-
}
|
|
717
|
-
return files;
|
|
158
|
+
function createJsonParser() {
|
|
159
|
+
return (value) => parseJson(value);
|
|
718
160
|
}
|
|
719
|
-
function
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
envLogger.debug("Environment file loaded successfully", {
|
|
744
|
-
path: filePath,
|
|
745
|
-
variables: Object.keys(parsed),
|
|
746
|
-
count: Object.keys(parsed).length
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
return { success: true, parsed };
|
|
750
|
-
} catch (error) {
|
|
751
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
752
|
-
envLogger.error("Error loading environment file", {
|
|
753
|
-
path: filePath,
|
|
754
|
-
error: message
|
|
161
|
+
function parseArray(value, options = {}) {
|
|
162
|
+
const { separator = ",", trim = true, filter } = options;
|
|
163
|
+
if (value.trim() === "") {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
let items = value.split(separator);
|
|
167
|
+
if (trim) {
|
|
168
|
+
items = items.map((item) => item.trim());
|
|
169
|
+
}
|
|
170
|
+
if (filter) {
|
|
171
|
+
items = items.filter(filter);
|
|
172
|
+
}
|
|
173
|
+
return items;
|
|
174
|
+
}
|
|
175
|
+
function createArrayParser(itemParser, options = {}) {
|
|
176
|
+
return (value) => {
|
|
177
|
+
const items = parseArray(value, options);
|
|
178
|
+
return items.map((item, index) => {
|
|
179
|
+
try {
|
|
180
|
+
return itemParser(item);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
throw new Error(`Invalid item at index ${index}: ${message}`);
|
|
184
|
+
}
|
|
755
185
|
});
|
|
756
|
-
|
|
757
|
-
}
|
|
186
|
+
};
|
|
758
187
|
}
|
|
759
|
-
function
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
188
|
+
function calculateEntropy(str) {
|
|
189
|
+
const len = str.length;
|
|
190
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
191
|
+
for (const char of str) {
|
|
192
|
+
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
765
193
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
required
|
|
771
|
-
});
|
|
772
|
-
throw new Error(error);
|
|
773
|
-
}
|
|
774
|
-
if (debug) {
|
|
775
|
-
envLogger.debug("Required environment variables validated", {
|
|
776
|
-
required,
|
|
777
|
-
allPresent: true
|
|
778
|
-
});
|
|
194
|
+
let entropy = 0;
|
|
195
|
+
for (const count of frequencies.values()) {
|
|
196
|
+
const probability = count / len;
|
|
197
|
+
entropy -= probability * Math.log2(probability);
|
|
779
198
|
}
|
|
199
|
+
return entropy;
|
|
780
200
|
}
|
|
781
|
-
function
|
|
201
|
+
function createSecureSecretParser(options = {}) {
|
|
782
202
|
const {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
nodeEnv = process.env.NODE_ENV || "",
|
|
787
|
-
required = [],
|
|
788
|
-
useCache = true
|
|
203
|
+
minLength = 32,
|
|
204
|
+
minUniqueChars = 16,
|
|
205
|
+
minEntropy = 3.5
|
|
789
206
|
} = options;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
failed: [],
|
|
811
|
-
parsed: {},
|
|
812
|
-
warnings: []
|
|
207
|
+
return (value) => {
|
|
208
|
+
const length = value.length;
|
|
209
|
+
const uniqueChars = new Set(value).size;
|
|
210
|
+
const entropy = calculateEntropy(value);
|
|
211
|
+
if (length < minLength) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Secret too short: ${length} characters (minimum: ${minLength})`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (uniqueChars < minUniqueChars) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Secret has low diversity: ${uniqueChars} unique characters (minimum: ${minUniqueChars})`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (entropy < minEntropy) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Secret has low entropy: ${entropy.toFixed(2)} bits/char (minimum: ${minEntropy}). Use a more random secret.`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
813
227
|
};
|
|
814
|
-
const standardFiles = buildFileList(basePath, nodeEnv);
|
|
815
|
-
const allFiles = [...standardFiles, ...customPaths];
|
|
816
|
-
if (debug) {
|
|
817
|
-
envLogger.debug("Environment files to load", {
|
|
818
|
-
standardFiles,
|
|
819
|
-
customPaths,
|
|
820
|
-
total: allFiles.length
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
const reversedFiles = [...allFiles].reverse();
|
|
824
|
-
for (const filePath of reversedFiles) {
|
|
825
|
-
const fileResult = loadSingleFile(filePath, debug);
|
|
826
|
-
if (fileResult.success) {
|
|
827
|
-
result.loaded.push(filePath);
|
|
828
|
-
Object.assign(result.parsed, fileResult.parsed);
|
|
829
|
-
if (fileResult.parsed["NODE_ENV"]) {
|
|
830
|
-
const fileName = filePath.split("/").pop() || filePath;
|
|
831
|
-
result.warnings.push(
|
|
832
|
-
`NODE_ENV found in ${fileName}. It's recommended to set NODE_ENV via CLI (e.g., 'spfn dev', 'spfn build') instead of .env files for consistent environment behavior.`
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
} else if (fileResult.error) {
|
|
836
|
-
result.failed.push({
|
|
837
|
-
path: filePath,
|
|
838
|
-
reason: fileResult.error
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
if (debug || result.loaded.length > 0) {
|
|
843
|
-
envLogger.info("Environment loading complete", {
|
|
844
|
-
loaded: result.loaded.length,
|
|
845
|
-
failed: result.failed.length,
|
|
846
|
-
variables: Object.keys(result.parsed).length,
|
|
847
|
-
files: result.loaded
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
if (required.length > 0) {
|
|
851
|
-
try {
|
|
852
|
-
validateRequiredVars(required, debug);
|
|
853
|
-
} catch (error) {
|
|
854
|
-
result.success = false;
|
|
855
|
-
result.errors = [
|
|
856
|
-
error instanceof Error ? error.message : "Validation failed"
|
|
857
|
-
];
|
|
858
|
-
throw error;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
if (result.warnings.length > 0) {
|
|
862
|
-
for (const warning of result.warnings) {
|
|
863
|
-
envLogger.warn(warning);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
environmentLoaded = true;
|
|
867
|
-
cachedLoadResult = result;
|
|
868
|
-
return result;
|
|
869
228
|
}
|
|
870
|
-
function
|
|
229
|
+
function createPasswordParser(options = {}) {
|
|
871
230
|
const {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
231
|
+
minLength = 8,
|
|
232
|
+
requireUppercase = true,
|
|
233
|
+
requireLowercase = true,
|
|
234
|
+
requireNumber = true,
|
|
235
|
+
requireSpecial = true
|
|
876
236
|
} = options;
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
if (
|
|
880
|
-
|
|
237
|
+
return (value) => {
|
|
238
|
+
const errors = [];
|
|
239
|
+
if (value.length < minLength) {
|
|
240
|
+
errors.push(`Must be at least ${minLength} characters`);
|
|
881
241
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
242
|
+
if (requireUppercase && !/[A-Z]/.test(value)) {
|
|
243
|
+
errors.push("Must contain at least one uppercase letter");
|
|
244
|
+
}
|
|
245
|
+
if (requireLowercase && !/[a-z]/.test(value)) {
|
|
246
|
+
errors.push("Must contain at least one lowercase letter");
|
|
247
|
+
}
|
|
248
|
+
if (requireNumber && !/[0-9]/.test(value)) {
|
|
249
|
+
errors.push("Must contain at least one number");
|
|
250
|
+
}
|
|
251
|
+
if (requireSpecial && !/[^A-Za-z0-9]/.test(value)) {
|
|
252
|
+
errors.push("Must contain at least one special character");
|
|
253
|
+
}
|
|
254
|
+
if (errors.length > 0) {
|
|
255
|
+
throw new Error(`Password validation failed: ${errors.join(", ")}`);
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
};
|
|
889
259
|
}
|
|
890
|
-
function
|
|
891
|
-
return
|
|
260
|
+
function chain(...parsers) {
|
|
261
|
+
return (value) => {
|
|
262
|
+
let result = value;
|
|
263
|
+
for (const parser of parsers) {
|
|
264
|
+
result = parser(result);
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
};
|
|
892
268
|
}
|
|
893
|
-
function
|
|
894
|
-
|
|
895
|
-
|
|
269
|
+
function withFallback(parser, fallback) {
|
|
270
|
+
return (value) => {
|
|
271
|
+
try {
|
|
272
|
+
return parser(value);
|
|
273
|
+
} catch {
|
|
274
|
+
return fallback;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
896
277
|
}
|
|
897
|
-
function
|
|
278
|
+
function optional(parser) {
|
|
279
|
+
return (value) => {
|
|
280
|
+
if (value.trim() === "") {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
return parser(value);
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/env/schema.ts
|
|
288
|
+
function defineEnvSchema(schema) {
|
|
898
289
|
const result = {};
|
|
899
|
-
for (const key
|
|
900
|
-
result[key] =
|
|
290
|
+
for (const key in schema) {
|
|
291
|
+
result[key] = {
|
|
292
|
+
...schema[key],
|
|
293
|
+
key
|
|
294
|
+
};
|
|
901
295
|
}
|
|
902
296
|
return result;
|
|
903
297
|
}
|
|
904
|
-
function
|
|
905
|
-
return
|
|
298
|
+
function envString(options) {
|
|
299
|
+
return {
|
|
300
|
+
...options,
|
|
301
|
+
type: "string"
|
|
302
|
+
};
|
|
906
303
|
}
|
|
907
|
-
function
|
|
908
|
-
|
|
909
|
-
|
|
304
|
+
function envNumber(options) {
|
|
305
|
+
return {
|
|
306
|
+
...options,
|
|
307
|
+
type: "number",
|
|
308
|
+
validator: options.validator || parseNumber
|
|
309
|
+
};
|
|
910
310
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
311
|
+
function envBoolean(options) {
|
|
312
|
+
return {
|
|
313
|
+
...options,
|
|
314
|
+
type: "boolean",
|
|
315
|
+
validator: options.validator || parseBoolean
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function envUrl(options) {
|
|
319
|
+
return {
|
|
320
|
+
...options,
|
|
321
|
+
type: "url"
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function envEnum(allowed, options) {
|
|
325
|
+
return {
|
|
326
|
+
...options,
|
|
327
|
+
type: "enum",
|
|
328
|
+
validator: (val) => {
|
|
329
|
+
if (!allowed.includes(val)) {
|
|
330
|
+
throw new Error(`Must be one of: ${allowed.join(", ")}, got: ${val}`);
|
|
331
|
+
}
|
|
332
|
+
return val;
|
|
922
333
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function envJson(options) {
|
|
337
|
+
return {
|
|
338
|
+
...options,
|
|
339
|
+
type: "json",
|
|
340
|
+
validator: (val) => parseJson(val)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function isClientAccessible(key) {
|
|
344
|
+
return key.startsWith("NEXT_PUBLIC_");
|
|
345
|
+
}
|
|
346
|
+
function isServerOnly(key) {
|
|
347
|
+
return !isClientAccessible(key);
|
|
348
|
+
}
|
|
349
|
+
function isNextjsAccessible(schema) {
|
|
350
|
+
if (schema.nextjs !== void 0) {
|
|
351
|
+
return schema.nextjs;
|
|
926
352
|
}
|
|
353
|
+
return isClientAccessible(schema.key);
|
|
927
354
|
}
|
|
928
|
-
function
|
|
929
|
-
return (
|
|
355
|
+
function isSpfnServerOnly(schema) {
|
|
356
|
+
return !isNextjsAccessible(schema);
|
|
930
357
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
358
|
+
var envLogger = logger.child("@spfn/core:env-registry");
|
|
359
|
+
var EnvRegistry = class {
|
|
360
|
+
schemas = /* @__PURE__ */ new Map();
|
|
361
|
+
hasValidated = false;
|
|
362
|
+
constructor(schemas) {
|
|
363
|
+
if (schemas) {
|
|
364
|
+
this.registerMultiple(schemas);
|
|
365
|
+
}
|
|
935
366
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
367
|
+
/**
|
|
368
|
+
* 스키마 등록
|
|
369
|
+
*/
|
|
370
|
+
register(schema) {
|
|
371
|
+
this.schemas.set(schema.key, schema);
|
|
939
372
|
}
|
|
940
|
-
|
|
941
|
-
|
|
373
|
+
/**
|
|
374
|
+
* 여러 스키마 등록
|
|
375
|
+
*/
|
|
376
|
+
registerMultiple(schemas) {
|
|
377
|
+
for (const [key, schema] of Object.entries(schemas)) {
|
|
378
|
+
this.register({ ...schema, key });
|
|
379
|
+
}
|
|
942
380
|
}
|
|
943
|
-
|
|
944
|
-
|
|
381
|
+
/**
|
|
382
|
+
* 검증 상태 리셋 (테스트용)
|
|
383
|
+
*/
|
|
384
|
+
reset() {
|
|
385
|
+
this.hasValidated = false;
|
|
945
386
|
}
|
|
946
|
-
|
|
947
|
-
|
|
387
|
+
/**
|
|
388
|
+
* 환경변수 원시값 가져오기 (fallback 지원)
|
|
389
|
+
*/
|
|
390
|
+
getRawValue(key, fallbackKeys) {
|
|
391
|
+
let value = process.env[key];
|
|
392
|
+
if (!value && fallbackKeys) {
|
|
393
|
+
for (const fallbackKey of fallbackKeys) {
|
|
394
|
+
value = process.env[fallbackKey];
|
|
395
|
+
if (value) {
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
948
401
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
function parseBoolean(value) {
|
|
959
|
-
const normalized = value.toLowerCase().trim();
|
|
960
|
-
return ["true", "1", "yes"].includes(normalized);
|
|
961
|
-
}
|
|
962
|
-
function validateEnum(value, allowed, caseInsensitive = false) {
|
|
963
|
-
if (caseInsensitive) {
|
|
964
|
-
const normalizedValue = value.toLowerCase();
|
|
965
|
-
const normalizedAllowed = allowed.map((v) => v.toLowerCase());
|
|
966
|
-
return normalizedAllowed.includes(normalizedValue);
|
|
402
|
+
/**
|
|
403
|
+
* 값에 validator 적용
|
|
404
|
+
*/
|
|
405
|
+
applyValidator(value, schema) {
|
|
406
|
+
if (schema.validator) {
|
|
407
|
+
return schema.validator(value);
|
|
408
|
+
}
|
|
409
|
+
return value;
|
|
967
410
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
return
|
|
411
|
+
/**
|
|
412
|
+
* 스키마 검증 수행 (값 읽기 없이)
|
|
413
|
+
*
|
|
414
|
+
* @internal
|
|
415
|
+
*/
|
|
416
|
+
validateSchemas() {
|
|
417
|
+
if (this.hasValidated) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const warnings = [];
|
|
421
|
+
for (const [key, schema] of this.schemas) {
|
|
422
|
+
if (isClientAccessible(key) && schema.sensitive) {
|
|
423
|
+
warnings.push(
|
|
424
|
+
`${key} is marked as sensitive but accessible from client (NEXT_PUBLIC_*). Remove NEXT_PUBLIC_ prefix or unmark as sensitive.`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (warnings.length > 0) {
|
|
429
|
+
envLogger.warn("Environment validation warnings:");
|
|
430
|
+
warnings.forEach((w) => envLogger.warn(` - ${w}`));
|
|
431
|
+
}
|
|
432
|
+
this.hasValidated = true;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* SKIP_ENV_VALIDATION 환경변수 확인
|
|
436
|
+
*/
|
|
437
|
+
shouldSkipValidation() {
|
|
438
|
+
const skip = process.env.SKIP_ENV_VALIDATION;
|
|
439
|
+
return skip === "true" || skip === "1";
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* 실제 접근 시점에 환경변수 값 가져오기 및 검증
|
|
443
|
+
*
|
|
444
|
+
* @internal
|
|
445
|
+
*/
|
|
446
|
+
getAndValidate(key) {
|
|
447
|
+
const schema = this.schemas.get(key);
|
|
448
|
+
if (!schema) {
|
|
449
|
+
return void 0;
|
|
450
|
+
}
|
|
451
|
+
const value = this.getRawValue(key, schema.fallbackKeys);
|
|
452
|
+
if (schema.required && !value && !this.shouldSkipValidation()) {
|
|
453
|
+
const fallbackHint = schema.fallbackKeys ? ` (or ${schema.fallbackKeys.join(", ")})` : "";
|
|
454
|
+
const errorMsg = `${key}${fallbackHint} is required but not set. ${schema.description || ""}`;
|
|
455
|
+
envLogger.error(`Environment validation failed:
|
|
456
|
+
- ${errorMsg}`);
|
|
457
|
+
throw new Error("Environment validation failed");
|
|
458
|
+
}
|
|
459
|
+
if (!value) {
|
|
460
|
+
return schema.default;
|
|
461
|
+
}
|
|
462
|
+
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
463
|
+
const errorMsg = `${key} must be at least ${schema.minLength} characters long (current: ${value.length})`;
|
|
464
|
+
envLogger.error(`Environment validation failed:
|
|
465
|
+
- ${errorMsg}`);
|
|
466
|
+
throw new Error("Environment validation failed");
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
return this.applyValidator(value, schema);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
const errorMsg = `${key} validation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
472
|
+
envLogger.error(`Environment validation failed:
|
|
473
|
+
- ${errorMsg}`);
|
|
474
|
+
throw new Error("Environment validation failed");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* 모든 환경변수를 명시적으로 검증 (SKIP_ENV_VALIDATION 무시)
|
|
479
|
+
*
|
|
480
|
+
* CLI에서 사용하기 위한 메서드로, 모든 required 환경변수를 강제 검증합니다.
|
|
481
|
+
*
|
|
482
|
+
* @returns 검증 결과 (errors, warnings)
|
|
483
|
+
*/
|
|
484
|
+
validateAll() {
|
|
485
|
+
const errors = [];
|
|
486
|
+
const warnings = [];
|
|
487
|
+
for (const [key, schema] of this.schemas) {
|
|
488
|
+
if (isClientAccessible(key) && schema.sensitive) {
|
|
489
|
+
warnings.push({
|
|
490
|
+
key,
|
|
491
|
+
message: `${key} is marked as sensitive but accessible from client (NEXT_PUBLIC_*).`
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const value = this.getRawValue(key, schema.fallbackKeys);
|
|
495
|
+
if (schema.required && !value) {
|
|
496
|
+
const fallbackHint = schema.fallbackKeys ? ` (or ${schema.fallbackKeys.join(", ")})` : "";
|
|
497
|
+
errors.push({
|
|
498
|
+
key,
|
|
499
|
+
message: `${key}${fallbackHint} is required but not set. ${schema.description || ""}`
|
|
500
|
+
});
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (value && schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
504
|
+
errors.push({
|
|
505
|
+
key,
|
|
506
|
+
message: `${key} must be at least ${schema.minLength} characters long (current: ${value.length})`
|
|
507
|
+
});
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (value && schema.validator) {
|
|
511
|
+
try {
|
|
512
|
+
schema.validator(value);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
errors.push({
|
|
515
|
+
key,
|
|
516
|
+
message: `${key} validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { errors, warnings };
|
|
997
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* 환경변수 검증 및 타입 안전한 env 객체 반환
|
|
525
|
+
*
|
|
526
|
+
* Proxy 기반으로 구현되어 실제 환경변수 접근 시점에 값을 읽고 검증합니다.
|
|
527
|
+
* 이를 통해 dotenv 로딩 타이밍과 무관하게 최신 환경변수 값을 가져올 수 있습니다.
|
|
528
|
+
*
|
|
529
|
+
* @returns 검증된 환경변수 객체 (Proxy)
|
|
530
|
+
* @throws {Error} 필수 변수 누락 또는 검증 실패 시
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* ```typescript
|
|
534
|
+
* const registry = createEnvRegistry(schema);
|
|
535
|
+
* const env = registry.validate(); // 스키마만 검증
|
|
536
|
+
* // ... dotenv 로딩 ...
|
|
537
|
+
* console.log(env.DATABASE_URL); // 이 시점에 실제 값 읽기
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
validate() {
|
|
541
|
+
this.validateSchemas();
|
|
542
|
+
return new Proxy({}, {
|
|
543
|
+
get: (_target, prop) => {
|
|
544
|
+
return this.getAndValidate(prop);
|
|
545
|
+
},
|
|
546
|
+
ownKeys: () => {
|
|
547
|
+
return Array.from(this.schemas.keys());
|
|
548
|
+
},
|
|
549
|
+
getOwnPropertyDescriptor: (_target, prop) => {
|
|
550
|
+
if (this.schemas.has(prop)) {
|
|
551
|
+
return {
|
|
552
|
+
enumerable: true,
|
|
553
|
+
configurable: true,
|
|
554
|
+
get: () => this.getAndValidate(prop)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return void 0;
|
|
558
|
+
},
|
|
559
|
+
has: (_target, prop) => {
|
|
560
|
+
return this.schemas.has(prop);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
function createEnvRegistry(schemas) {
|
|
566
|
+
return new EnvRegistry(schemas);
|
|
998
567
|
}
|
|
999
|
-
function
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
568
|
+
function validateAllEnv(registries) {
|
|
569
|
+
const errors = [];
|
|
570
|
+
const warnings = [];
|
|
571
|
+
for (const registry of registries) {
|
|
572
|
+
const result = registry.validateAll();
|
|
573
|
+
errors.push(...result.errors);
|
|
574
|
+
warnings.push(...result.warnings);
|
|
1005
575
|
}
|
|
576
|
+
return {
|
|
577
|
+
valid: errors.length === 0,
|
|
578
|
+
errors,
|
|
579
|
+
warnings
|
|
580
|
+
};
|
|
1006
581
|
}
|
|
1007
582
|
|
|
1008
|
-
export {
|
|
583
|
+
export { EnvRegistry, chain, createArrayParser, createEnumParser, createEnvRegistry, createJsonParser, createNumberParser, createPasswordParser, createSecureSecretParser, createStringParser, createUrlParser, defineEnvSchema, envBoolean, envEnum, envJson, envNumber, envString, envUrl, isClientAccessible, isNextjsAccessible, isServerOnly, isSpfnServerOnly, optional, parseArray, parseBoolean, parseDecimal, parseEnum, parseInteger, parseJson, parseNumber, parsePostgresUrl, parseRedisUrl, parseString, parseUrl, validateAllEnv, withFallback };
|
|
1009
584
|
//# sourceMappingURL=index.js.map
|
|
1010
585
|
//# sourceMappingURL=index.js.map
|