@spfn/core 0.1.0-alpha.88 → 0.2.0-beta.2

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.
Files changed (71) hide show
  1. package/README.md +1046 -384
  2. package/dist/boss-D-fGtVgM.d.ts +187 -0
  3. package/dist/cache/index.d.ts +13 -33
  4. package/dist/cache/index.js +14 -703
  5. package/dist/cache/index.js.map +1 -1
  6. package/dist/codegen/index.d.ts +167 -17
  7. package/dist/codegen/index.js +76 -1419
  8. package/dist/codegen/index.js.map +1 -1
  9. package/dist/config/index.d.ts +1191 -0
  10. package/dist/config/index.js +264 -0
  11. package/dist/config/index.js.map +1 -0
  12. package/dist/db/index.d.ts +728 -59
  13. package/dist/db/index.js +1028 -1225
  14. package/dist/db/index.js.map +1 -1
  15. package/dist/env/index.d.ts +579 -308
  16. package/dist/env/index.js +438 -930
  17. package/dist/env/index.js.map +1 -1
  18. package/dist/errors/index.d.ts +417 -29
  19. package/dist/errors/index.js +359 -98
  20. package/dist/errors/index.js.map +1 -1
  21. package/dist/event/index.d.ts +108 -0
  22. package/dist/event/index.js +122 -0
  23. package/dist/event/index.js.map +1 -0
  24. package/dist/job/index.d.ts +172 -0
  25. package/dist/job/index.js +361 -0
  26. package/dist/job/index.js.map +1 -0
  27. package/dist/logger/index.d.ts +20 -79
  28. package/dist/logger/index.js +82 -387
  29. package/dist/logger/index.js.map +1 -1
  30. package/dist/middleware/index.d.ts +2 -11
  31. package/dist/middleware/index.js +49 -703
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/nextjs/index.d.ts +120 -0
  34. package/dist/nextjs/index.js +416 -0
  35. package/dist/nextjs/index.js.map +1 -0
  36. package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +288 -262
  37. package/dist/nextjs/server.js +568 -0
  38. package/dist/nextjs/server.js.map +1 -0
  39. package/dist/route/index.d.ts +686 -25
  40. package/dist/route/index.js +440 -1287
  41. package/dist/route/index.js.map +1 -1
  42. package/dist/route/types.d.ts +38 -0
  43. package/dist/route/types.js +3 -0
  44. package/dist/route/types.js.map +1 -0
  45. package/dist/server/index.d.ts +201 -67
  46. package/dist/server/index.js +921 -3182
  47. package/dist/server/index.js.map +1 -1
  48. package/dist/types-BGl4QL1w.d.ts +77 -0
  49. package/dist/types-DRG2XMTR.d.ts +157 -0
  50. package/package.json +52 -47
  51. package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
  52. package/dist/client/index.d.ts +0 -358
  53. package/dist/client/index.js +0 -357
  54. package/dist/client/index.js.map +0 -1
  55. package/dist/client/nextjs/index.js +0 -371
  56. package/dist/client/nextjs/index.js.map +0 -1
  57. package/dist/codegen/generators/index.d.ts +0 -19
  58. package/dist/codegen/generators/index.js +0 -1404
  59. package/dist/codegen/generators/index.js.map +0 -1
  60. package/dist/database-errors-BNNmLTJE.d.ts +0 -86
  61. package/dist/events/index.d.ts +0 -183
  62. package/dist/events/index.js +0 -77
  63. package/dist/events/index.js.map +0 -1
  64. package/dist/index-DHiAqhKv.d.ts +0 -101
  65. package/dist/index.d.ts +0 -8
  66. package/dist/index.js +0 -3674
  67. package/dist/index.js.map +0 -1
  68. package/dist/types/index.d.ts +0 -121
  69. package/dist/types/index.js +0 -38
  70. package/dist/types/index.js.map +0 -1
  71. package/dist/types-BXibIEyj.d.ts +0 -60
