@spfn/core 0.1.0-alpha.7 → 0.1.0-alpha.74

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 (59) hide show
  1. package/README.md +168 -195
  2. package/dist/auto-loader-JFaZ9gON.d.ts +80 -0
  3. package/dist/cache/index.d.ts +211 -0
  4. package/dist/cache/index.js +992 -0
  5. package/dist/cache/index.js.map +1 -0
  6. package/dist/client/index.d.ts +92 -92
  7. package/dist/client/index.js +80 -85
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/codegen/generators/index.d.ts +19 -0
  10. package/dist/codegen/generators/index.js +1500 -0
  11. package/dist/codegen/generators/index.js.map +1 -0
  12. package/dist/codegen/index.d.ts +76 -60
  13. package/dist/codegen/index.js +1486 -736
  14. package/dist/codegen/index.js.map +1 -1
  15. package/dist/database-errors-BNNmLTJE.d.ts +86 -0
  16. package/dist/db/index.d.ts +844 -44
  17. package/dist/db/index.js +1262 -1309
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/env/index.d.ts +508 -0
  20. package/dist/env/index.js +1106 -0
  21. package/dist/env/index.js.map +1 -0
  22. package/dist/error-handler-wjLL3v-a.d.ts +44 -0
  23. package/dist/errors/index.d.ts +136 -0
  24. package/dist/errors/index.js +172 -0
  25. package/dist/errors/index.js.map +1 -0
  26. package/dist/index-DHiAqhKv.d.ts +101 -0
  27. package/dist/index.d.ts +3 -374
  28. package/dist/index.js +2394 -2176
  29. package/dist/index.js.map +1 -1
  30. package/dist/logger/index.d.ts +94 -0
  31. package/dist/logger/index.js +774 -0
  32. package/dist/logger/index.js.map +1 -0
  33. package/dist/middleware/index.d.ts +33 -0
  34. package/dist/middleware/index.js +890 -0
  35. package/dist/middleware/index.js.map +1 -0
  36. package/dist/route/index.d.ts +21 -53
  37. package/dist/route/index.js +1234 -219
  38. package/dist/route/index.js.map +1 -1
  39. package/dist/server/index.d.ts +18 -0
  40. package/dist/server/index.js +2390 -2058
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/types-Dzggq1Yb.d.ts +170 -0
  43. package/package.json +59 -15
  44. package/dist/auto-loader-C44TcLmM.d.ts +0 -125
  45. package/dist/bind-pssq1NRT.d.ts +0 -34
  46. package/dist/postgres-errors-CY_Es8EJ.d.ts +0 -1703
  47. package/dist/scripts/index.d.ts +0 -24
  48. package/dist/scripts/index.js +0 -1201
  49. package/dist/scripts/index.js.map +0 -1
  50. package/dist/scripts/templates/api-index.template.txt +0 -10
  51. package/dist/scripts/templates/api-tag.template.txt +0 -11
  52. package/dist/scripts/templates/contract.template.txt +0 -87
  53. package/dist/scripts/templates/entity-type.template.txt +0 -31
  54. package/dist/scripts/templates/entity.template.txt +0 -19
  55. package/dist/scripts/templates/index.template.txt +0 -10
  56. package/dist/scripts/templates/repository.template.txt +0 -37
  57. package/dist/scripts/templates/routes-id.template.txt +0 -59
  58. package/dist/scripts/templates/routes-index.template.txt +0 -44
  59. package/dist/types-SlzTr8ZO.d.ts +0 -143
