@spfn/core 0.1.0-alpha.88 → 0.2.0-beta.1
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 +1046 -384
- package/dist/boss-D-fGtVgM.d.ts +187 -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 +167 -17
- package/dist/codegen/index.js +76 -1419
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +1191 -0
- package/dist/config/index.js +264 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +728 -59
- package/dist/db/index.js +1028 -1225
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +579 -308
- package/dist/env/index.js +438 -930
- package/dist/env/index.js.map +1 -1
- 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 +108 -0
- package/dist/event/index.js +122 -0
- package/dist/event/index.js.map +1 -0
- package/dist/job/index.d.ts +172 -0
- package/dist/job/index.js +361 -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 +2 -11
- package/dist/middleware/index.js +49 -703
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +120 -0
- package/dist/nextjs/index.js +416 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +288 -262
- package/dist/nextjs/server.js +568 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/route/index.d.ts +667 -25
- package/dist/route/index.js +437 -1287
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +38 -0
- package/dist/route/types.js +3 -0
- package/dist/route/types.js.map +1 -0
- package/dist/server/index.d.ts +201 -67
- package/dist/server/index.js +921 -3182
- package/dist/server/index.js.map +1 -1
- package/dist/types-BGl4QL1w.d.ts +77 -0
- package/dist/types-DRG2XMTR.d.ts +157 -0
- package/package.json +52 -47
- 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,518 @@
|
|
|
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;
|
|
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");
|
|
250
17
|
}
|
|
251
|
-
|
|
252
|
-
|
|
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;
|
|
308
|
-
}
|
|
309
|
-
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
310
|
-
return;
|
|
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;
|
|
342
|
-
}
|
|
343
|
-
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
344
|
-
return;
|
|
24
|
+
if (pattern && !pattern.test(result)) {
|
|
25
|
+
throw new Error(`Must match pattern ${pattern}`);
|
|
345
26
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
}
|
|
765
|
-
}
|
|
766
|
-
if (missing.length > 0) {
|
|
767
|
-
const error = `Required environment variables missing: ${missing.join(", ")}`;
|
|
768
|
-
envLogger.error("Environment validation failed", {
|
|
769
|
-
missing,
|
|
770
|
-
required
|
|
771
|
-
});
|
|
772
|
-
throw new Error(error);
|
|
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);
|
|
773
193
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function withFallback(parser, fallback) {
|
|
270
|
+
return (value) => {
|
|
271
|
+
try {
|
|
272
|
+
return parser(value);
|
|
273
|
+
} catch {
|
|
274
|
+
return fallback;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
892
277
|
}
|
|
893
|
-
function
|
|
894
|
-
|
|
895
|
-
|
|
278
|
+
function optional(parser) {
|
|
279
|
+
return (value) => {
|
|
280
|
+
if (value.trim() === "") {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
return parser(value);
|
|
284
|
+
};
|
|
896
285
|
}
|
|
897
|
-
|
|
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
|
-
|
|
926
|
-
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function envJson(options) {
|
|
337
|
+
return {
|
|
338
|
+
...options,
|
|
339
|
+
type: "json",
|
|
340
|
+
validator: (val) => parseJson(val)
|
|
341
|
+
};
|
|
927
342
|
}
|
|
928
|
-
function
|
|
929
|
-
return (
|
|
343
|
+
function isClientAccessible(key) {
|
|
344
|
+
return key.startsWith("NEXT_PUBLIC_");
|
|
930
345
|
}
|
|
931
|
-
function
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
346
|
+
function isServerOnly(key) {
|
|
347
|
+
return !isClientAccessible(key);
|
|
348
|
+
}
|
|
349
|
+
var envLogger = logger.child("@spfn/core:env-registry");
|
|
350
|
+
var EnvRegistry = class {
|
|
351
|
+
schemas = /* @__PURE__ */ new Map();
|
|
352
|
+
hasValidated = false;
|
|
353
|
+
valueCache = /* @__PURE__ */ new Map();
|
|
354
|
+
constructor(schemas) {
|
|
355
|
+
if (schemas) {
|
|
356
|
+
this.registerMultiple(schemas);
|
|
357
|
+
}
|
|
935
358
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
359
|
+
/**
|
|
360
|
+
* 스키마 등록
|
|
361
|
+
*/
|
|
362
|
+
register(schema) {
|
|
363
|
+
this.schemas.set(schema.key, schema);
|
|
939
364
|
}
|
|
940
|
-
|
|
941
|
-
|
|
365
|
+
/**
|
|
366
|
+
* 여러 스키마 등록
|
|
367
|
+
*/
|
|
368
|
+
registerMultiple(schemas) {
|
|
369
|
+
for (const [key, schema] of Object.entries(schemas)) {
|
|
370
|
+
this.register({ ...schema, key });
|
|
371
|
+
}
|
|
942
372
|
}
|
|
943
|
-
|
|
944
|
-
|
|
373
|
+
/**
|
|
374
|
+
* 캐시 및 검증 상태 리셋 (테스트용)
|
|
375
|
+
*/
|
|
376
|
+
reset() {
|
|
377
|
+
this.valueCache.clear();
|
|
378
|
+
this.hasValidated = false;
|
|
945
379
|
}
|
|
946
|
-
|
|
947
|
-
|
|
380
|
+
/**
|
|
381
|
+
* 환경변수 원시값 가져오기 (fallback 지원)
|
|
382
|
+
*/
|
|
383
|
+
getRawValue(key, fallbackKeys) {
|
|
384
|
+
let value = process.env[key];
|
|
385
|
+
if (!value && fallbackKeys) {
|
|
386
|
+
for (const fallbackKey of fallbackKeys) {
|
|
387
|
+
value = process.env[fallbackKey];
|
|
388
|
+
if (value) {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return value;
|
|
948
394
|
}
|
|
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);
|
|
395
|
+
/**
|
|
396
|
+
* 값에 validator 적용
|
|
397
|
+
*/
|
|
398
|
+
applyValidator(value, schema) {
|
|
399
|
+
if (schema.validator) {
|
|
400
|
+
return schema.validator(value);
|
|
401
|
+
}
|
|
402
|
+
return value;
|
|
967
403
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
function validatePostgresUrl(value) {
|
|
992
|
-
try {
|
|
993
|
-
const url = new URL(value);
|
|
994
|
-
return url.protocol === "postgres:" || url.protocol === "postgresql:";
|
|
995
|
-
} catch {
|
|
996
|
-
return false;
|
|
404
|
+
/**
|
|
405
|
+
* 스키마 검증 수행 (값 읽기 없이)
|
|
406
|
+
*
|
|
407
|
+
* @internal
|
|
408
|
+
*/
|
|
409
|
+
validateSchemas() {
|
|
410
|
+
if (this.hasValidated) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const warnings = [];
|
|
414
|
+
for (const [key, schema] of this.schemas) {
|
|
415
|
+
if (isClientAccessible(key) && schema.sensitive) {
|
|
416
|
+
warnings.push(
|
|
417
|
+
`${key} is marked as sensitive but accessible from client (NEXT_PUBLIC_*). Remove NEXT_PUBLIC_ prefix or unmark as sensitive.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (warnings.length > 0) {
|
|
422
|
+
envLogger.warn("Environment validation warnings:");
|
|
423
|
+
warnings.forEach((w) => envLogger.warn(` - ${w}`));
|
|
424
|
+
}
|
|
425
|
+
this.hasValidated = true;
|
|
997
426
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
427
|
+
/**
|
|
428
|
+
* 실제 접근 시점에 환경변수 값 가져오기 및 검증
|
|
429
|
+
*
|
|
430
|
+
* @internal
|
|
431
|
+
*/
|
|
432
|
+
getAndValidate(key) {
|
|
433
|
+
if (this.valueCache.has(key)) {
|
|
434
|
+
return this.valueCache.get(key);
|
|
435
|
+
}
|
|
436
|
+
const schema = this.schemas.get(key);
|
|
437
|
+
if (!schema) {
|
|
438
|
+
return void 0;
|
|
439
|
+
}
|
|
440
|
+
const value = this.getRawValue(key, schema.fallbackKeys);
|
|
441
|
+
if (schema.required && !value) {
|
|
442
|
+
const fallbackHint = schema.fallbackKeys ? ` (or ${schema.fallbackKeys.join(", ")})` : "";
|
|
443
|
+
const errorMsg = `${key}${fallbackHint} is required but not set. ${schema.description || ""}`;
|
|
444
|
+
envLogger.error(`Environment validation failed:
|
|
445
|
+
- ${errorMsg}`);
|
|
446
|
+
throw new Error("Environment validation failed");
|
|
447
|
+
}
|
|
448
|
+
if (!value) {
|
|
449
|
+
const result = schema.default;
|
|
450
|
+
this.valueCache.set(key, result);
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
454
|
+
const errorMsg = `${key} must be at least ${schema.minLength} characters long (current: ${value.length})`;
|
|
455
|
+
envLogger.error(`Environment validation failed:
|
|
456
|
+
- ${errorMsg}`);
|
|
457
|
+
throw new Error("Environment validation failed");
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const result = this.applyValidator(value, schema);
|
|
461
|
+
this.valueCache.set(key, result);
|
|
462
|
+
return result;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const errorMsg = `${key} validation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
465
|
+
envLogger.error(`Environment validation failed:
|
|
466
|
+
- ${errorMsg}`);
|
|
467
|
+
throw new Error("Environment validation failed");
|
|
468
|
+
}
|
|
1005
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* 환경변수 검증 및 타입 안전한 env 객체 반환
|
|
472
|
+
*
|
|
473
|
+
* Proxy 기반으로 구현되어 실제 환경변수 접근 시점에 값을 읽고 검증합니다.
|
|
474
|
+
* 이를 통해 dotenv 로딩 타이밍과 무관하게 최신 환경변수 값을 가져올 수 있습니다.
|
|
475
|
+
*
|
|
476
|
+
* @returns 검증된 환경변수 객체 (Proxy)
|
|
477
|
+
* @throws {Error} 필수 변수 누락 또는 검증 실패 시
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* ```typescript
|
|
481
|
+
* const registry = createEnvRegistry(schema);
|
|
482
|
+
* const env = registry.validate(); // 스키마만 검증
|
|
483
|
+
* // ... dotenv 로딩 ...
|
|
484
|
+
* console.log(env.DATABASE_URL); // 이 시점에 실제 값 읽기
|
|
485
|
+
* ```
|
|
486
|
+
*/
|
|
487
|
+
validate() {
|
|
488
|
+
this.validateSchemas();
|
|
489
|
+
return new Proxy({}, {
|
|
490
|
+
get: (_target, prop) => {
|
|
491
|
+
return this.getAndValidate(prop);
|
|
492
|
+
},
|
|
493
|
+
ownKeys: () => {
|
|
494
|
+
return Array.from(this.schemas.keys());
|
|
495
|
+
},
|
|
496
|
+
getOwnPropertyDescriptor: (_target, prop) => {
|
|
497
|
+
if (this.schemas.has(prop)) {
|
|
498
|
+
return {
|
|
499
|
+
enumerable: true,
|
|
500
|
+
configurable: true,
|
|
501
|
+
get: () => this.getAndValidate(prop)
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return void 0;
|
|
505
|
+
},
|
|
506
|
+
has: (_target, prop) => {
|
|
507
|
+
return this.schemas.has(prop);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
function createEnvRegistry(schemas) {
|
|
513
|
+
return new EnvRegistry(schemas);
|
|
1006
514
|
}
|
|
1007
515
|
|
|
1008
|
-
export {
|
|
516
|
+
export { EnvRegistry, chain, createArrayParser, createEnumParser, createEnvRegistry, createJsonParser, createNumberParser, createPasswordParser, createSecureSecretParser, createStringParser, createUrlParser, defineEnvSchema, envBoolean, envEnum, envJson, envNumber, envString, envUrl, isClientAccessible, isServerOnly, optional, parseArray, parseBoolean, parseDecimal, parseEnum, parseInteger, parseJson, parseNumber, parsePostgresUrl, parseRedisUrl, parseString, parseUrl, withFallback };
|
|
1009
517
|
//# sourceMappingURL=index.js.map
|
|
1010
518
|
//# sourceMappingURL=index.js.map
|