@spfn/core 0.2.0-beta.67 → 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.
- package/README.md +309 -116
- package/dist/authz/index.js +398 -3
- package/dist/authz/index.js.map +1 -1
- package/dist/codegen/index.d.ts +114 -8
- package/dist/codegen/index.js +162 -3
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +288 -0
- package/dist/contract/index.js +534 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/{define-middleware-DuXD8Hvu.d.ts → define-middleware-B9bFuXVU.d.ts} +1 -1
- package/dist/errors/index.js +398 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +4 -4
- package/dist/event/sse/index.js +9 -0
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/middleware/index.d.ts +108 -11
- package/dist/middleware/index.js +769 -632
- package/dist/middleware/index.js.map +1 -1
- package/dist/route/index.d.ts +8 -552
- package/dist/route/index.js +36 -0
- package/dist/route/index.js.map +1 -1
- package/dist/router-DhvbMhef.d.ts +641 -0
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -1
- package/dist/{token-manager-jKD_EsSE.d.ts → token-manager-vZeqBbtA.d.ts} +7 -0
- package/dist/{types-DVjf37yO.d.ts → types-CF-37KAG.d.ts} +1 -1
- package/dist/{types-BFB72jbM.d.ts → types-D9uMxeQS.d.ts} +1 -1
- package/package.json +11 -9
package/dist/middleware/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
398
|
-
var
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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/
|
|
422
|
-
var
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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/
|
|
585
|
-
var
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
this.
|
|
591
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
this.
|
|
624
|
+
/**
|
|
625
|
+
* Get current log level
|
|
626
|
+
*/
|
|
627
|
+
get level() {
|
|
628
|
+
return this.config.level;
|
|
610
629
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
return false;
|
|
657
|
+
debug(message, errorOrContext, context) {
|
|
658
|
+
this.logWithLevel("debug", message, errorOrContext, context);
|
|
638
659
|
}
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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/
|
|
682
|
-
var
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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 (
|
|
937
|
-
this.
|
|
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
|
-
|
|
947
|
-
|
|
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
|
-
|
|
950
|
-
|
|
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
|
-
|
|
953
|
-
|
|
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
|
-
|
|
956
|
-
|
|
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
|
-
|
|
959
|
-
|
|
1050
|
+
// Message/details are constructed from the resource + id, not driver text
|
|
1051
|
+
get internal() {
|
|
1052
|
+
return false;
|
|
960
1053
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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/
|
|
1012
|
-
var
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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 (
|
|
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
|
|
1030
|
-
if (
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
|
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
|
|
1136
|
-
|
|
1137
|
-
1,
|
|
1270
|
+
const { count, pttl } = await hitWithFallback(
|
|
1271
|
+
store,
|
|
1138
1272
|
`ratelimit:${ns}:${key}`,
|
|
1139
|
-
|
|
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
|