@@ -0,0 +1,1106 @@
1
+ import { config } from 'dotenv';
2
+ import { existsSync, mkdirSync, accessSync, constants, writeFileSync, unlinkSync, createWriteStream, statSync, readdirSync, renameSync } from 'fs';
3
+ import { join } from 'path';
4
+ import pino from 'pino';
5
+
6
+ // src/env/loader.ts
7
+ var PinoAdapter = class _PinoAdapter {
8
+ logger;
9
+ constructor(config) {
10
+ this.logger = pino({
11
+ level: config.level,
12
+ // 기본 필드
13
+ base: config.module ? { module: config.module } : void 0
14
+ });
15
+ }
16
+ child(module) {
17
+ const childLogger = new _PinoAdapter({ level: this.logger.level, module });
18
+ childLogger.logger = this.logger.child({ module });
19
+ return childLogger;
20
+ }
21
+ debug(message, context) {
22
+ this.logger.debug(context || {}, message);
23
+ }
24
+ info(message, context) {
25
+ this.logger.info(context || {}, message);
26
+ }
27
+ warn(message, errorOrContext, context) {
28
+ if (errorOrContext instanceof Error) {
29
+ this.logger.warn({ err: errorOrContext, ...context }, message);
30
+ } else {
31
+ this.logger.warn(errorOrContext || {}, message);
32
+ }
33
+ }
34
+ error(message, errorOrContext, context) {
35
+ if (errorOrContext instanceof Error) {
36
+ this.logger.error({ err: errorOrContext, ...context }, message);
37
+ } else {
38
+ this.logger.error(errorOrContext || {}, message);
39
+ }
40
+ }
41
+ fatal(message, errorOrContext, context) {
42
+ if (errorOrContext instanceof Error) {
43
+ this.logger.fatal({ err: errorOrContext, ...context }, message);
44
+ } else {
45
+ this.logger.fatal(errorOrContext || {}, message);
46
+ }
47
+ }
48
+ async close() {
49
+ }
50
+ };
51
+
52
+ // src/logger/types.ts
53
+ var LOG_LEVEL_PRIORITY = {
54
+ debug: 0,
55
+ info: 1,
56
+ warn: 2,
57
+ error: 3,
58
+ fatal: 4
59
+ };
60
+
61
+ // src/logger/formatters.ts
62
+ var SENSITIVE_KEYS = [
63
+ "password",
64
+ "passwd",
65
+ "pwd",
66
+ "secret",
67
+ "token",
68
+ "apikey",
69
+ "api_key",
70
+ "accesstoken",
71
+ "access_token",
72
+ "refreshtoken",
73
+ "refresh_token",
74
+ "authorization",
75
+ "auth",
76
+ "cookie",
77
+ "session",
78
+ "sessionid",
79
+ "session_id",
80
+ "privatekey",
81
+ "private_key",
82
+ "creditcard",
83
+ "credit_card",
84
+ "cardnumber",
85
+ "card_number",
86
+ "cvv",
87
+ "ssn",
88
+ "pin"
89
+ ];
90
+ var MASKED_VALUE = "***MASKED***";
91
+ function isSensitiveKey(key) {
92
+ const lowerKey = key.toLowerCase();
93
+ return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
94
+ }
95
+ function maskSensitiveData(data) {
96
+ if (data === null || data === void 0) {
97
+ return data;
98
+ }
99
+ if (Array.isArray(data)) {
100
+ return data.map((item) => maskSensitiveData(item));
101
+ }
102
+ if (typeof data === "object") {
103
+ const masked = {};
104
+ for (const [key, value] of Object.entries(data)) {
105
+ if (isSensitiveKey(key)) {
106
+ masked[key] = MASKED_VALUE;
107
+ } else if (typeof value === "object" && value !== null) {
108
+ masked[key] = maskSensitiveData(value);
109
+ } else {
110
+ masked[key] = value;
111
+ }
112
+ }
113
+ return masked;
114
+ }
115
+ return data;
116
+ }
117
+ var COLORS = {
118
+ reset: "\x1B[0m",
119
+ bright: "\x1B[1m",
120
+ dim: "\x1B[2m",
121
+ // 로그 레벨 컬러
122
+ debug: "\x1B[36m",
123
+ // cyan
124
+ info: "\x1B[32m",
125
+ // green
126
+ warn: "\x1B[33m",
127
+ // yellow
128
+ error: "\x1B[31m",
129
+ // red
130
+ fatal: "\x1B[35m",
131
+ // magenta
132
+ // 추가 컬러
133
+ gray: "\x1B[90m"
134
+ };
135
+ function formatTimestamp(date) {
136
+ return date.toISOString();
137
+ }
138
+ function formatTimestampHuman(date) {
139
+ const year = date.getFullYear();
140
+ const month = String(date.getMonth() + 1).padStart(2, "0");
141
+ const day = String(date.getDate()).padStart(2, "0");
142
+ const hours = String(date.getHours()).padStart(2, "0");
143
+ const minutes = String(date.getMinutes()).padStart(2, "0");
144
+ const seconds = String(date.getSeconds()).padStart(2, "0");
145
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
146
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
147
+ }
148
+ function formatError(error) {
149
+ const lines = [];
150
+ lines.push(`${error.name}: ${error.message}`);
151
+ if (error.stack) {
152
+ const stackLines = error.stack.split("\n").slice(1);
153
+ lines.push(...stackLines);
154
+ }
155
+ return lines.join("\n");
156
+ }
157
+ function formatConsole(metadata, colorize = true) {
158
+ const parts = [];
159
+ const timestamp = formatTimestampHuman(metadata.timestamp);
160
+ if (colorize) {
161
+ parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
162
+ } else {
163
+ parts.push(`[${timestamp}]`);
164
+ }
165
+ if (metadata.module) {
166
+ if (colorize) {
167
+ parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
168
+ } else {
169
+ parts.push(`[module=${metadata.module}]`);
170
+ }
171
+ }
172
+ if (metadata.context && Object.keys(metadata.context).length > 0) {
173
+ Object.entries(metadata.context).forEach(([key, value]) => {
174
+ const valueStr = typeof value === "string" ? value : String(value);
175
+ if (colorize) {
176
+ parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
177
+ } else {
178
+ parts.push(`[${key}=${valueStr}]`);
179
+ }
180
+ });
181
+ }
182
+ const levelStr = metadata.level.toUpperCase();
183
+ if (colorize) {
184
+ const color = COLORS[metadata.level];
185
+ parts.push(`${color}(${levelStr})${COLORS.reset}:`);
186
+ } else {
187
+ parts.push(`(${levelStr}):`);
188
+ }
189
+ if (colorize) {
190
+ parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
191
+ } else {
192
+ parts.push(metadata.message);
193
+ }
194
+ let output = parts.join(" ");
195
+ if (metadata.error) {
196
+ output += "\n" + formatError(metadata.error);
197
+ }
198
+ return output;
199
+ }
200
+ function formatJSON(metadata) {
201
+ const obj = {
202
+ timestamp: formatTimestamp(metadata.timestamp),
203
+ level: metadata.level,
204
+ message: metadata.message
205
+ };
206
+ if (metadata.module) {
207
+ obj.module = metadata.module;
208
+ }
209
+ if (metadata.context) {
210
+ obj.context = metadata.context;
211
+ }
212
+ if (metadata.error) {
213
+ obj.error = {
214
+ name: metadata.error.name,
215
+ message: metadata.error.message,
216
+ stack: metadata.error.stack
217
+ };
218
+ }
219
+ return JSON.stringify(obj);
220
+ }
221
+
222
+ // src/logger/logger.ts
223
+ var Logger = class _Logger {
224
+ config;
225
+ module;
226
+ constructor(config) {
227
+ this.config = config;
228
+ this.module = config.module;
229
+ }
230
+ /**
231
+ * Get current log level
232
+ */
233
+ get level() {
234
+ return this.config.level;
235
+ }
236
+ /**
237
+ * Create child logger (per module)
238
+ */
239
+ child(module) {
240
+ return new _Logger({
241
+ ...this.config,
242
+ module
243
+ });
244
+ }
245
+ /**
246
+ * Debug log
247
+ */
248
+ debug(message, context) {
249
+ this.log("debug", message, void 0, context);
250
+ }
251
+ /**
252
+ * Info log
253
+ */
254
+ info(message, context) {
255
+ this.log("info", message, void 0, context);
256
+ }
257
+ warn(message, errorOrContext, context) {
258
+ if (errorOrContext instanceof Error) {
259
+ this.log("warn", message, errorOrContext, context);
260
+ } else {
261
+ this.log("warn", message, void 0, errorOrContext);
262
+ }
263
+ }
264
+ error(message, errorOrContext, context) {
265
+ if (errorOrContext instanceof Error) {
266
+ this.log("error", message, errorOrContext, context);
267
+ } else {
268
+ this.log("error", message, void 0, errorOrContext);
269
+ }
270
+ }
271
+ fatal(message, errorOrContext, context) {
272
+ if (errorOrContext instanceof Error) {
273
+ this.log("fatal", message, errorOrContext, context);
274
+ } else {
275
+ this.log("fatal", message, void 0, errorOrContext);
276
+ }
277
+ }
278
+ /**
279
+ * Log processing (internal)
280
+ */
281
+ log(level, message, error, context) {
282
+ if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
283
+ return;
284
+ }
285
+ const metadata = {
286
+ timestamp: /* @__PURE__ */ new Date(),
287
+ level,
288
+ message,
289
+ module: this.module,
290
+ error,
291
+ // Mask sensitive information in context to prevent credential leaks
292
+ context: context ? maskSensitiveData(context) : void 0
293
+ };
294
+ this.processTransports(metadata);
295
+ }
296
+ /**
297
+ * Process Transports
298
+ */
299
+ processTransports(metadata) {
300
+ const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
301
+ Promise.all(promises).catch((error) => {
302
+ const errorMessage = error instanceof Error ? error.message : String(error);
303
+ process.stderr.write(`[Logger] Transport error: ${errorMessage}
304
+ `);
305
+ });
306
+ }
307
+ /**
308
+ * Transport log (error-safe)
309
+ */
310
+ async safeTransportLog(transport, metadata) {
311
+ try {
312
+ await transport.log(metadata);
313
+ } catch (error) {
314
+ const errorMessage = error instanceof Error ? error.message : String(error);
315
+ process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
316
+ `);
317
+ }
318
+ }
319
+ /**
320
+ * Close all Transports
321
+ */
322
+ async close() {
323
+ const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
324
+ await Promise.all(closePromises);
325
+ }
326
+ };
327
+
328
+ // src/logger/transports/console.ts
329
+ var ConsoleTransport = class {
330
+ name = "console";
331
+ level;
332
+ enabled;
333
+ colorize;
334
+ constructor(config) {
335
+ this.level = config.level;
336
+ this.enabled = config.enabled;
337
+ this.colorize = config.colorize ?? true;
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;
345
+ }
346
+ const message = formatConsole(metadata, this.colorize);
347
+ if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
348
+ console.error(message);
349
+ } else {
350
+ console.log(message);
351
+ }
352
+ }
353
+ };
354
+ var FileTransport = class {
355
+ name = "file";
356
+ level;
357
+ enabled;
358
+ logDir;
359
+ maxFileSize;
360
+ maxFiles;
361
+ currentStream = null;
362
+ currentFilename = null;
363
+ constructor(config) {
364
+ this.level = config.level;
365
+ this.enabled = config.enabled;
366
+ this.logDir = config.logDir;
367
+ this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
368
+ this.maxFiles = config.maxFiles ?? 10;
369
+ if (!existsSync(this.logDir)) {
370
+ mkdirSync(this.logDir, { recursive: true });
371
+ }
372
+ }
373
+ async log(metadata) {
374
+ if (!this.enabled) {
375
+ return;
376
+ }
377
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
378
+ return;
379
+ }
380
+ const message = formatJSON(metadata);
381
+ const filename = this.getLogFilename(metadata.timestamp);
382
+ if (this.currentFilename !== filename) {
383
+ await this.rotateStream(filename);
384
+ await this.cleanOldFiles();
385
+ } else if (this.currentFilename) {
386
+ await this.checkAndRotateBySize();
387
+ }
388
+ if (this.currentStream) {
389
+ return new Promise((resolve, reject) => {
390
+ this.currentStream.write(message + "\n", "utf-8", (error) => {
391
+ if (error) {
392
+ process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
393
+ `);
394
+ reject(error);
395
+ } else {
396
+ resolve();
397
+ }
398
+ });
399
+ });
400
+ }
401
+ }
402
+ /**
403
+ * 스트림 교체 (날짜 변경 시)
404
+ */
405
+ async rotateStream(filename) {
406
+ if (this.currentStream) {
407
+ await this.closeStream();
408
+ }
409
+ const filepath = join(this.logDir, filename);
410
+ this.currentStream = createWriteStream(filepath, {
411
+ flags: "a",
412
+ // append mode
413
+ encoding: "utf-8"
414
+ });
415
+ this.currentFilename = filename;
416
+ this.currentStream.on("error", (error) => {
417
+ process.stderr.write(`[FileTransport] Stream error: ${error.message}
418
+ `);
419
+ this.currentStream = null;
420
+ this.currentFilename = null;
421
+ });
422
+ }
423
+ /**
424
+ * 현재 스트림 닫기
425
+ */
426
+ async closeStream() {
427
+ if (!this.currentStream) {
428
+ return;
429
+ }
430
+ return new Promise((resolve, reject) => {
431
+ this.currentStream.end((error) => {
432
+ if (error) {
433
+ reject(error);
434
+ } else {
435
+ this.currentStream = null;
436
+ this.currentFilename = null;
437
+ resolve();
438
+ }
439
+ });
440
+ });
441
+ }
442
+ /**
443
+ * 파일 크기 체크 및 크기 기반 로테이션
444
+ */
445
+ async checkAndRotateBySize() {
446
+ if (!this.currentFilename) {
447
+ return;
448
+ }
449
+ const filepath = join(this.logDir, this.currentFilename);
450
+ if (!existsSync(filepath)) {
451
+ return;
452
+ }
453
+ try {
454
+ const stats = statSync(filepath);
455
+ if (stats.size >= this.maxFileSize) {
456
+ await this.rotateBySize();
457
+ }
458
+ } catch (error) {
459
+ const errorMessage = error instanceof Error ? error.message : String(error);
460
+ process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage}
461
+ `);
462
+ }
463
+ }
464
+ /**
465
+ * 크기 기반 로테이션 수행
466
+ * 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log
467
+ */
468
+ async rotateBySize() {
469
+ if (!this.currentFilename) {
470
+ return;
471
+ }
472
+ await this.closeStream();
473
+ const baseName = this.currentFilename.replace(/\.log$/, "");
474
+ const files = readdirSync(this.logDir);
475
+ const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse();
476
+ for (const file of relatedFiles) {
477
+ const match = file.match(/\.(\d+)\.log$/);
478
+ if (match) {
479
+ const oldNum = parseInt(match[1], 10);
480
+ const newNum = oldNum + 1;
481
+ const oldPath = join(this.logDir, file);
482
+ const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`);
483
+ try {
484
+ renameSync(oldPath, newPath2);
485
+ } catch (error) {
486
+ const errorMessage = error instanceof Error ? error.message : String(error);
487
+ process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage}
488
+ `);
489
+ }
490
+ }
491
+ }
492
+ const currentPath = join(this.logDir, this.currentFilename);
493
+ const newPath = join(this.logDir, `${baseName}.1.log`);
494
+ try {
495
+ if (existsSync(currentPath)) {
496
+ renameSync(currentPath, newPath);
497
+ }
498
+ } catch (error) {
499
+ const errorMessage = error instanceof Error ? error.message : String(error);
500
+ process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage}
501
+ `);
502
+ }
503
+ await this.rotateStream(this.currentFilename);
504
+ }
505
+ /**
506
+ * 오래된 로그 파일 정리
507
+ * maxFiles 개수를 초과하는 로그 파일 삭제
508
+ */
509
+ async cleanOldFiles() {
510
+ try {
511
+ if (!existsSync(this.logDir)) {
512
+ return;
513
+ }
514
+ const files = readdirSync(this.logDir);
515
+ const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => {
516
+ const filepath = join(this.logDir, file);
517
+ const stats = statSync(filepath);
518
+ return { file, mtime: stats.mtime };
519
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
520
+ if (logFiles.length > this.maxFiles) {
521
+ const filesToDelete = logFiles.slice(this.maxFiles);
522
+ for (const { file } of filesToDelete) {
523
+ const filepath = join(this.logDir, file);
524
+ try {
525
+ unlinkSync(filepath);
526
+ } catch (error) {
527
+ const errorMessage = error instanceof Error ? error.message : String(error);
528
+ process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage}
529
+ `);
530
+ }
531
+ }
532
+ }
533
+ } catch (error) {
534
+ const errorMessage = error instanceof Error ? error.message : String(error);
535
+ process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
536
+ `);
537
+ }
538
+ }
539
+ /**
540
+ * 날짜별 로그 파일명 생성
541
+ */
542
+ getLogFilename(date) {
543
+ const year = date.getFullYear();
544
+ const month = String(date.getMonth() + 1).padStart(2, "0");
545
+ const day = String(date.getDate()).padStart(2, "0");
546
+ return `${year}-${month}-${day}.log`;
547
+ }
548
+ async close() {
549
+ await this.closeStream();
550
+ }
551
+ };
552
+ function isFileLoggingEnabled() {
553
+ return process.env.LOGGER_FILE_ENABLED === "true";
554
+ }
555
+ function getDefaultLogLevel() {
556
+ const isProduction = process.env.NODE_ENV === "production";
557
+ const isDevelopment = process.env.NODE_ENV === "development";
558
+ if (isDevelopment) {
559
+ return "debug";
560
+ }
561
+ if (isProduction) {
562
+ return "info";
563
+ }
564
+ return "warn";
565
+ }
566
+ function getConsoleConfig() {
567
+ const isProduction = process.env.NODE_ENV === "production";
568
+ return {
569
+ level: "debug",
570
+ enabled: true,
571
+ colorize: !isProduction
572
+ // Dev: colored output, Production: plain text
573
+ };
574
+ }
575
+ function getFileConfig() {
576
+ const isProduction = process.env.NODE_ENV === "production";
577
+ return {
578
+ level: "info",
579
+ enabled: isProduction,
580
+ // File logging in production only
581
+ logDir: process.env.LOG_DIR || "./logs",
582
+ maxFileSize: 10 * 1024 * 1024,
583
+ // 10MB
584
+ maxFiles: 10
585
+ };
586
+ }
587
+ function validateDirectoryWritable(dirPath) {
588
+ if (!existsSync(dirPath)) {
589
+ try {
590
+ mkdirSync(dirPath, { recursive: true });
591
+ } catch (error) {
592
+ const errorMessage = error instanceof Error ? error.message : String(error);
593
+ throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
594
+ }
595
+ }
596
+ try {
597
+ accessSync(dirPath, constants.W_OK);
598
+ } catch {
599
+ throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
600
+ }
601
+ const testFile = join(dirPath, ".logger-write-test");
602
+ try {
603
+ writeFileSync(testFile, "test", "utf-8");
604
+ unlinkSync(testFile);
605
+ } catch (error) {
606
+ const errorMessage = error instanceof Error ? error.message : String(error);
607
+ throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
608
+ }
609
+ }
610
+ function validateFileConfig() {
611
+ if (!isFileLoggingEnabled()) {
612
+ return;
613
+ }
614
+ const logDir = process.env.LOG_DIR;
615
+ if (!logDir) {
616
+ throw new Error(
617
+ "LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
618
+ );
619
+ }
620
+ validateDirectoryWritable(logDir);
621
+ }
622
+ function validateSlackConfig() {
623
+ const webhookUrl = process.env.SLACK_WEBHOOK_URL;
624
+ if (!webhookUrl) {
625
+ return;
626
+ }
627
+ if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
628
+ throw new Error(
629
+ `Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
630
+ );
631
+ }
632
+ }
633
+ function validateEmailConfig() {
634
+ const smtpHost = process.env.SMTP_HOST;
635
+ const smtpPort = process.env.SMTP_PORT;
636
+ const emailFrom = process.env.EMAIL_FROM;
637
+ const emailTo = process.env.EMAIL_TO;
638
+ const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo;
639
+ if (!hasAnyEmailConfig) {
640
+ return;
641
+ }
642
+ const missingFields = [];
643
+ if (!smtpHost) missingFields.push("SMTP_HOST");
644
+ if (!smtpPort) missingFields.push("SMTP_PORT");
645
+ if (!emailFrom) missingFields.push("EMAIL_FROM");
646
+ if (!emailTo) missingFields.push("EMAIL_TO");
647
+ if (missingFields.length > 0) {
648
+ throw new Error(
649
+ `Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
650
+ );
651
+ }
652
+ const port = parseInt(smtpPort, 10);
653
+ if (isNaN(port) || port < 1 || port > 65535) {
654
+ throw new Error(
655
+ `Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
656
+ );
657
+ }
658
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
659
+ if (!emailRegex.test(emailFrom)) {
660
+ throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
661
+ }
662
+ const recipients = emailTo.split(",").map((e) => e.trim());
663
+ for (const email of recipients) {
664
+ if (!emailRegex.test(email)) {
665
+ throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
666
+ }
667
+ }
668
+ }
669
+ function validateEnvironment() {
670
+ const nodeEnv = process.env.NODE_ENV;
671
+ if (!nodeEnv) {
672
+ process.stderr.write(
673
+ "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
674
+ );
675
+ }
676
+ }
677
+ function validateConfig() {
678
+ try {
679
+ validateEnvironment();
680
+ validateFileConfig();
681
+ validateSlackConfig();
682
+ validateEmailConfig();
683
+ } catch (error) {
684
+ if (error instanceof Error) {
685
+ throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
686
+ }
687
+ throw error;
688
+ }
689
+ }
690
+
691
+ // src/logger/adapters/custom.ts
692
+ function initializeTransports() {
693
+ const transports = [];
694
+ const consoleConfig = getConsoleConfig();
695
+ transports.push(new ConsoleTransport(consoleConfig));
696
+ const fileConfig = getFileConfig();
697
+ if (fileConfig.enabled) {
698
+ transports.push(new FileTransport(fileConfig));
699
+ }
700
+ return transports;
701
+ }
702
+ var CustomAdapter = class _CustomAdapter {
703
+ logger;
704
+ constructor(config) {
705
+ this.logger = new Logger({
706
+ level: config.level,
707
+ module: config.module,
708
+ transports: initializeTransports()
709
+ });
710
+ }
711
+ child(module) {
712
+ const adapter = new _CustomAdapter({ level: this.logger.level, module });
713
+ adapter.logger = this.logger.child(module);
714
+ return adapter;
715
+ }
716
+ debug(message, context) {
717
+ this.logger.debug(message, context);
718
+ }
719
+ info(message, context) {
720
+ this.logger.info(message, context);
721
+ }
722
+ warn(message, errorOrContext, context) {
723
+ if (errorOrContext instanceof Error) {
724
+ this.logger.warn(message, errorOrContext, context);
725
+ } else {
726
+ this.logger.warn(message, errorOrContext);
727
+ }
728
+ }
729
+ error(message, errorOrContext, context) {
730
+ if (errorOrContext instanceof Error) {
731
+ this.logger.error(message, errorOrContext, context);
732
+ } else {
733
+ this.logger.error(message, errorOrContext);
734
+ }
735
+ }
736
+ fatal(message, errorOrContext, context) {
737
+ if (errorOrContext instanceof Error) {
738
+ this.logger.fatal(message, errorOrContext, context);
739
+ } else {
740
+ this.logger.fatal(message, errorOrContext);
741
+ }
742
+ }
743
+ async close() {
744
+ await this.logger.close();
745
+ }
746
+ };
747
+
748
+ // src/logger/adapter-factory.ts
749
+ function createAdapter(type) {
750
+ const level = getDefaultLogLevel();
751
+ switch (type) {
752
+ case "pino":
753
+ return new PinoAdapter({ level });
754
+ case "custom":
755
+ return new CustomAdapter({ level });
756
+ default:
757
+ return new PinoAdapter({ level });
758
+ }
759
+ }
760
+ function getAdapterType() {
761
+ const adapterEnv = process.env.LOGGER_ADAPTER;
762
+ if (adapterEnv === "custom" || adapterEnv === "pino") {
763
+ return adapterEnv;
764
+ }
765
+ return "pino";
766
+ }
767
+ function initializeLogger() {
768
+ validateConfig();
769
+ return createAdapter(getAdapterType());
770
+ }
771
+ var logger = initializeLogger();
772
+
773
+ // src/env/config.ts
774
+ var ENV_FILE_PRIORITY = [
775
+ ".env",
776
+ // Base configuration (lowest priority)
777
+ ".env.{NODE_ENV}",
778
+ // Environment-specific
779
+ ".env.local",
780
+ // Local overrides (excluded in test)
781
+ ".env.{NODE_ENV}.local"
782
+ // Local environment-specific (highest priority)
783
+ ];
784
+ var TEST_ONLY_FILES = [
785
+ ".env.test",
786
+ ".env.test.local"
787
+ ];
788
+
789
+ // src/env/loader.ts
790
+ var envLogger = logger.child("environment");
791
+ var environmentLoaded = false;
792
+ var cachedLoadResult;
793
+ function buildFileList(basePath, nodeEnv) {
794
+ const files = [];
795
+ if (!nodeEnv) {
796
+ files.push(join(basePath, ".env"));
797
+ files.push(join(basePath, ".env.local"));
798
+ return files;
799
+ }
800
+ for (const pattern of ENV_FILE_PRIORITY) {
801
+ const fileName = pattern.replace("{NODE_ENV}", nodeEnv);
802
+ if (nodeEnv === "test" && fileName === ".env.local") {
803
+ continue;
804
+ }
805
+ if (nodeEnv === "local" && pattern === ".env.local") {
806
+ continue;
807
+ }
808
+ if (nodeEnv !== "test" && TEST_ONLY_FILES.includes(fileName)) {
809
+ continue;
810
+ }
811
+ files.push(join(basePath, fileName));
812
+ }
813
+ return files;
814
+ }
815
+ function loadSingleFile(filePath, debug) {
816
+ if (!existsSync(filePath)) {
817
+ if (debug) {
818
+ envLogger.debug("Environment file not found (optional)", {
819
+ path: filePath
820
+ });
821
+ }
822
+ return { success: false, parsed: {}, error: "File not found" };
823
+ }
824
+ try {
825
+ const result = config({ path: filePath });
826
+ if (result.error) {
827
+ envLogger.warn("Failed to parse environment file", {
828
+ path: filePath,
829
+ error: result.error.message
830
+ });
831
+ return {
832
+ success: false,
833
+ parsed: {},
834
+ error: result.error.message
835
+ };
836
+ }
837
+ const parsed = result.parsed || {};
838
+ if (debug) {
839
+ envLogger.debug("Environment file loaded successfully", {
840
+ path: filePath,
841
+ variables: Object.keys(parsed),
842
+ count: Object.keys(parsed).length
843
+ });
844
+ }
845
+ return { success: true, parsed };
846
+ } catch (error) {
847
+ const message = error instanceof Error ? error.message : "Unknown error";
848
+ envLogger.error("Error loading environment file", {
849
+ path: filePath,
850
+ error: message
851
+ });
852
+ return { success: false, parsed: {}, error: message };
853
+ }
854
+ }
855
+ function validateRequiredVars(required, debug) {
856
+ const missing = [];
857
+ for (const varName of required) {
858
+ if (!process.env[varName]) {
859
+ missing.push(varName);
860
+ }
861
+ }
862
+ if (missing.length > 0) {
863
+ const error = `Required environment variables missing: ${missing.join(", ")}`;
864
+ envLogger.error("Environment validation failed", {
865
+ missing,
866
+ required
867
+ });
868
+ throw new Error(error);
869
+ }
870
+ if (debug) {
871
+ envLogger.debug("Required environment variables validated", {
872
+ required,
873
+ allPresent: true
874
+ });
875
+ }
876
+ }
877
+ function loadEnvironment(options = {}) {
878
+ const {
879
+ basePath = process.cwd(),
880
+ customPaths = [],
881
+ debug = false,
882
+ nodeEnv = process.env.NODE_ENV || "",
883
+ required = [],
884
+ useCache = true
885
+ } = options;
886
+ if (useCache && environmentLoaded && cachedLoadResult) {
887
+ if (debug) {
888
+ envLogger.debug("Returning cached environment", {
889
+ loaded: cachedLoadResult.loaded.length,
890
+ variables: Object.keys(cachedLoadResult.parsed).length
891
+ });
892
+ }
893
+ return cachedLoadResult;
894
+ }
895
+ if (debug) {
896
+ envLogger.debug("Loading environment variables", {
897
+ basePath,
898
+ nodeEnv,
899
+ customPaths,
900
+ required
901
+ });
902
+ }
903
+ const result = {
904
+ success: true,
905
+ loaded: [],
906
+ failed: [],
907
+ parsed: {},
908
+ warnings: []
909
+ };
910
+ const standardFiles = buildFileList(basePath, nodeEnv);
911
+ const allFiles = [...standardFiles, ...customPaths];
912
+ if (debug) {
913
+ envLogger.debug("Environment files to load", {
914
+ standardFiles,
915
+ customPaths,
916
+ total: allFiles.length
917
+ });
918
+ }
919
+ const reversedFiles = [...allFiles].reverse();
920
+ for (const filePath of reversedFiles) {
921
+ const fileResult = loadSingleFile(filePath, debug);
922
+ if (fileResult.success) {
923
+ result.loaded.push(filePath);
924
+ Object.assign(result.parsed, fileResult.parsed);
925
+ if (fileResult.parsed["NODE_ENV"]) {
926
+ const fileName = filePath.split("/").pop() || filePath;
927
+ result.warnings.push(
928
+ `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.`
929
+ );
930
+ }
931
+ } else if (fileResult.error) {
932
+ result.failed.push({
933
+ path: filePath,
934
+ reason: fileResult.error
935
+ });
936
+ }
937
+ }
938
+ if (debug || result.loaded.length > 0) {
939
+ envLogger.info("Environment loading complete", {
940
+ loaded: result.loaded.length,
941
+ failed: result.failed.length,
942
+ variables: Object.keys(result.parsed).length,
943
+ files: result.loaded
944
+ });
945
+ }
946
+ if (required.length > 0) {
947
+ try {
948
+ validateRequiredVars(required, debug);
949
+ } catch (error) {
950
+ result.success = false;
951
+ result.errors = [
952
+ error instanceof Error ? error.message : "Validation failed"
953
+ ];
954
+ throw error;
955
+ }
956
+ }
957
+ if (result.warnings.length > 0) {
958
+ for (const warning of result.warnings) {
959
+ envLogger.warn(warning);
960
+ }
961
+ }
962
+ environmentLoaded = true;
963
+ cachedLoadResult = result;
964
+ return result;
965
+ }
966
+ function getEnvVar(key, options = {}) {
967
+ const {
968
+ required = false,
969
+ default: defaultValue,
970
+ validator,
971
+ validationError
972
+ } = options;
973
+ const value = process.env[key];
974
+ if (value === void 0 || value === "") {
975
+ if (required) {
976
+ throw new Error(`Required environment variable not found: ${key}`);
977
+ }
978
+ return defaultValue;
979
+ }
980
+ if (validator && !validator(value)) {
981
+ const message = validationError || `Invalid value for environment variable: ${key}`;
982
+ throw new Error(message);
983
+ }
984
+ return value;
985
+ }
986
+ function requireEnvVar(key) {
987
+ return getEnvVar(key, { required: true });
988
+ }
989
+ function hasEnvVar(key) {
990
+ const value = process.env[key];
991
+ return value !== void 0 && value !== "";
992
+ }
993
+ function getEnvVars(keys) {
994
+ const result = {};
995
+ for (const key of keys) {
996
+ result[key] = process.env[key];
997
+ }
998
+ return result;
999
+ }
1000
+ function isEnvironmentLoaded() {
1001
+ return environmentLoaded;
1002
+ }
1003
+ function resetEnvironment() {
1004
+ environmentLoaded = false;
1005
+ cachedLoadResult = void 0;
1006
+ }
1007
+
1008
+ // src/env/validator.ts
1009
+ function validateUrl(value, options = {}) {
1010
+ const { protocol = "any" } = options;
1011
+ try {
1012
+ const url = new URL(value);
1013
+ if (protocol === "http" && url.protocol !== "http:") {
1014
+ return false;
1015
+ }
1016
+ if (protocol === "https" && url.protocol !== "https:") {
1017
+ return false;
1018
+ }
1019
+ return true;
1020
+ } catch {
1021
+ return false;
1022
+ }
1023
+ }
1024
+ function createUrlValidator(protocol = "any") {
1025
+ return (value) => validateUrl(value, { protocol });
1026
+ }
1027
+ function validateNumber(value, options = {}) {
1028
+ const { min, max, integer = false } = options;
1029
+ if (value.trim() === "") {
1030
+ return false;
1031
+ }
1032
+ const num = Number(value);
1033
+ if (isNaN(num)) {
1034
+ return false;
1035
+ }
1036
+ if (integer && !Number.isInteger(num)) {
1037
+ return false;
1038
+ }
1039
+ if (min !== void 0 && num < min) {
1040
+ return false;
1041
+ }
1042
+ if (max !== void 0 && num > max) {
1043
+ return false;
1044
+ }
1045
+ return true;
1046
+ }
1047
+ function createNumberValidator(options = {}) {
1048
+ return (value) => validateNumber(value, options);
1049
+ }
1050
+ function validateBoolean(value) {
1051
+ const normalized = value.toLowerCase().trim();
1052
+ return ["true", "false", "1", "0", "yes", "no"].includes(normalized);
1053
+ }
1054
+ function parseBoolean(value) {
1055
+ const normalized = value.toLowerCase().trim();
1056
+ return ["true", "1", "yes"].includes(normalized);
1057
+ }
1058
+ function validateEnum(value, allowed, caseInsensitive = false) {
1059
+ if (caseInsensitive) {
1060
+ const normalizedValue = value.toLowerCase();
1061
+ const normalizedAllowed = allowed.map((v) => v.toLowerCase());
1062
+ return normalizedAllowed.includes(normalizedValue);
1063
+ }
1064
+ return allowed.includes(value);
1065
+ }
1066
+ function createEnumValidator(allowed, caseInsensitive = false) {
1067
+ return (value) => validateEnum(value, allowed, caseInsensitive);
1068
+ }
1069
+ function validatePattern(value, pattern) {
1070
+ return pattern.test(value);
1071
+ }
1072
+ function createPatternValidator(pattern) {
1073
+ return (value) => validatePattern(value, pattern);
1074
+ }
1075
+ function validateNotEmpty(value) {
1076
+ return value.trim().length > 0;
1077
+ }
1078
+ function validateMinLength(value, minLength) {
1079
+ return value.length >= minLength;
1080
+ }
1081
+ function createMinLengthValidator(minLength) {
1082
+ return (value) => validateMinLength(value, minLength);
1083
+ }
1084
+ function combineValidators(validators) {
1085
+ return (value) => validators.every((validator) => validator(value));
1086
+ }
1087
+ function validatePostgresUrl(value) {
1088
+ try {
1089
+ const url = new URL(value);
1090
+ return url.protocol === "postgres:" || url.protocol === "postgresql:";
1091
+ } catch {
1092
+ return false;
1093
+ }
1094
+ }
1095
+ function validateRedisUrl(value) {
1096
+ try {
1097
+ const url = new URL(value);
1098
+ return url.protocol === "redis:" || url.protocol === "rediss:";
1099
+ } catch {
1100
+ return false;
1101
+ }
1102
+ }
1103
+
1104
+ 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 };
1105
+ //# sourceMappingURL=index.js.map
1106
+ //# sourceMappingURL=index.js.map