package/dist/env/index.js CHANGED
@@ -1,1010 +1,518 @@
1
- import { config } from 'dotenv';
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/loader.ts
6
-
7
- // src/logger/types.ts
8
- var LOG_LEVEL_PRIORITY = {
9
- debug: 0,
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 JSON.stringify(obj);
9
+ return trimmed;
186
10
  }
187
-
188
- // src/logger/logger.ts
189
- var Logger = class _Logger {
190
- config;
191
- module;
192
- constructor(config) {
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
- 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;
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
- const message = formatConsole(metadata, this.colorize);
313
- if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
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
- const message = formatJSON(metadata);
347
- const filename = this.getLogFilename(metadata.timestamp);
348
- if (this.currentFilename !== filename) {
349
- await this.rotateStream(filename);
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
- async checkAndRotateBySize() {
412
- if (!this.currentFilename) {
413
- return;
414
- }
415
- const filepath = join(this.logDir, this.currentFilename);
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
- * 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log
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
- async close() {
515
- await this.closeStream();
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 getDefaultLogLevel() {
522
- const isProduction = process.env.NODE_ENV === "production";
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 getConsoleConfig() {
533
- const isProduction = process.env.NODE_ENV === "production";
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 getFileConfig() {
542
- const isProduction = process.env.NODE_ENV === "production";
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 validateDirectoryWritable(dirPath) {
554
- if (!existsSync(dirPath)) {
555
- try {
556
- mkdirSync(dirPath, { recursive: true });
557
- } catch (error) {
558
- const errorMessage = error instanceof Error ? error.message : String(error);
559
- throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
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
- try {
563
- accessSync(dirPath, constants.W_OK);
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
- const testFile = join(dirPath, ".logger-write-test");
568
- try {
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 validateFileConfig() {
577
- if (!isFileLoggingEnabled()) {
578
- return;
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
- const logDir = process.env.LOG_DIR;
581
- if (!logDir) {
103
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
582
104
  throw new Error(
583
- "LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
105
+ `Must be a PostgreSQL URL (postgres:// or postgresql://), got ${url.protocol}`
584
106
  );
585
107
  }
586
- validateDirectoryWritable(logDir);
108
+ return value;
587
109
  }
588
- function validateSlackConfig() {
589
- const webhookUrl = process.env.SLACK_WEBHOOK_URL;
590
- if (!webhookUrl) {
591
- return;
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 (!webhookUrl.startsWith("https://hooks.slack.com/")) {
120
+ if (url.protocol !== "redis:" && url.protocol !== "rediss:") {
594
121
  throw new Error(
595
- `Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
122
+ `Must be a Redis URL (redis:// or rediss://), got ${url.protocol}`
596
123
  );
597
124
  }
125
+ return value;
598
126
  }
599
- function validateEmailConfig() {
600
- const smtpHost = process.env.SMTP_HOST;
601
- const smtpPort = process.env.SMTP_PORT;
602
- const emailFrom = process.env.EMAIL_FROM;
603
- const emailTo = process.env.EMAIL_TO;
604
- const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo;
605
- if (!hasAnyEmailConfig) {
606
- return;
607
- }
608
- const missingFields = [];
609
- if (!smtpHost) missingFields.push("SMTP_HOST");
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
- const port = parseInt(smtpPort, 10);
619
- if (isNaN(port) || port < 1 || port > 65535) {
139
+ if (!allowed.includes(value)) {
620
140
  throw new Error(
621
- `Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
141
+ `Must be one of [${allowed.join(", ")}], got: ${value}`
622
142
  );
623
143
  }
624
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
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 validateEnvironment() {
636
- const nodeEnv = process.env.NODE_ENV;
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 validateConfig() {
149
+ function parseJson(value) {
644
150
  try {
645
- validateEnvironment();
646
- validateFileConfig();
647
- validateSlackConfig();
648
- validateEmailConfig();
151
+ return JSON.parse(value);
649
152
  } catch (error) {
650
- if (error instanceof Error) {
651
- throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
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
- // src/logger/factory.ts
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 loadSingleFile(filePath, debug) {
720
- if (!existsSync(filePath)) {
721
- if (debug) {
722
- envLogger.debug("Environment file not found (optional)", {
723
- path: filePath
724
- });
725
- }
726
- return { success: false, parsed: {}, error: "File not found" };
727
- }
728
- try {
729
- const result = config({ path: filePath });
730
- if (result.error) {
731
- envLogger.warn("Failed to parse environment file", {
732
- path: filePath,
733
- error: result.error.message
734
- });
735
- return {
736
- success: false,
737
- parsed: {},
738
- error: result.error.message
739
- };
740
- }
741
- const parsed = result.parsed || {};
742
- if (debug) {
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
- return { success: false, parsed: {}, error: message };
757
- }
186
+ };
758
187
  }
759
- function validateRequiredVars(required, debug) {
760
- const missing = [];
761
- for (const varName of required) {
762
- if (!process.env[varName]) {
763
- missing.push(varName);
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
- 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 loadEnvironment(options = {}) {
201
+ function createSecureSecretParser(options = {}) {
782
202
  const {
783
- basePath = process.cwd(),
784
- customPaths = [],
785
- debug = false,
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
- if (useCache && environmentLoaded && cachedLoadResult) {
791
- if (debug) {
792
- envLogger.debug("Returning cached environment", {
793
- loaded: cachedLoadResult.loaded.length,
794
- variables: Object.keys(cachedLoadResult.parsed).length
795
- });
796
- }
797
- return cachedLoadResult;
798
- }
799
- if (debug) {
800
- envLogger.debug("Loading environment variables", {
801
- basePath,
802
- nodeEnv,
803
- customPaths,
804
- required
805
- });
806
- }
807
- const result = {
808
- success: true,
809
- loaded: [],
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 getEnvVar(key, options = {}) {
229
+ function createPasswordParser(options = {}) {
871
230
  const {
872
- required = false,
873
- default: defaultValue,
874
- validator,
875
- validationError
231
+ minLength = 8,
232
+ requireUppercase = true,
233
+ requireLowercase = true,
234
+ requireNumber = true,
235
+ requireSpecial = true
876
236
  } = options;
877
- const value = process.env[key];
878
- if (value === void 0 || value === "") {
879
- if (required) {
880
- throw new Error(`Required environment variable not found: ${key}`);
237
+ return (value) => {
238
+ const errors = [];
239
+ if (value.length < minLength) {
240
+ errors.push(`Must be at least ${minLength} characters`);
881
241
  }
882
- return defaultValue;
883
- }
884
- if (validator && !validator(value)) {
885
- const message = validationError || `Invalid value for environment variable: ${key}`;
886
- throw new Error(message);
887
- }
888
- return value;
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 requireEnvVar(key) {
891
- return getEnvVar(key, { required: true });
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 hasEnvVar(key) {
894
- const value = process.env[key];
895
- return value !== void 0 && value !== "";
278
+ function optional(parser) {
279
+ return (value) => {
280
+ if (value.trim() === "") {
281
+ return void 0;
282
+ }
283
+ return parser(value);
284
+ };
896
285
  }
897
- function getEnvVars(keys) {
286
+
287
+ // src/env/schema.ts
288
+ function defineEnvSchema(schema) {
898
289
  const result = {};
899
- for (const key of keys) {
900
- result[key] = process.env[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 isEnvironmentLoaded() {
905
- return environmentLoaded;
298
+ function envString(options) {
299
+ return {
300
+ ...options,
301
+ type: "string"
302
+ };
906
303
  }
907
- function resetEnvironment() {
908
- environmentLoaded = false;
909
- cachedLoadResult = void 0;
304
+ function envNumber(options) {
305
+ return {
306
+ ...options,
307
+ type: "number",
308
+ validator: options.validator || parseNumber
309
+ };
910
310
  }
911
-
912
- // src/env/validator.ts
913
- function validateUrl(value, options = {}) {
914
- const { protocol = "any" } = options;
915
- try {
916
- const url = new URL(value);
917
- if (protocol === "http" && url.protocol !== "http:") {
918
- return false;
919
- }
920
- if (protocol === "https" && url.protocol !== "https:") {
921
- return false;
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
- return true;
924
- } catch {
925
- return false;
926
- }
334
+ };
335
+ }
336
+ function envJson(options) {
337
+ return {
338
+ ...options,
339
+ type: "json",
340
+ validator: (val) => parseJson(val)
341
+ };
927
342
  }
928
- function createUrlValidator(protocol = "any") {
929
- return (value) => validateUrl(value, { protocol });
343
+ function isClientAccessible(key) {
344
+ return key.startsWith("NEXT_PUBLIC_");
930
345
  }
931
- function validateNumber(value, options = {}) {
932
- const { min, max, integer = false } = options;
933
- if (value.trim() === "") {
934
- return false;
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
- const num = Number(value);
937
- if (isNaN(num)) {
938
- return false;
359
+ /**
360
+ * 스키마 등록
361
+ */
362
+ register(schema) {
363
+ this.schemas.set(schema.key, schema);
939
364
  }
940
- if (integer && !Number.isInteger(num)) {
941
- return false;
365
+ /**
366
+ * 여러 스키마 등록
367
+ */
368
+ registerMultiple(schemas) {
369
+ for (const [key, schema] of Object.entries(schemas)) {
370
+ this.register({ ...schema, key });
371
+ }
942
372
  }
943
- if (min !== void 0 && num < min) {
944
- return false;
373
+ /**
374
+ * 캐시 및 검증 상태 리셋 (테스트용)
375
+ */
376
+ reset() {
377
+ this.valueCache.clear();
378
+ this.hasValidated = false;
945
379
  }
946
- if (max !== void 0 && num > max) {
947
- return false;
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
- return true;
950
- }
951
- function createNumberValidator(options = {}) {
952
- return (value) => validateNumber(value, options);
953
- }
954
- function validateBoolean(value) {
955
- const normalized = value.toLowerCase().trim();
956
- return ["true", "false", "1", "0", "yes", "no"].includes(normalized);
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
- return allowed.includes(value);
969
- }
970
- function createEnumValidator(allowed, caseInsensitive = false) {
971
- return (value) => validateEnum(value, allowed, caseInsensitive);
972
- }
973
- function validatePattern(value, pattern) {
974
- return pattern.test(value);
975
- }
976
- function createPatternValidator(pattern) {
977
- return (value) => validatePattern(value, pattern);
978
- }
979
- function validateNotEmpty(value) {
980
- return value.trim().length > 0;
981
- }
982
- function validateMinLength(value, minLength) {
983
- return value.length >= minLength;
984
- }
985
- function createMinLengthValidator(minLength) {
986
- return (value) => validateMinLength(value, minLength);
987
- }
988
- function combineValidators(validators) {
989
- return (value) => validators.every((validator) => validator(value));
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
- function validateRedisUrl(value) {
1000
- try {
1001
- const url = new URL(value);
1002
- return url.protocol === "redis:" || url.protocol === "rediss:";
1003
- } catch {
1004
- return false;
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 { ENV_FILE_PRIORITY, TEST_ONLY_FILES, combineValidators, createEnumValidator, createMinLengthValidator, createNumberValidator, createPatternValidator, createUrlValidator, getEnvVar, getEnvVars, hasEnvVar, isEnvironmentLoaded, loadEnvironment, parseBoolean, requireEnvVar, resetEnvironment, validateBoolean, validateEnum, validateMinLength, validateNotEmpty, validateNumber, validatePattern, validatePostgresUrl, validateRedisUrl, validateUrl };
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