@spfn/core 0.2.0-beta.66 → 0.2.0-beta.68

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 (37) hide show
  1. package/README.md +309 -116
  2. package/dist/authz/index.js +398 -3
  3. package/dist/authz/index.js.map +1 -1
  4. package/dist/codegen/index.d.ts +114 -8
  5. package/dist/codegen/index.js +162 -3
  6. package/dist/codegen/index.js.map +1 -1
  7. package/dist/config/index.js +1 -1
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/contract/index.d.ts +288 -0
  10. package/dist/contract/index.js +534 -0
  11. package/dist/contract/index.js.map +1 -0
  12. package/dist/db/index.js +13 -10
  13. package/dist/db/index.js.map +1 -1
  14. package/dist/{define-middleware-DuXD8Hvu.d.ts → define-middleware-B9bFuXVU.d.ts} +1 -1
  15. package/dist/errors/index.js +398 -3
  16. package/dist/errors/index.js.map +1 -1
  17. package/dist/event/index.d.ts +3 -3
  18. package/dist/event/sse/client.d.ts +2 -2
  19. package/dist/event/sse/index.d.ts +4 -4
  20. package/dist/event/sse/index.js +9 -0
  21. package/dist/event/sse/index.js.map +1 -1
  22. package/dist/event/ws/client.d.ts +2 -2
  23. package/dist/event/ws/index.d.ts +3 -3
  24. package/dist/middleware/index.d.ts +108 -11
  25. package/dist/middleware/index.js +769 -632
  26. package/dist/middleware/index.js.map +1 -1
  27. package/dist/route/index.d.ts +8 -552
  28. package/dist/route/index.js +36 -0
  29. package/dist/route/index.js.map +1 -1
  30. package/dist/router-DhvbMhef.d.ts +641 -0
  31. package/dist/server/index.d.ts +3 -3
  32. package/dist/server/index.js +9 -0
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/{token-manager-jKD_EsSE.d.ts → token-manager-vZeqBbtA.d.ts} +7 -0
  35. package/dist/{types-DVjf37yO.d.ts → types-CF-37KAG.d.ts} +1 -1
  36. package/dist/{types-BFB72jbM.d.ts → types-D9uMxeQS.d.ts} +1 -1
  37. package/package.json +11 -9
@@ -1,11 +1,25 @@
1
+ import { randomBytes, createHmac, createHash, timingSafeEqual } from 'crypto';
1
2
  import { SerializableError } from '@spfn/core/errors';
2
3
  import { logger } from '@spfn/core/logger';
3
4
  import { env } from '@spfn/core/config';
4
- import { randomBytes, createHmac, createHash, timingSafeEqual } from 'crypto';
5
5
  import { format } from 'util';
6
6
 
7
7
  // src/middleware/error-handler.ts
8
8
  var errorLogger = logger.child("@spfn/core:error-handler");
9
+ function withEnvelope(body, c) {
10
+ return {
11
+ ...body,
12
+ error: {
13
+ code: body.__type,
14
+ message: body.message,
15
+ requestId: resolveRequestId(c)
16
+ }
17
+ };
18
+ }
19
+ function resolveRequestId(c) {
20
+ const existing = c.get("requestId");
21
+ return existing ?? randomBytes(16).toString("hex");
22
+ }
9
23
  function extractCauseMessage(err) {
10
24
  const cause = err.cause;
11
25
  if (!cause) {
@@ -86,8 +100,15 @@ function ErrorHandler(options = {}) {
86
100
  const {
87
101
  includeStack = env.NODE_ENV !== "production",
88
102
  enableLogging = true,
103
+ errorEnvelope = true,
89
104
  onError
90
105
  } = options;
106
+ const respond = (c, body, statusCode) => {
107
+ return c.json(
108
+ errorEnvelope ? withEnvelope(body, c) : body,
109
+ statusCode
110
+ );
111
+ };
91
112
  return (err, c) => {
92
113
  const path = c.req.path;
93
114
  const method = c.req.method;
@@ -109,7 +130,8 @@ function ErrorHandler(options = {}) {
109
130
  Promise.resolve(onError(err, ctx)).catch((e) => errorLogger.warn("onError callback failed", e));
110
131
  }
111
132
  if (err.internal === true && !includeStack) {
112
- return c.json(
133
+ return respond(
134
+ c,
113
135
  { __type: err.constructor.name, message: "Internal server error" },
114
136
  statusCode2
115
137
  );
@@ -118,7 +140,7 @@ function ErrorHandler(options = {}) {
118
140
  if (includeStack && err.stack) {
119
141
  serialized.stack = err.stack;
120
142
  }
121
- return c.json(serialized, statusCode2);
143
+ return respond(c, serialized, statusCode2);
122
144
  }
123
145
  const errorWithCode = err;
124
146
  const statusCode = errorWithCode.statusCode || 500;
@@ -146,7 +168,7 @@ function ErrorHandler(options = {}) {
146
168
  if (includeStack && err.stack) {
147
169
  response.stack = err.stack;
148
170
  }
149
- return c.json(response, statusCode);
171
+ return respond(c, response, statusCode);
150
172
  };
151
173
  }
152
174
  var PROXY_SIGNATURE_HEADER = "x-spfn-proxy-signature";
@@ -368,7 +390,8 @@ var ErrorRegistry = class _ErrorRegistry {
368
390
  if (!ErrorClass) {
369
391
  throw new Error(`Unknown error type: ${data.__type}`);
370
392
  }
371
- return new ErrorClass(data);
393
+ const { __type: _type, error: _envelope, ...fields } = data;
394
+ return new ErrorClass(fields);
372
395
  }
373
396
  /**
374
397
  * Try to deserialize error, return null if type unknown
@@ -394,705 +417,801 @@ var ErrorRegistry = class _ErrorRegistry {
394
417
  }
395
418
  };
396
419
 
397
- // src/errors/serializable-error.ts
398
- var SerializableError2 = class extends Error {
399
- /**
400
- * Serialize error to JSON-compatible object
401
- *
402
- * Automatically includes:
403
- * - __type: Constructor name for deserialization
404
- * - message: Error message
405
- * - All public instance properties (except name, stack)
406
- */
407
- toJSON() {
408
- const json = {
409
- __type: this.constructor.name,
410
- message: this.message
411
- };
412
- for (const key of Object.keys(this)) {
413
- if (key !== "name" && key !== "message" && key !== "stack" && key !== "statusCode") {
414
- json[key] = this[key];
415
- }
416
- }
417
- return json;
418
- }
420
+ // src/logger/types.ts
421
+ var LOG_LEVEL_PRIORITY = {
422
+ debug: 0,
423
+ info: 1,
424
+ warn: 2,
425
+ error: 3,
426
+ fatal: 4
419
427
  };
420
428
 
421
- // src/errors/http-errors.ts
422
- var HttpError = class extends SerializableError2 {
423
- statusCode;
424
- details;
425
- constructor(data) {
426
- super(data.message);
427
- this.name = "HttpError";
428
- this.statusCode = data.statusCode;
429
- if (data.details) {
430
- this.details = data.details;
431
- }
432
- Error.captureStackTrace(this, this.constructor);
433
- }
434
- };
435
- var BadRequestError = class extends HttpError {
436
- constructor(data = {}) {
437
- super({
438
- message: data.message || "Bad request",
439
- statusCode: 400,
440
- details: data.details
441
- });
442
- this.name = "BadRequestError";
429
+ // src/logger/formatters.ts
430
+ var SENSITIVE_KEYS = [
431
+ "password",
432
+ "passwd",
433
+ "pwd",
434
+ "secret",
435
+ "token",
436
+ "apikey",
437
+ "api_key",
438
+ "accesstoken",
439
+ "access_token",
440
+ "refreshtoken",
441
+ "refresh_token",
442
+ "authorization",
443
+ "auth",
444
+ "cookie",
445
+ "session",
446
+ "sessionid",
447
+ "session_id",
448
+ "privatekey",
449
+ "private_key",
450
+ "creditcard",
451
+ "credit_card",
452
+ "cardnumber",
453
+ "card_number",
454
+ "cvv",
455
+ "ssn",
456
+ "pin"
457
+ ];
458
+ var MASKED_VALUE = "***MASKED***";
459
+ function isSensitiveKey(key) {
460
+ const lowerKey = key.toLowerCase();
461
+ return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
462
+ }
463
+ function maskSensitiveData(data, seen = /* @__PURE__ */ new WeakSet()) {
464
+ if (data === null || data === void 0) {
465
+ return data;
443
466
  }
444
- };
445
- var ValidationError = class extends HttpError {
446
- fields;
447
- constructor(data) {
448
- super({
449
- message: data.message,
450
- statusCode: 400,
451
- details: data.details
452
- });
453
- this.name = "ValidationError";
454
- if (data.fields) {
455
- this.fields = data.fields;
456
- }
467
+ if (typeof data !== "object") {
468
+ return data;
457
469
  }
458
- };
459
- var UnauthorizedError = class extends HttpError {
460
- constructor(data = {}) {
461
- super({
462
- message: data.message || "Authentication required",
463
- statusCode: 401,
464
- details: data.details
465
- });
466
- this.name = "UnauthorizedError";
470
+ if (seen.has(data)) {
471
+ return "[Circular]";
467
472
  }
468
- };
469
- var ForbiddenError = class extends HttpError {
470
- constructor(data = {}) {
471
- super({
472
- message: data.message || "Access forbidden",
473
- statusCode: 403,
474
- details: data.details
475
- });
476
- this.name = "ForbiddenError";
473
+ seen.add(data);
474
+ if (Array.isArray(data)) {
475
+ return data.map((item) => maskSensitiveData(item, seen));
477
476
  }
478
- };
479
- var NotFoundError = class extends HttpError {
480
- resource;
481
- constructor(data = {}) {
482
- super({
483
- message: data.message || "Resource not found",
484
- statusCode: 404,
485
- details: data.details
486
- });
487
- this.name = "NotFoundError";
488
- if (data.resource) {
489
- this.resource = data.resource;
477
+ const masked = {};
478
+ for (const [key, value] of Object.entries(data)) {
479
+ if (isSensitiveKey(key)) {
480
+ masked[key] = MASKED_VALUE;
481
+ } else if (typeof value === "object" && value !== null) {
482
+ masked[key] = maskSensitiveData(value, seen);
483
+ } else {
484
+ masked[key] = value;
490
485
  }
491
486
  }
487
+ return masked;
488
+ }
489
+ var COLORS = {
490
+ reset: "\x1B[0m",
491
+ bright: "\x1B[1m",
492
+ dim: "\x1B[2m",
493
+ // 로그 레벨 컬러
494
+ debug: "\x1B[36m",
495
+ // cyan
496
+ info: "\x1B[32m",
497
+ // green
498
+ warn: "\x1B[33m",
499
+ // yellow
500
+ error: "\x1B[31m",
501
+ // red
502
+ fatal: "\x1B[35m",
503
+ // magenta
504
+ // 추가 컬러
505
+ gray: "\x1B[90m"
492
506
  };
493
- var ConflictError = class extends HttpError {
494
- constructor(data = {}) {
495
- super({
496
- message: data.message || "Resource conflict",
497
- statusCode: 409,
498
- details: data.details
499
- });
500
- this.name = "ConflictError";
507
+ function formatTimestampHuman(date) {
508
+ const year = date.getFullYear();
509
+ const month = String(date.getMonth() + 1).padStart(2, "0");
510
+ const day = String(date.getDate()).padStart(2, "0");
511
+ const hours = String(date.getHours()).padStart(2, "0");
512
+ const minutes = String(date.getMinutes()).padStart(2, "0");
513
+ const seconds = String(date.getSeconds()).padStart(2, "0");
514
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
515
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
516
+ }
517
+ function formatError(error) {
518
+ const lines = [];
519
+ lines.push(`${error.name}: ${error.message}`);
520
+ if (error.stack) {
521
+ const stackLines = error.stack.split("\n").slice(1);
522
+ lines.push(...stackLines);
501
523
  }
502
- };
503
- var GoneError = class extends HttpError {
504
- resource;
505
- constructor(data = {}) {
506
- super({
507
- message: data.message || "Resource permanently deleted",
508
- statusCode: 410,
509
- details: data.details
510
- });
511
- this.name = "GoneError";
512
- if (data.resource) {
513
- this.resource = data.resource;
514
- }
524
+ if (error.cause instanceof Error) {
525
+ lines.push(`Caused by: ${formatError(error.cause)}`);
526
+ } else if (error.cause !== void 0) {
527
+ lines.push(`Caused by: ${String(error.cause)}`);
515
528
  }
516
- };
517
- var TooManyRequestsError = class extends HttpError {
518
- retryAfter;
519
- constructor(data = {}) {
520
- super({
521
- message: data.message || "Too many requests",
522
- statusCode: 429,
523
- details: data.details
524
- });
525
- this.name = "TooManyRequestsError";
526
- if (data.retryAfter) {
527
- this.retryAfter = data.retryAfter;
528
- }
529
+ return lines.join("\n");
530
+ }
531
+ function formatConsole(metadata, colorize = true) {
532
+ const parts = [];
533
+ const timestamp = formatTimestampHuman(metadata.timestamp);
534
+ if (colorize) {
535
+ parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
536
+ } else {
537
+ parts.push(`[${timestamp}]`);
529
538
  }
530
- };
531
- var InternalServerError = class extends HttpError {
532
- constructor(data = {}) {
533
- super({
534
- message: data.message || "Internal server error",
535
- statusCode: 500,
536
- details: data.details
537
- });
538
- this.name = "InternalServerError";
539
+ const pid = process.pid;
540
+ if (colorize) {
541
+ parts.push(`${COLORS.dim}[pid=${pid}]${COLORS.reset}`);
542
+ } else {
543
+ parts.push(`[pid=${pid}]`);
539
544
  }
540
- };
541
- var UnsupportedMediaTypeError = class extends HttpError {
542
- mediaType;
543
- supportedTypes;
544
- constructor(data = {}) {
545
- super({
546
- message: data.message || "Unsupported media type",
547
- statusCode: 415,
548
- details: data.details
549
- });
550
- this.name = "UnsupportedMediaTypeError";
551
- if (data.mediaType) {
552
- this.mediaType = data.mediaType;
553
- }
554
- if (data.supportedTypes) {
555
- this.supportedTypes = data.supportedTypes;
545
+ if (metadata.module) {
546
+ if (colorize) {
547
+ parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
548
+ } else {
549
+ parts.push(`[module=${metadata.module}]`);
556
550
  }
557
551
  }
558
- };
559
- var UnprocessableEntityError = class extends HttpError {
560
- constructor(data = {}) {
561
- super({
562
- message: data.message || "Unprocessable entity",
563
- statusCode: 422,
564
- details: data.details
552
+ if (metadata.context && Object.keys(metadata.context).length > 0) {
553
+ Object.entries(metadata.context).forEach(([key, value]) => {
554
+ let valueStr;
555
+ if (typeof value === "string") {
556
+ valueStr = value;
557
+ } else if (typeof value === "object" && value !== null) {
558
+ try {
559
+ valueStr = JSON.stringify(value);
560
+ } catch (error) {
561
+ valueStr = "[circular]";
562
+ }
563
+ } else {
564
+ valueStr = String(value);
565
+ }
566
+ if (colorize) {
567
+ parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
568
+ } else {
569
+ parts.push(`[${key}=${valueStr}]`);
570
+ }
565
571
  });
566
- this.name = "UnprocessableEntityError";
567
572
  }
568
- };
569
- var ServiceUnavailableError = class extends HttpError {
570
- retryAfter;
571
- constructor(data = {}) {
572
- super({
573
- message: data.message || "Service unavailable",
574
- statusCode: 503,
575
- details: data.details
576
- });
577
- this.name = "ServiceUnavailableError";
578
- if (data.retryAfter) {
579
- this.retryAfter = data.retryAfter;
580
- }
573
+ const levelStr = metadata.level.toUpperCase();
574
+ if (colorize) {
575
+ const color = COLORS[metadata.level];
576
+ parts.push(`${color}(${levelStr})${COLORS.reset}:`);
577
+ } else {
578
+ parts.push(`(${levelStr}):`);
581
579
  }
582
- };
580
+ if (colorize) {
581
+ parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
582
+ } else {
583
+ parts.push(metadata.message);
584
+ }
585
+ let output = parts.join(" ");
586
+ if (metadata.error) {
587
+ output += "\n" + formatError(metadata.error);
588
+ }
589
+ return output;
590
+ }
583
591
 
584
- // src/errors/database-errors.ts
585
- var DatabaseError = class extends SerializableError2 {
586
- statusCode;
587
- details;
588
- constructor(data) {
589
- super(data.message);
590
- this.name = "DatabaseError";
591
- this.statusCode = data.statusCode ?? 500;
592
- this.details = data.details;
593
- Error.captureStackTrace(this, this.constructor);
592
+ // src/logger/logger.ts
593
+ var FORMAT_PATTERN = /%[sdifjoOc%]/;
594
+ var Logger = class _Logger {
595
+ config;
596
+ module;
597
+ constructor(config) {
598
+ this.config = config;
599
+ this.module = config.module;
594
600
  }
595
601
  /**
596
- * Whether this error's message/details are derived from the raw database
597
- * driver (SQL text, table/column/constraint names, parameter values) and
598
- * therefore must NOT be exposed to clients in production. Defined as a
599
- * prototype getter so it is never serialized into the response by toJSON().
600
- * Subclasses with a safe, constructed message override this to `false`.
602
+ * Convert unknown error to Error object
601
603
  */
602
- get internal() {
604
+ toError(error) {
605
+ if (error instanceof Error) return error;
606
+ if (typeof error === "string") return new Error(error);
607
+ if (typeof error === "object" && error !== null) {
608
+ return new Error(JSON.stringify(error));
609
+ }
610
+ return new Error(String(error));
611
+ }
612
+ /**
613
+ * Check if value is a context object (not an error)
614
+ */
615
+ isContext(value) {
616
+ if (typeof value !== "object" || value === null) return false;
617
+ if (value instanceof Error) return false;
618
+ const hasStack = "stack" in value && typeof value.stack === "string";
619
+ if (hasStack) {
620
+ return false;
621
+ }
603
622
  return true;
604
623
  }
605
- };
606
- var ConnectionError = class extends DatabaseError {
607
- constructor(data) {
608
- super({ message: data.message, statusCode: 503, details: data.details });
609
- this.name = "ConnectionError";
624
+ /**
625
+ * Get current log level
626
+ */
627
+ get level() {
628
+ return this.config.level;
610
629
  }
611
- };
612
- var QueryError = class extends DatabaseError {
613
- constructor(data) {
614
- super({
615
- message: data.message,
616
- statusCode: data.statusCode ?? 500,
617
- details: data.details
630
+ /**
631
+ * Create child logger (per module)
632
+ */
633
+ child(module) {
634
+ return new _Logger({
635
+ ...this.config,
636
+ module
618
637
  });
619
- this.name = "QueryError";
620
638
  }
621
- };
622
- var EntityNotFoundError = class extends QueryError {
623
- resource;
624
- id;
625
- constructor(data) {
626
- super({
627
- message: `${data.resource} with id ${data.id} not found`,
628
- statusCode: 404,
629
- details: { resource: data.resource, id: data.id }
630
- });
631
- this.name = "EntityNotFoundError";
632
- this.resource = data.resource;
633
- this.id = data.id;
639
+ /**
640
+ * Common log method with error/context detection
641
+ */
642
+ logWithLevel(level, message, errorOrContext, context) {
643
+ if (errorOrContext !== void 0 && FORMAT_PATTERN.test(message)) {
644
+ this.log(level, format(message, errorOrContext), void 0, context);
645
+ return;
646
+ }
647
+ if (errorOrContext instanceof Error) {
648
+ this.log(level, message, errorOrContext, context);
649
+ } else if (errorOrContext !== void 0 && typeof errorOrContext === "object" && !this.isContext(errorOrContext)) {
650
+ this.log(level, message, this.toError(errorOrContext), context);
651
+ } else if (typeof errorOrContext === "string" || typeof errorOrContext === "number" || typeof errorOrContext === "boolean") {
652
+ this.log(level, message, this.toError(errorOrContext), context);
653
+ } else {
654
+ this.log(level, message, void 0, errorOrContext);
655
+ }
634
656
  }
635
- // Message/details are constructed from the resource + id, not driver text
636
- get internal() {
637
- return false;
657
+ debug(message, errorOrContext, context) {
658
+ this.logWithLevel("debug", message, errorOrContext, context);
638
659
  }
639
- };
640
- var ConstraintViolationError = class extends QueryError {
641
- constructor(data) {
642
- super({ message: data.message, statusCode: 400, details: data.details });
643
- this.name = "ConstraintViolationError";
660
+ info(message, errorOrContext, context) {
661
+ this.logWithLevel("info", message, errorOrContext, context);
644
662
  }
645
- };
646
- var TransactionError = class extends DatabaseError {
647
- constructor(data) {
648
- super({
649
- message: data.message,
650
- statusCode: data.statusCode ?? 500,
651
- details: data.details
652
- });
653
- this.name = "TransactionError";
663
+ warn(message, errorOrContext, context) {
664
+ this.logWithLevel("warn", message, errorOrContext, context);
654
665
  }
655
- };
656
- var DeadlockError = class extends TransactionError {
657
- constructor(data) {
658
- super({ message: data.message, statusCode: 409, details: data.details });
659
- this.name = "DeadlockError";
666
+ error(message, errorOrContext, context) {
667
+ this.logWithLevel("error", message, errorOrContext, context);
660
668
  }
661
- };
662
- var DuplicateEntryError = class extends QueryError {
663
- field;
664
- value;
665
- constructor(data) {
666
- super({
667
- message: `${data.field} already exists`,
668
- statusCode: 409,
669
- details: { field: data.field }
669
+ fatal(message, errorOrContext, context) {
670
+ this.logWithLevel("fatal", message, errorOrContext, context);
671
+ }
672
+ /**
673
+ * Log processing (internal)
674
+ */
675
+ log(level, message, error, context) {
676
+ if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
677
+ return;
678
+ }
679
+ const metadata = {
680
+ timestamp: /* @__PURE__ */ new Date(),
681
+ level,
682
+ message,
683
+ module: this.module,
684
+ error,
685
+ // Mask sensitive information in context to prevent credential leaks
686
+ context: context ? maskSensitiveData(context) : void 0
687
+ };
688
+ this.processTransports(metadata);
689
+ }
690
+ /**
691
+ * Process Transports
692
+ */
693
+ processTransports(metadata) {
694
+ const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
695
+ Promise.all(promises).catch((error) => {
696
+ const errorMessage = error instanceof Error ? error.message : String(error);
697
+ process.stderr.write(`[Logger] Transport error: ${errorMessage}
698
+ `);
670
699
  });
671
- this.name = "DuplicateEntryError";
672
- this.field = data.field;
673
- this.value = data.value;
674
700
  }
675
- // Field-only message/details — safe to surface to the client (not internal)
676
- get internal() {
677
- return false;
701
+ /**
702
+ * Transport log (error-safe)
703
+ */
704
+ async safeTransportLog(transport, metadata) {
705
+ try {
706
+ await transport.log(metadata);
707
+ } catch (error) {
708
+ const errorMessage = error instanceof Error ? error.message : String(error);
709
+ process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
710
+ `);
711
+ }
712
+ }
713
+ /**
714
+ * Close all Transports
715
+ */
716
+ async close() {
717
+ const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
718
+ await Promise.all(closePromises);
678
719
  }
679
720
  };
680
721
 
681
- // src/errors/index.ts
682
- var errorRegistry = new ErrorRegistry();
683
- errorRegistry.append([
684
- HttpError,
685
- BadRequestError,
686
- ValidationError,
687
- UnauthorizedError,
688
- ForbiddenError,
689
- NotFoundError,
690
- ConflictError,
691
- GoneError,
692
- TooManyRequestsError,
693
- UnsupportedMediaTypeError,
694
- UnprocessableEntityError,
695
- InternalServerError,
696
- ServiceUnavailableError
697
- ]);
698
- errorRegistry.append([
699
- DatabaseError,
700
- ConnectionError,
701
- QueryError,
702
- EntityNotFoundError,
703
- ConstraintViolationError,
704
- TransactionError,
705
- DeadlockError,
706
- DuplicateEntryError
707
- ]);
708
-
709
- // src/logger/types.ts
710
- var LOG_LEVEL_PRIORITY = {
711
- debug: 0,
712
- info: 1,
713
- warn: 2,
714
- error: 3,
715
- fatal: 4
716
- };
717
-
718
- // src/logger/formatters.ts
719
- var SENSITIVE_KEYS = [
720
- "password",
721
- "passwd",
722
- "pwd",
723
- "secret",
724
- "token",
725
- "apikey",
726
- "api_key",
727
- "accesstoken",
728
- "access_token",
729
- "refreshtoken",
730
- "refresh_token",
731
- "authorization",
732
- "auth",
733
- "cookie",
734
- "session",
735
- "sessionid",
736
- "session_id",
737
- "privatekey",
738
- "private_key",
739
- "creditcard",
740
- "credit_card",
741
- "cardnumber",
742
- "card_number",
743
- "cvv",
744
- "ssn",
745
- "pin"
746
- ];
747
- var MASKED_VALUE = "***MASKED***";
748
- function isSensitiveKey(key) {
749
- const lowerKey = key.toLowerCase();
750
- return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
751
- }
752
- function maskSensitiveData(data, seen = /* @__PURE__ */ new WeakSet()) {
753
- if (data === null || data === void 0) {
754
- return data;
755
- }
756
- if (typeof data !== "object") {
757
- return data;
758
- }
759
- if (seen.has(data)) {
760
- return "[Circular]";
761
- }
762
- seen.add(data);
763
- if (Array.isArray(data)) {
764
- return data.map((item) => maskSensitiveData(item, seen));
722
+ // src/logger/transports/console.ts
723
+ var ConsoleTransport = class {
724
+ name = "console";
725
+ level;
726
+ enabled;
727
+ colorize;
728
+ constructor(config) {
729
+ this.level = config.level;
730
+ this.enabled = config.enabled;
731
+ this.colorize = config.colorize ?? true;
765
732
  }
766
- const masked = {};
767
- for (const [key, value] of Object.entries(data)) {
768
- if (isSensitiveKey(key)) {
769
- masked[key] = MASKED_VALUE;
770
- } else if (typeof value === "object" && value !== null) {
771
- masked[key] = maskSensitiveData(value, seen);
733
+ async log(metadata) {
734
+ if (!this.enabled) {
735
+ return;
736
+ }
737
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
738
+ return;
739
+ }
740
+ const message = formatConsole(metadata, this.colorize);
741
+ if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
742
+ console.error(message);
772
743
  } else {
773
- masked[key] = value;
744
+ console.log(message);
774
745
  }
775
746
  }
776
- return masked;
777
- }
778
- var COLORS = {
779
- reset: "\x1B[0m",
780
- bright: "\x1B[1m",
781
- dim: "\x1B[2m",
782
- // 로그 레벨 컬러
783
- debug: "\x1B[36m",
784
- // cyan
785
- info: "\x1B[32m",
786
- // green
787
- warn: "\x1B[33m",
788
- // yellow
789
- error: "\x1B[31m",
790
- // red
791
- fatal: "\x1B[35m",
792
- // magenta
793
- // 추가 컬러
794
- gray: "\x1B[90m"
795
747
  };
796
- function formatTimestampHuman(date) {
797
- const year = date.getFullYear();
798
- const month = String(date.getMonth() + 1).padStart(2, "0");
799
- const day = String(date.getDate()).padStart(2, "0");
800
- const hours = String(date.getHours()).padStart(2, "0");
801
- const minutes = String(date.getMinutes()).padStart(2, "0");
802
- const seconds = String(date.getSeconds()).padStart(2, "0");
803
- const ms = String(date.getMilliseconds()).padStart(3, "0");
804
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
748
+
749
+ // src/logger/config.ts
750
+ function getConsoleConfig() {
751
+ const isProduction = process.env.NODE_ENV === "production";
752
+ return {
753
+ level: "debug",
754
+ enabled: true,
755
+ colorize: !isProduction
756
+ // Dev: colored output, Production: plain text
757
+ };
805
758
  }
806
- function formatError(error) {
807
- const lines = [];
808
- lines.push(`${error.name}: ${error.message}`);
809
- if (error.stack) {
810
- const stackLines = error.stack.split("\n").slice(1);
811
- lines.push(...stackLines);
812
- }
813
- if (error.cause instanceof Error) {
814
- lines.push(`Caused by: ${formatError(error.cause)}`);
815
- } else if (error.cause !== void 0) {
816
- lines.push(`Caused by: ${String(error.cause)}`);
759
+ function validateEnvironment() {
760
+ const nodeEnv = process.env.NODE_ENV;
761
+ if (!nodeEnv) {
762
+ process.stderr.write(
763
+ "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
764
+ );
817
765
  }
818
- return lines.join("\n");
819
766
  }
820
- function formatConsole(metadata, colorize = true) {
821
- const parts = [];
822
- const timestamp = formatTimestampHuman(metadata.timestamp);
823
- if (colorize) {
824
- parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
825
- } else {
826
- parts.push(`[${timestamp}]`);
767
+ function validateConfig() {
768
+ validateEnvironment();
769
+ }
770
+
771
+ // src/logger/factory.ts
772
+ function initializeTransports() {
773
+ const transports = [];
774
+ const consoleConfig = getConsoleConfig();
775
+ transports.push(new ConsoleTransport(consoleConfig));
776
+ return transports;
777
+ }
778
+ function getLogLevel() {
779
+ const envLevel = process.env.SPFN_LOG_LEVEL || process.env.NEXT_PUBLIC_SPFN_LOG_LEVEL || "info";
780
+ if (envLevel in LOG_LEVEL_PRIORITY) {
781
+ return envLevel;
827
782
  }
828
- const pid = process.pid;
829
- if (colorize) {
830
- parts.push(`${COLORS.dim}[pid=${pid}]${COLORS.reset}`);
831
- } else {
832
- parts.push(`[pid=${pid}]`);
783
+ process.stderr.write(
784
+ `[Logger] Invalid log level "${envLevel}", defaulting to "info"
785
+ `
786
+ );
787
+ return "info";
788
+ }
789
+ function initializeLogger() {
790
+ validateConfig();
791
+ return new Logger({
792
+ level: getLogLevel(),
793
+ transports: initializeTransports()
794
+ });
795
+ }
796
+ var logger4 = initializeLogger();
797
+
798
+ // src/errors/serializable-error.ts
799
+ var RESERVED_RESPONSE_KEYS = /* @__PURE__ */ new Set(["__type", "message", "error"]);
800
+ var AUTHORING_ENVIRONMENTS = /* @__PURE__ */ new Set(["local", "development", "test"]);
801
+ function refuseReservedKey(className, key) {
802
+ const detail = `${className} declares the reserved field "${key}". Reserved response keys: ${[...RESERVED_RESPONSE_KEYS].join(", ")}. Rename the field.`;
803
+ if (AUTHORING_ENVIRONMENTS.has(process.env.NODE_ENV ?? "local")) {
804
+ throw new Error(detail);
805
+ }
806
+ logger4.error(`[SerializableError] ${detail} The field is omitted from the response.`);
807
+ }
808
+ var SerializableError2 = class extends Error {
809
+ /**
810
+ * Serialize error to JSON-compatible object
811
+ *
812
+ * Automatically includes:
813
+ * - __type: Constructor name for deserialization
814
+ * - message: Error message
815
+ * - All public instance properties (except name, stack)
816
+ */
817
+ toJSON() {
818
+ const json = {
819
+ __type: this.constructor.name,
820
+ message: this.message
821
+ };
822
+ for (const key of Object.keys(this)) {
823
+ if (key === "name" || key === "stack" || key === "statusCode") {
824
+ continue;
825
+ }
826
+ if (RESERVED_RESPONSE_KEYS.has(key)) {
827
+ refuseReservedKey(this.constructor.name, key);
828
+ continue;
829
+ }
830
+ json[key] = this[key];
831
+ }
832
+ return json;
833
833
  }
834
- if (metadata.module) {
835
- if (colorize) {
836
- parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
837
- } else {
838
- parts.push(`[module=${metadata.module}]`);
834
+ };
835
+
836
+ // src/errors/http-errors.ts
837
+ var HttpError = class extends SerializableError2 {
838
+ statusCode;
839
+ details;
840
+ constructor(data) {
841
+ super(data.message);
842
+ this.name = "HttpError";
843
+ this.statusCode = data.statusCode;
844
+ if (data.details) {
845
+ this.details = data.details;
839
846
  }
847
+ Error.captureStackTrace(this, this.constructor);
840
848
  }
841
- if (metadata.context && Object.keys(metadata.context).length > 0) {
842
- Object.entries(metadata.context).forEach(([key, value]) => {
843
- let valueStr;
844
- if (typeof value === "string") {
845
- valueStr = value;
846
- } else if (typeof value === "object" && value !== null) {
847
- try {
848
- valueStr = JSON.stringify(value);
849
- } catch (error) {
850
- valueStr = "[circular]";
851
- }
852
- } else {
853
- valueStr = String(value);
854
- }
855
- if (colorize) {
856
- parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
857
- } else {
858
- parts.push(`[${key}=${valueStr}]`);
859
- }
849
+ };
850
+ var BadRequestError = class extends HttpError {
851
+ constructor(data = {}) {
852
+ super({
853
+ message: data.message || "Bad request",
854
+ statusCode: 400,
855
+ details: data.details
860
856
  });
857
+ this.name = "BadRequestError";
861
858
  }
862
- const levelStr = metadata.level.toUpperCase();
863
- if (colorize) {
864
- const color = COLORS[metadata.level];
865
- parts.push(`${color}(${levelStr})${COLORS.reset}:`);
866
- } else {
867
- parts.push(`(${levelStr}):`);
868
- }
869
- if (colorize) {
870
- parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
871
- } else {
872
- parts.push(metadata.message);
859
+ };
860
+ var ValidationError = class extends HttpError {
861
+ fields;
862
+ constructor(data) {
863
+ super({
864
+ message: data.message,
865
+ statusCode: 400,
866
+ details: data.details
867
+ });
868
+ this.name = "ValidationError";
869
+ if (data.fields) {
870
+ this.fields = data.fields;
871
+ }
873
872
  }
874
- let output = parts.join(" ");
875
- if (metadata.error) {
876
- output += "\n" + formatError(metadata.error);
873
+ };
874
+ var UnauthorizedError = class extends HttpError {
875
+ constructor(data = {}) {
876
+ super({
877
+ message: data.message || "Authentication required",
878
+ statusCode: 401,
879
+ details: data.details
880
+ });
881
+ this.name = "UnauthorizedError";
877
882
  }
878
- return output;
879
- }
880
-
881
- // src/logger/logger.ts
882
- var FORMAT_PATTERN = /%[sdifjoOc%]/;
883
- var Logger = class _Logger {
884
- config;
885
- module;
886
- constructor(config) {
887
- this.config = config;
888
- this.module = config.module;
883
+ };
884
+ var ForbiddenError = class extends HttpError {
885
+ constructor(data = {}) {
886
+ super({
887
+ message: data.message || "Access forbidden",
888
+ statusCode: 403,
889
+ details: data.details
890
+ });
891
+ this.name = "ForbiddenError";
889
892
  }
890
- /**
891
- * Convert unknown error to Error object
892
- */
893
- toError(error) {
894
- if (error instanceof Error) return error;
895
- if (typeof error === "string") return new Error(error);
896
- if (typeof error === "object" && error !== null) {
897
- return new Error(JSON.stringify(error));
893
+ };
894
+ var NotFoundError = class extends HttpError {
895
+ resource;
896
+ constructor(data = {}) {
897
+ super({
898
+ message: data.message || "Resource not found",
899
+ statusCode: 404,
900
+ details: data.details
901
+ });
902
+ this.name = "NotFoundError";
903
+ if (data.resource) {
904
+ this.resource = data.resource;
898
905
  }
899
- return new Error(String(error));
900
906
  }
901
- /**
902
- * Check if value is a context object (not an error)
903
- */
904
- isContext(value) {
905
- if (typeof value !== "object" || value === null) return false;
906
- if (value instanceof Error) return false;
907
- const hasStack = "stack" in value && typeof value.stack === "string";
908
- if (hasStack) {
909
- return false;
907
+ };
908
+ var ConflictError = class extends HttpError {
909
+ constructor(data = {}) {
910
+ super({
911
+ message: data.message || "Resource conflict",
912
+ statusCode: 409,
913
+ details: data.details
914
+ });
915
+ this.name = "ConflictError";
916
+ }
917
+ };
918
+ var GoneError = class extends HttpError {
919
+ resource;
920
+ constructor(data = {}) {
921
+ super({
922
+ message: data.message || "Resource permanently deleted",
923
+ statusCode: 410,
924
+ details: data.details
925
+ });
926
+ this.name = "GoneError";
927
+ if (data.resource) {
928
+ this.resource = data.resource;
910
929
  }
911
- return true;
912
930
  }
913
- /**
914
- * Get current log level
915
- */
916
- get level() {
917
- return this.config.level;
931
+ };
932
+ var TooManyRequestsError = class extends HttpError {
933
+ retryAfter;
934
+ constructor(data = {}) {
935
+ super({
936
+ message: data.message || "Too many requests",
937
+ statusCode: 429,
938
+ details: data.details
939
+ });
940
+ this.name = "TooManyRequestsError";
941
+ if (data.retryAfter) {
942
+ this.retryAfter = data.retryAfter;
943
+ }
918
944
  }
919
- /**
920
- * Create child logger (per module)
921
- */
922
- child(module) {
923
- return new _Logger({
924
- ...this.config,
925
- module
945
+ };
946
+ var InternalServerError = class extends HttpError {
947
+ constructor(data = {}) {
948
+ super({
949
+ message: data.message || "Internal server error",
950
+ statusCode: 500,
951
+ details: data.details
926
952
  });
953
+ this.name = "InternalServerError";
927
954
  }
928
- /**
929
- * Common log method with error/context detection
930
- */
931
- logWithLevel(level, message, errorOrContext, context) {
932
- if (errorOrContext !== void 0 && FORMAT_PATTERN.test(message)) {
933
- this.log(level, format(message, errorOrContext), void 0, context);
934
- return;
955
+ };
956
+ var UnsupportedMediaTypeError = class extends HttpError {
957
+ mediaType;
958
+ supportedTypes;
959
+ constructor(data = {}) {
960
+ super({
961
+ message: data.message || "Unsupported media type",
962
+ statusCode: 415,
963
+ details: data.details
964
+ });
965
+ this.name = "UnsupportedMediaTypeError";
966
+ if (data.mediaType) {
967
+ this.mediaType = data.mediaType;
935
968
  }
936
- if (errorOrContext instanceof Error) {
937
- this.log(level, message, errorOrContext, context);
938
- } else if (errorOrContext !== void 0 && typeof errorOrContext === "object" && !this.isContext(errorOrContext)) {
939
- this.log(level, message, this.toError(errorOrContext), context);
940
- } else if (typeof errorOrContext === "string" || typeof errorOrContext === "number" || typeof errorOrContext === "boolean") {
941
- this.log(level, message, this.toError(errorOrContext), context);
942
- } else {
943
- this.log(level, message, void 0, errorOrContext);
969
+ if (data.supportedTypes) {
970
+ this.supportedTypes = data.supportedTypes;
944
971
  }
945
972
  }
946
- debug(message, errorOrContext, context) {
947
- this.logWithLevel("debug", message, errorOrContext, context);
973
+ };
974
+ var UnprocessableEntityError = class extends HttpError {
975
+ constructor(data = {}) {
976
+ super({
977
+ message: data.message || "Unprocessable entity",
978
+ statusCode: 422,
979
+ details: data.details
980
+ });
981
+ this.name = "UnprocessableEntityError";
982
+ }
983
+ };
984
+ var ServiceUnavailableError = class extends HttpError {
985
+ retryAfter;
986
+ constructor(data = {}) {
987
+ super({
988
+ message: data.message || "Service unavailable",
989
+ statusCode: 503,
990
+ details: data.details
991
+ });
992
+ this.name = "ServiceUnavailableError";
993
+ if (data.retryAfter) {
994
+ this.retryAfter = data.retryAfter;
995
+ }
996
+ }
997
+ };
998
+
999
+ // src/errors/database-errors.ts
1000
+ var DatabaseError = class extends SerializableError2 {
1001
+ statusCode;
1002
+ details;
1003
+ constructor(data) {
1004
+ super(data.message);
1005
+ this.name = "DatabaseError";
1006
+ this.statusCode = data.statusCode ?? 500;
1007
+ this.details = data.details;
1008
+ Error.captureStackTrace(this, this.constructor);
1009
+ }
1010
+ /**
1011
+ * Whether this error's message/details are derived from the raw database
1012
+ * driver (SQL text, table/column/constraint names, parameter values) and
1013
+ * therefore must NOT be exposed to clients in production. Defined as a
1014
+ * prototype getter so it is never serialized into the response by toJSON().
1015
+ * Subclasses with a safe, constructed message override this to `false`.
1016
+ */
1017
+ get internal() {
1018
+ return true;
948
1019
  }
949
- info(message, errorOrContext, context) {
950
- this.logWithLevel("info", message, errorOrContext, context);
1020
+ };
1021
+ var ConnectionError = class extends DatabaseError {
1022
+ constructor(data) {
1023
+ super({ message: data.message, statusCode: 503, details: data.details });
1024
+ this.name = "ConnectionError";
951
1025
  }
952
- warn(message, errorOrContext, context) {
953
- this.logWithLevel("warn", message, errorOrContext, context);
1026
+ };
1027
+ var QueryError = class extends DatabaseError {
1028
+ constructor(data) {
1029
+ super({
1030
+ message: data.message,
1031
+ statusCode: data.statusCode ?? 500,
1032
+ details: data.details
1033
+ });
1034
+ this.name = "QueryError";
954
1035
  }
955
- error(message, errorOrContext, context) {
956
- this.logWithLevel("error", message, errorOrContext, context);
1036
+ };
1037
+ var EntityNotFoundError = class extends QueryError {
1038
+ resource;
1039
+ id;
1040
+ constructor(data) {
1041
+ super({
1042
+ message: `${data.resource} with id ${data.id} not found`,
1043
+ statusCode: 404,
1044
+ details: { resource: data.resource, id: data.id }
1045
+ });
1046
+ this.name = "EntityNotFoundError";
1047
+ this.resource = data.resource;
1048
+ this.id = data.id;
957
1049
  }
958
- fatal(message, errorOrContext, context) {
959
- this.logWithLevel("fatal", message, errorOrContext, context);
1050
+ // Message/details are constructed from the resource + id, not driver text
1051
+ get internal() {
1052
+ return false;
960
1053
  }
961
- /**
962
- * Log processing (internal)
963
- */
964
- log(level, message, error, context) {
965
- if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
966
- return;
967
- }
968
- const metadata = {
969
- timestamp: /* @__PURE__ */ new Date(),
970
- level,
971
- message,
972
- module: this.module,
973
- error,
974
- // Mask sensitive information in context to prevent credential leaks
975
- context: context ? maskSensitiveData(context) : void 0
976
- };
977
- this.processTransports(metadata);
1054
+ };
1055
+ var ConstraintViolationError = class extends QueryError {
1056
+ constructor(data) {
1057
+ super({ message: data.message, statusCode: 400, details: data.details });
1058
+ this.name = "ConstraintViolationError";
978
1059
  }
979
- /**
980
- * Process Transports
981
- */
982
- processTransports(metadata) {
983
- const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
984
- Promise.all(promises).catch((error) => {
985
- const errorMessage = error instanceof Error ? error.message : String(error);
986
- process.stderr.write(`[Logger] Transport error: ${errorMessage}
987
- `);
1060
+ };
1061
+ var TransactionError = class extends DatabaseError {
1062
+ constructor(data) {
1063
+ super({
1064
+ message: data.message,
1065
+ statusCode: data.statusCode ?? 500,
1066
+ details: data.details
988
1067
  });
1068
+ this.name = "TransactionError";
989
1069
  }
990
- /**
991
- * Transport log (error-safe)
992
- */
993
- async safeTransportLog(transport, metadata) {
994
- try {
995
- await transport.log(metadata);
996
- } catch (error) {
997
- const errorMessage = error instanceof Error ? error.message : String(error);
998
- process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
999
- `);
1000
- }
1070
+ };
1071
+ var DeadlockError = class extends TransactionError {
1072
+ constructor(data) {
1073
+ super({ message: data.message, statusCode: 409, details: data.details });
1074
+ this.name = "DeadlockError";
1001
1075
  }
1002
- /**
1003
- * Close all Transports
1004
- */
1005
- async close() {
1006
- const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
1007
- await Promise.all(closePromises);
1076
+ };
1077
+ var DuplicateEntryError = class extends QueryError {
1078
+ field;
1079
+ value;
1080
+ constructor(data) {
1081
+ super({
1082
+ message: `${data.field} already exists`,
1083
+ statusCode: 409,
1084
+ details: { field: data.field }
1085
+ });
1086
+ this.name = "DuplicateEntryError";
1087
+ this.field = data.field;
1088
+ this.value = data.value;
1089
+ }
1090
+ // Field-only message/details — safe to surface to the client (not internal)
1091
+ get internal() {
1092
+ return false;
1008
1093
  }
1009
1094
  };
1010
1095
 
1011
- // src/logger/transports/console.ts
1012
- var ConsoleTransport = class {
1013
- name = "console";
1014
- level;
1015
- enabled;
1016
- colorize;
1017
- constructor(config) {
1018
- this.level = config.level;
1019
- this.enabled = config.enabled;
1020
- this.colorize = config.colorize ?? true;
1096
+ // src/errors/index.ts
1097
+ var errorRegistry = new ErrorRegistry();
1098
+ errorRegistry.append([
1099
+ HttpError,
1100
+ BadRequestError,
1101
+ ValidationError,
1102
+ UnauthorizedError,
1103
+ ForbiddenError,
1104
+ NotFoundError,
1105
+ ConflictError,
1106
+ GoneError,
1107
+ TooManyRequestsError,
1108
+ UnsupportedMediaTypeError,
1109
+ UnprocessableEntityError,
1110
+ InternalServerError,
1111
+ ServiceUnavailableError
1112
+ ]);
1113
+ errorRegistry.append([
1114
+ DatabaseError,
1115
+ ConnectionError,
1116
+ QueryError,
1117
+ EntityNotFoundError,
1118
+ ConstraintViolationError,
1119
+ TransactionError,
1120
+ DeadlockError,
1121
+ DuplicateEntryError
1122
+ ]);
1123
+
1124
+ // src/middleware/rate-limit-store.ts
1125
+ var FIXED_WINDOW_LUA = `
1126
+ local count = redis.call('INCR', KEYS[1])
1127
+ if count == 1 then
1128
+ redis.call('PEXPIRE', KEYS[1], ARGV[1])
1129
+ end
1130
+ return { count, redis.call('PTTL', KEYS[1]) }
1131
+ `;
1132
+ var CacheRateLimitStore = class {
1133
+ constructor(cache) {
1134
+ this.cache = cache;
1135
+ }
1136
+ async hit(key, windowMs) {
1137
+ const [count, pttl] = await this.cache.eval(
1138
+ FIXED_WINDOW_LUA,
1139
+ 1,
1140
+ key,
1141
+ String(windowMs)
1142
+ );
1143
+ return { count, pttl };
1021
1144
  }
1022
- async log(metadata) {
1023
- if (!this.enabled) {
1024
- return;
1145
+ };
1146
+ var MemoryRateLimitStore = class {
1147
+ constructor(maxKeys = 1e4) {
1148
+ this.maxKeys = maxKeys;
1149
+ }
1150
+ windows = /* @__PURE__ */ new Map();
1151
+ /** How many live windows were dropped to stay under `maxKeys`. */
1152
+ evicted = 0;
1153
+ async hit(key, windowMs) {
1154
+ const now = Date.now();
1155
+ const existing = this.windows.get(key);
1156
+ if (existing !== void 0 && existing.expiresAt > now) {
1157
+ existing.count += 1;
1158
+ return { count: existing.count, pttl: existing.expiresAt - now };
1025
1159
  }
1026
- if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
1160
+ if (this.windows.size >= this.maxKeys) {
1161
+ this.makeRoom(now);
1162
+ }
1163
+ this.windows.set(key, { count: 1, expiresAt: now + windowMs });
1164
+ return { count: 1, pttl: windowMs };
1165
+ }
1166
+ /** Drops every window that has already reset. */
1167
+ prune(nowMillis) {
1168
+ for (const [key, window] of this.windows) {
1169
+ if (window.expiresAt <= nowMillis) {
1170
+ this.windows.delete(key);
1171
+ }
1172
+ }
1173
+ }
1174
+ /**
1175
+ * Frees a slot for a new key.
1176
+ *
1177
+ * Expired windows go first. If every window is still live the oldest one is
1178
+ * dropped, because a hard bound is the only thing standing between a
1179
+ * per-IP limiter and unbounded memory. A dropped window loses its count,
1180
+ * so a caller who can mint `maxKeys` distinct identities can push another
1181
+ * one out — which is why `evictionCount` is worth watching: it says this
1182
+ * process is past what an in-memory limiter should be asked to hold, and
1183
+ * the deployment wants a real cache.
1184
+ */
1185
+ makeRoom(nowMillis) {
1186
+ this.prune(nowMillis);
1187
+ if (this.windows.size < this.maxKeys) {
1027
1188
  return;
1028
1189
  }
1029
- const message = formatConsole(metadata, this.colorize);
1030
- if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
1031
- console.error(message);
1032
- } else {
1033
- console.log(message);
1190
+ const oldest = this.windows.keys().next();
1191
+ if (!oldest.done) {
1192
+ this.windows.delete(oldest.value);
1193
+ this.evicted += 1;
1034
1194
  }
1035
1195
  }
1036
- };
1037
-
1038
- // src/logger/config.ts
1039
- function getConsoleConfig() {
1040
- const isProduction = process.env.NODE_ENV === "production";
1041
- return {
1042
- level: "debug",
1043
- enabled: true,
1044
- colorize: !isProduction
1045
- // Dev: colored output, Production: plain text
1046
- };
1047
- }
1048
- function validateEnvironment() {
1049
- const nodeEnv = process.env.NODE_ENV;
1050
- if (!nodeEnv) {
1051
- process.stderr.write(
1052
- "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
1053
- );
1196
+ get size() {
1197
+ return this.windows.size;
1054
1198
  }
1055
- }
1056
- function validateConfig() {
1057
- validateEnvironment();
1058
- }
1059
-
1060
- // src/logger/factory.ts
1061
- function initializeTransports() {
1062
- const transports = [];
1063
- const consoleConfig = getConsoleConfig();
1064
- transports.push(new ConsoleTransport(consoleConfig));
1065
- return transports;
1066
- }
1067
- function getLogLevel() {
1068
- const envLevel = process.env.SPFN_LOG_LEVEL || process.env.NEXT_PUBLIC_SPFN_LOG_LEVEL || "info";
1069
- if (envLevel in LOG_LEVEL_PRIORITY) {
1070
- return envLevel;
1199
+ /** Live windows dropped for capacity — nonzero means the bound is binding. */
1200
+ get evictionCount() {
1201
+ return this.evicted;
1071
1202
  }
1072
- process.stderr.write(
1073
- `[Logger] Invalid log level "${envLevel}", defaulting to "info"
1074
- `
1075
- );
1076
- return "info";
1077
- }
1078
- function initializeLogger() {
1079
- validateConfig();
1080
- return new Logger({
1081
- level: getLogLevel(),
1082
- transports: initializeTransports()
1083
- });
1084
- }
1085
- var logger4 = initializeLogger();
1203
+ clear() {
1204
+ this.windows.clear();
1205
+ this.evicted = 0;
1206
+ }
1207
+ };
1086
1208
 
1087
1209
  // src/middleware/rate-limit.ts
1088
1210
  var rateLimitLogger = logger4.child("@spfn/core:rate-limit");
1089
- var FIXED_WINDOW_LUA = `
1090
- local count = redis.call('INCR', KEYS[1])
1091
- if count == 1 then
1092
- redis.call('PEXPIRE', KEYS[1], ARGV[1])
1093
- end
1094
- return { count, redis.call('PTTL', KEYS[1]) }
1095
- `;
1211
+ var memoryStore = new MemoryRateLimitStore();
1212
+ function resetMemoryRateLimitStore() {
1213
+ memoryStore.clear();
1214
+ }
1096
1215
  function getClientIp(c) {
1097
1216
  const clientType = c.get("clientType");
1098
1217
  if (clientType && clientType !== "untrusted") {
@@ -1109,21 +1228,37 @@ function socketRemoteAddress(c) {
1109
1228
  const bindings = env4?.server ?? env4;
1110
1229
  return bindings?.incoming?.socket?.remoteAddress;
1111
1230
  }
1231
+ function selectStore(failClosed, message, path) {
1232
+ const cache = getCache();
1233
+ if (cache && !isCacheDisabled()) {
1234
+ return new CacheRateLimitStore(cache);
1235
+ }
1236
+ if (failClosed) {
1237
+ throw new TooManyRequestsError({ message: message || "Rate limiter unavailable" });
1238
+ }
1239
+ rateLimitLogger.debug("No cache \u2014 counting rate limits in this process", { path });
1240
+ return memoryStore;
1241
+ }
1242
+ async function hitWithFallback(store, key, windowMs, failClosed, message, path) {
1243
+ try {
1244
+ return await store.hit(key, windowMs);
1245
+ } catch (error) {
1246
+ if (failClosed) {
1247
+ throw new TooManyRequestsError({ message: message || "Rate limiter unavailable" });
1248
+ }
1249
+ rateLimitLogger.warn("Cache failed \u2014 counting this rate limit in the process instead", {
1250
+ path,
1251
+ error: error instanceof Error ? error.message : String(error)
1252
+ });
1253
+ return memoryStore.hit(key, windowMs);
1254
+ }
1255
+ }
1112
1256
  var rateLimit = defineMiddlewareFactory(
1113
1257
  "rateLimit",
1114
1258
  (options) => {
1115
1259
  const { limit, windowMs, scope, by, failClosed = false, message } = options;
1116
1260
  return async (c, next) => {
1117
- const cache = getCache();
1118
- if (!cache || isCacheDisabled()) {
1119
- if (failClosed) {
1120
- throw new TooManyRequestsError({ message: message || "Rate limiter unavailable" });
1121
- }
1122
- rateLimitLogger.warn("Cache unavailable \u2014 rate limit not enforced (fail-open)", {
1123
- path: c.req.path
1124
- });
1125
- return next();
1126
- }
1261
+ const store = selectStore(failClosed, message, c.req.path);
1127
1262
  const dimensions = (by ? await by(c) : [getClientIp(c)]).filter((d) => Boolean(d));
1128
1263
  const ns = scope || `${c.req.method} ${c.req.routePath || c.req.path}`;
1129
1264
  for (const dimension of dimensions) {
@@ -1132,11 +1267,13 @@ var rateLimit = defineMiddlewareFactory(
1132
1267
  continue;
1133
1268
  }
1134
1269
  const dimLimit = typeof dimension === "string" ? limit : dimension.limit ?? limit;
1135
- const [count, pttl] = await cache.eval(
1136
- FIXED_WINDOW_LUA,
1137
- 1,
1270
+ const { count, pttl } = await hitWithFallback(
1271
+ store,
1138
1272
  `ratelimit:${ns}:${key}`,
1139
- String(windowMs)
1273
+ windowMs,
1274
+ failClosed,
1275
+ message,
1276
+ c.req.path
1140
1277
  );
1141
1278
  if (count > dimLimit) {
1142
1279
  const retryAfter = Math.max(1, Math.ceil((pttl > 0 ? pttl : windowMs) / 1e3));
@@ -1464,6 +1601,6 @@ function reject(c, mode, next, reason) {
1464
1601
  return next();
1465
1602
  }
1466
1603
 
1467
- export { ErrorHandler, RequestLogger, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, getRateLimitPolicy, maskSensitiveData2 as maskSensitiveData, rateLimit, rateLimitPolicy, setRateLimitFailClosedDefault, setRateLimitPolicies };
1604
+ export { CacheRateLimitStore, ErrorHandler, MemoryRateLimitStore, RequestLogger, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, getRateLimitPolicy, maskSensitiveData2 as maskSensitiveData, rateLimit, rateLimitPolicy, resetMemoryRateLimitStore, setRateLimitFailClosedDefault, setRateLimitPolicies };
1468
1605
  //# sourceMappingURL=index.js.map
1469
1606
  //# sourceMappingURL=index.js.map