@spfn/core 0.2.0-beta.53 → 0.2.0-beta.54
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/dist/{boss-Cxqc-Oiw.d.ts → boss-CEik0yq-.d.ts} +7 -0
- package/dist/cache/index.js +10 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/config/index.d.ts +213 -0
- package/dist/config/index.js +43 -1
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +57 -0
- package/dist/db/index.js +55 -8
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DuXD8Hvu.d.ts +167 -0
- package/dist/env/index.js +4 -3
- package/dist/env/index.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +20 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +4 -4
- package/dist/event/sse/index.js +41 -16
- 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/event/ws/index.js +75 -16
- package/dist/event/ws/index.js.map +1 -1
- package/dist/job/index.d.ts +2 -2
- package/dist/job/index.js +4 -4
- package/dist/job/index.js.map +1 -1
- package/dist/middleware/index.d.ts +64 -2
- package/dist/middleware/index.js +1030 -96
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/server.js +17 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +3 -165
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +125 -39
- package/dist/server/index.js.map +1 -1
- package/dist/{token-manager-C2Ag5-s8.d.ts → token-manager-jKD_EsSE.d.ts} +0 -1
- package/dist/{types-VpVQIsyB.d.ts → types-BFB72jbM.d.ts} +1 -1
- package/dist/{types-CKsmzaB8.d.ts → types-DVjf37yO.d.ts} +37 -1
- package/package.json +6 -6
package/dist/middleware/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SerializableError } from '@spfn/core/errors';
|
|
|
2
2
|
import { logger } from '@spfn/core/logger';
|
|
3
3
|
import { env } from '@spfn/core/config';
|
|
4
4
|
import { randomBytes, createHmac, createHash, timingSafeEqual } from 'crypto';
|
|
5
|
+
import { format } from 'util';
|
|
5
6
|
|
|
6
7
|
// src/middleware/error-handler.ts
|
|
7
8
|
var errorLogger = logger.child("@spfn/core:error-handler");
|
|
@@ -26,6 +27,35 @@ function extractHeaders(c) {
|
|
|
26
27
|
});
|
|
27
28
|
return headers;
|
|
28
29
|
}
|
|
30
|
+
var SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set([
|
|
31
|
+
"token",
|
|
32
|
+
"access_token",
|
|
33
|
+
"refresh_token",
|
|
34
|
+
"id_token",
|
|
35
|
+
"code",
|
|
36
|
+
"secret",
|
|
37
|
+
"client_secret",
|
|
38
|
+
"password",
|
|
39
|
+
"passwd",
|
|
40
|
+
"pwd",
|
|
41
|
+
"api_key",
|
|
42
|
+
"apikey",
|
|
43
|
+
"key",
|
|
44
|
+
"signature",
|
|
45
|
+
"sig",
|
|
46
|
+
"session",
|
|
47
|
+
"sessionid",
|
|
48
|
+
"auth",
|
|
49
|
+
"authorization"
|
|
50
|
+
]);
|
|
51
|
+
function extractQuery(c) {
|
|
52
|
+
const query = c.req.query();
|
|
53
|
+
const masked = {};
|
|
54
|
+
for (const [key, value] of Object.entries(query)) {
|
|
55
|
+
masked[key] = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()) ? "***" : value;
|
|
56
|
+
}
|
|
57
|
+
return masked;
|
|
58
|
+
}
|
|
29
59
|
function buildOnErrorContext(c, statusCode) {
|
|
30
60
|
const auth = c.get("auth");
|
|
31
61
|
return {
|
|
@@ -37,7 +67,7 @@ function buildOnErrorContext(c, statusCode) {
|
|
|
37
67
|
userId: auth?.userId,
|
|
38
68
|
request: {
|
|
39
69
|
headers: extractHeaders(c),
|
|
40
|
-
query: c
|
|
70
|
+
query: extractQuery(c)
|
|
41
71
|
}
|
|
42
72
|
};
|
|
43
73
|
}
|
|
@@ -78,6 +108,12 @@ function ErrorHandler(options = {}) {
|
|
|
78
108
|
const ctx = buildOnErrorContext(c, statusCode2);
|
|
79
109
|
Promise.resolve(onError(err, ctx)).catch((e) => errorLogger.warn("onError callback failed", e));
|
|
80
110
|
}
|
|
111
|
+
if (err.internal === true && !includeStack) {
|
|
112
|
+
return c.json(
|
|
113
|
+
{ __type: err.constructor.name, message: "Internal server error" },
|
|
114
|
+
statusCode2
|
|
115
|
+
);
|
|
116
|
+
}
|
|
81
117
|
const serialized = err.toJSON();
|
|
82
118
|
if (includeStack && err.stack) {
|
|
83
119
|
serialized.stack = err.stack;
|
|
@@ -102,9 +138,9 @@ function ErrorHandler(options = {}) {
|
|
|
102
138
|
}
|
|
103
139
|
const response = {
|
|
104
140
|
__type: "Error",
|
|
105
|
-
message: err.message || "Internal Server Error"
|
|
141
|
+
message: includeStack ? err.message || "Internal Server Error" : "Internal Server Error"
|
|
106
142
|
};
|
|
107
|
-
if (causeMessage) {
|
|
143
|
+
if (causeMessage && includeStack) {
|
|
108
144
|
response.cause = causeMessage;
|
|
109
145
|
}
|
|
110
146
|
if (includeStack && err.stack) {
|
|
@@ -113,6 +149,964 @@ function ErrorHandler(options = {}) {
|
|
|
113
149
|
return c.json(response, statusCode);
|
|
114
150
|
};
|
|
115
151
|
}
|
|
152
|
+
var PROXY_SIGNATURE_HEADER = "x-spfn-proxy-signature";
|
|
153
|
+
var PROXY_TIMESTAMP_HEADER = "x-spfn-proxy-timestamp";
|
|
154
|
+
var PROXY_NONCE_HEADER = "x-spfn-proxy-nonce";
|
|
155
|
+
var PROXY_KEY_ID_HEADER = "x-spfn-proxy-key-id";
|
|
156
|
+
var PROXY_CLIENT_IP_HEADER = "x-spfn-proxy-client-ip";
|
|
157
|
+
var DEFAULT_KEY_ID = "default";
|
|
158
|
+
function parseProxyKey(raw) {
|
|
159
|
+
const idx = raw.indexOf(":");
|
|
160
|
+
if (idx === -1) {
|
|
161
|
+
return { keyId: DEFAULT_KEY_ID, secret: raw };
|
|
162
|
+
}
|
|
163
|
+
return { keyId: raw.slice(0, idx) || DEFAULT_KEY_ID, secret: raw.slice(idx + 1) };
|
|
164
|
+
}
|
|
165
|
+
function parseProxyKeySet(raws) {
|
|
166
|
+
const keys = [];
|
|
167
|
+
const seen = /* @__PURE__ */ new Set();
|
|
168
|
+
for (const raw of raws) {
|
|
169
|
+
if (!raw) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
for (const part of raw.split(",")) {
|
|
173
|
+
const trimmed = part.trim();
|
|
174
|
+
if (!trimmed) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const key = parseProxyKey(trimmed);
|
|
178
|
+
if (!key.secret || seen.has(key.keyId)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
seen.add(key.keyId);
|
|
182
|
+
keys.push(key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return keys;
|
|
186
|
+
}
|
|
187
|
+
function buildCanonicalString(parts) {
|
|
188
|
+
return [parts.method.toUpperCase(), parts.path, parts.query, parts.timestamp, parts.nonce, parts.bodyHash].join("\n");
|
|
189
|
+
}
|
|
190
|
+
function hashBody(body) {
|
|
191
|
+
if (!body || body.length === 0) {
|
|
192
|
+
return "";
|
|
193
|
+
}
|
|
194
|
+
return typeof body === "string" ? createHash("sha256").update(body, "utf8").digest("hex") : createHash("sha256").update(body).digest("hex");
|
|
195
|
+
}
|
|
196
|
+
function computeSignature(secret, parts) {
|
|
197
|
+
return createHmac("sha256", secret).update(buildCanonicalString(parts)).digest("hex");
|
|
198
|
+
}
|
|
199
|
+
function safeEqualHex(a, b) {
|
|
200
|
+
const bufA = Buffer.from(a, "hex");
|
|
201
|
+
const bufB = Buffer.from(b, "hex");
|
|
202
|
+
if (bufA.length === 0 || bufA.length !== bufB.length) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return timingSafeEqual(bufA, bufB);
|
|
206
|
+
}
|
|
207
|
+
function verifyProxyRequest(input) {
|
|
208
|
+
const { signature, timestamp, nonce, keyId } = input;
|
|
209
|
+
if (!signature || !timestamp || !nonce || !keyId) {
|
|
210
|
+
return { valid: false, reason: "missing-headers" };
|
|
211
|
+
}
|
|
212
|
+
const ts = Number(timestamp);
|
|
213
|
+
if (!Number.isFinite(ts)) {
|
|
214
|
+
return { valid: false, reason: "bad-timestamp" };
|
|
215
|
+
}
|
|
216
|
+
const now = input.now ?? Date.now();
|
|
217
|
+
const windowMs = input.windowMs ?? 3e4;
|
|
218
|
+
if (Math.abs(now - ts) > windowMs) {
|
|
219
|
+
return { valid: false, reason: "stale-timestamp" };
|
|
220
|
+
}
|
|
221
|
+
const key = input.keys.find((k) => k.keyId === keyId);
|
|
222
|
+
if (!key) {
|
|
223
|
+
return { valid: false, reason: "unknown-key" };
|
|
224
|
+
}
|
|
225
|
+
const expected = computeSignature(key.secret, {
|
|
226
|
+
method: input.method,
|
|
227
|
+
path: input.path,
|
|
228
|
+
query: input.query ?? "",
|
|
229
|
+
timestamp,
|
|
230
|
+
nonce,
|
|
231
|
+
bodyHash: hashBody(input.body)
|
|
232
|
+
});
|
|
233
|
+
if (!safeEqualHex(signature, expected)) {
|
|
234
|
+
return { valid: false, reason: "signature-mismatch" };
|
|
235
|
+
}
|
|
236
|
+
return { valid: true, nonce, keyId };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/route/define-middleware.ts
|
|
240
|
+
function defineMiddlewareFactory(name, factory) {
|
|
241
|
+
const wrapper = (...args) => factory(...args);
|
|
242
|
+
Object.defineProperty(wrapper, "name", {
|
|
243
|
+
value: name,
|
|
244
|
+
writable: false,
|
|
245
|
+
enumerable: false,
|
|
246
|
+
configurable: true
|
|
247
|
+
});
|
|
248
|
+
Object.defineProperty(wrapper, "_name", {
|
|
249
|
+
value: name,
|
|
250
|
+
writable: false,
|
|
251
|
+
enumerable: false,
|
|
252
|
+
configurable: true
|
|
253
|
+
});
|
|
254
|
+
return wrapper;
|
|
255
|
+
}
|
|
256
|
+
logger.child("@spfn/core:cache");
|
|
257
|
+
logger.child("@spfn/core:cache");
|
|
258
|
+
var CACHE_KEY = Symbol.for("@spfn/core:cache");
|
|
259
|
+
var state = globalThis[CACHE_KEY] ??= {
|
|
260
|
+
write: void 0,
|
|
261
|
+
read: void 0,
|
|
262
|
+
disabled: false
|
|
263
|
+
};
|
|
264
|
+
function getCache() {
|
|
265
|
+
return state.write;
|
|
266
|
+
}
|
|
267
|
+
function isCacheDisabled() {
|
|
268
|
+
return state.disabled;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/errors/error-registry.ts
|
|
272
|
+
var ErrorRegistry = class _ErrorRegistry {
|
|
273
|
+
errors = /* @__PURE__ */ new Map();
|
|
274
|
+
constructor(initialErrors) {
|
|
275
|
+
if (initialErrors) {
|
|
276
|
+
for (const input of initialErrors) {
|
|
277
|
+
if (input instanceof _ErrorRegistry) {
|
|
278
|
+
this.concat(input);
|
|
279
|
+
} else if (Array.isArray(input)) {
|
|
280
|
+
this.append(input);
|
|
281
|
+
} else {
|
|
282
|
+
this.append(input);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Append error class(es) to the registry
|
|
289
|
+
*
|
|
290
|
+
* @param input - Error constructor or array of constructors
|
|
291
|
+
* @returns This registry for chaining
|
|
292
|
+
*/
|
|
293
|
+
append(input) {
|
|
294
|
+
if (Array.isArray(input)) {
|
|
295
|
+
for (const ErrorClass of input) {
|
|
296
|
+
this.errors.set(ErrorClass.name, ErrorClass);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
this.errors.set(input.name, input);
|
|
300
|
+
}
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Concatenate another ErrorRegistry into this one
|
|
305
|
+
*
|
|
306
|
+
* @param registry - Another ErrorRegistry to merge
|
|
307
|
+
* @returns This registry for chaining
|
|
308
|
+
*/
|
|
309
|
+
concat(registry) {
|
|
310
|
+
for (const [name, ErrorClass] of registry.errors) {
|
|
311
|
+
this.errors.set(name, ErrorClass);
|
|
312
|
+
}
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Check if error type is registered
|
|
317
|
+
*
|
|
318
|
+
* @param name - Error class name
|
|
319
|
+
*/
|
|
320
|
+
has(name) {
|
|
321
|
+
return this.errors.has(name);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Deserialize error from JSON data
|
|
325
|
+
*
|
|
326
|
+
* @param data - Serialized error data with __type field
|
|
327
|
+
* @returns Deserialized error instance
|
|
328
|
+
* @throws Error if error type is not registered
|
|
329
|
+
*/
|
|
330
|
+
deserialize(data) {
|
|
331
|
+
const ErrorClass = this.errors.get(data.__type);
|
|
332
|
+
if (!ErrorClass) {
|
|
333
|
+
throw new Error(`Unknown error type: ${data.__type}`);
|
|
334
|
+
}
|
|
335
|
+
return new ErrorClass(data);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Try to deserialize error, return null if type unknown
|
|
339
|
+
*
|
|
340
|
+
* @param data - Serialized error data
|
|
341
|
+
* @returns Deserialized error or null
|
|
342
|
+
*/
|
|
343
|
+
tryDeserialize(data) {
|
|
344
|
+
if (!data.__type || !this.has(data.__type)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
return this.deserialize(data);
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get all registered error types
|
|
355
|
+
*/
|
|
356
|
+
getRegisteredTypes() {
|
|
357
|
+
return Array.from(this.errors.keys());
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// src/errors/serializable-error.ts
|
|
362
|
+
var SerializableError2 = class extends Error {
|
|
363
|
+
/**
|
|
364
|
+
* Serialize error to JSON-compatible object
|
|
365
|
+
*
|
|
366
|
+
* Automatically includes:
|
|
367
|
+
* - __type: Constructor name for deserialization
|
|
368
|
+
* - message: Error message
|
|
369
|
+
* - All public instance properties (except name, stack)
|
|
370
|
+
*/
|
|
371
|
+
toJSON() {
|
|
372
|
+
const json = {
|
|
373
|
+
__type: this.constructor.name,
|
|
374
|
+
message: this.message
|
|
375
|
+
};
|
|
376
|
+
for (const key of Object.keys(this)) {
|
|
377
|
+
if (key !== "name" && key !== "message" && key !== "stack" && key !== "statusCode") {
|
|
378
|
+
json[key] = this[key];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return json;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/errors/http-errors.ts
|
|
386
|
+
var HttpError = class extends SerializableError2 {
|
|
387
|
+
statusCode;
|
|
388
|
+
details;
|
|
389
|
+
constructor(data) {
|
|
390
|
+
super(data.message);
|
|
391
|
+
this.name = "HttpError";
|
|
392
|
+
this.statusCode = data.statusCode;
|
|
393
|
+
if (data.details) {
|
|
394
|
+
this.details = data.details;
|
|
395
|
+
}
|
|
396
|
+
Error.captureStackTrace(this, this.constructor);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
var BadRequestError = class extends HttpError {
|
|
400
|
+
constructor(data = {}) {
|
|
401
|
+
super({
|
|
402
|
+
message: data.message || "Bad request",
|
|
403
|
+
statusCode: 400,
|
|
404
|
+
details: data.details
|
|
405
|
+
});
|
|
406
|
+
this.name = "BadRequestError";
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var ValidationError = class extends HttpError {
|
|
410
|
+
fields;
|
|
411
|
+
constructor(data) {
|
|
412
|
+
super({
|
|
413
|
+
message: data.message,
|
|
414
|
+
statusCode: 400,
|
|
415
|
+
details: data.details
|
|
416
|
+
});
|
|
417
|
+
this.name = "ValidationError";
|
|
418
|
+
if (data.fields) {
|
|
419
|
+
this.fields = data.fields;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
var UnauthorizedError = class extends HttpError {
|
|
424
|
+
constructor(data = {}) {
|
|
425
|
+
super({
|
|
426
|
+
message: data.message || "Authentication required",
|
|
427
|
+
statusCode: 401,
|
|
428
|
+
details: data.details
|
|
429
|
+
});
|
|
430
|
+
this.name = "UnauthorizedError";
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
var ForbiddenError = class extends HttpError {
|
|
434
|
+
constructor(data = {}) {
|
|
435
|
+
super({
|
|
436
|
+
message: data.message || "Access forbidden",
|
|
437
|
+
statusCode: 403,
|
|
438
|
+
details: data.details
|
|
439
|
+
});
|
|
440
|
+
this.name = "ForbiddenError";
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
var NotFoundError = class extends HttpError {
|
|
444
|
+
resource;
|
|
445
|
+
constructor(data = {}) {
|
|
446
|
+
super({
|
|
447
|
+
message: data.message || "Resource not found",
|
|
448
|
+
statusCode: 404,
|
|
449
|
+
details: data.details
|
|
450
|
+
});
|
|
451
|
+
this.name = "NotFoundError";
|
|
452
|
+
if (data.resource) {
|
|
453
|
+
this.resource = data.resource;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
var ConflictError = class extends HttpError {
|
|
458
|
+
constructor(data = {}) {
|
|
459
|
+
super({
|
|
460
|
+
message: data.message || "Resource conflict",
|
|
461
|
+
statusCode: 409,
|
|
462
|
+
details: data.details
|
|
463
|
+
});
|
|
464
|
+
this.name = "ConflictError";
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
var GoneError = class extends HttpError {
|
|
468
|
+
resource;
|
|
469
|
+
constructor(data = {}) {
|
|
470
|
+
super({
|
|
471
|
+
message: data.message || "Resource permanently deleted",
|
|
472
|
+
statusCode: 410,
|
|
473
|
+
details: data.details
|
|
474
|
+
});
|
|
475
|
+
this.name = "GoneError";
|
|
476
|
+
if (data.resource) {
|
|
477
|
+
this.resource = data.resource;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var TooManyRequestsError = class extends HttpError {
|
|
482
|
+
retryAfter;
|
|
483
|
+
constructor(data = {}) {
|
|
484
|
+
super({
|
|
485
|
+
message: data.message || "Too many requests",
|
|
486
|
+
statusCode: 429,
|
|
487
|
+
details: data.details
|
|
488
|
+
});
|
|
489
|
+
this.name = "TooManyRequestsError";
|
|
490
|
+
if (data.retryAfter) {
|
|
491
|
+
this.retryAfter = data.retryAfter;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
var InternalServerError = class extends HttpError {
|
|
496
|
+
constructor(data = {}) {
|
|
497
|
+
super({
|
|
498
|
+
message: data.message || "Internal server error",
|
|
499
|
+
statusCode: 500,
|
|
500
|
+
details: data.details
|
|
501
|
+
});
|
|
502
|
+
this.name = "InternalServerError";
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
var UnsupportedMediaTypeError = class extends HttpError {
|
|
506
|
+
mediaType;
|
|
507
|
+
supportedTypes;
|
|
508
|
+
constructor(data = {}) {
|
|
509
|
+
super({
|
|
510
|
+
message: data.message || "Unsupported media type",
|
|
511
|
+
statusCode: 415,
|
|
512
|
+
details: data.details
|
|
513
|
+
});
|
|
514
|
+
this.name = "UnsupportedMediaTypeError";
|
|
515
|
+
if (data.mediaType) {
|
|
516
|
+
this.mediaType = data.mediaType;
|
|
517
|
+
}
|
|
518
|
+
if (data.supportedTypes) {
|
|
519
|
+
this.supportedTypes = data.supportedTypes;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
var UnprocessableEntityError = class extends HttpError {
|
|
524
|
+
constructor(data = {}) {
|
|
525
|
+
super({
|
|
526
|
+
message: data.message || "Unprocessable entity",
|
|
527
|
+
statusCode: 422,
|
|
528
|
+
details: data.details
|
|
529
|
+
});
|
|
530
|
+
this.name = "UnprocessableEntityError";
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
var ServiceUnavailableError = class extends HttpError {
|
|
534
|
+
retryAfter;
|
|
535
|
+
constructor(data = {}) {
|
|
536
|
+
super({
|
|
537
|
+
message: data.message || "Service unavailable",
|
|
538
|
+
statusCode: 503,
|
|
539
|
+
details: data.details
|
|
540
|
+
});
|
|
541
|
+
this.name = "ServiceUnavailableError";
|
|
542
|
+
if (data.retryAfter) {
|
|
543
|
+
this.retryAfter = data.retryAfter;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// src/errors/database-errors.ts
|
|
549
|
+
var DatabaseError = class extends SerializableError2 {
|
|
550
|
+
statusCode;
|
|
551
|
+
details;
|
|
552
|
+
constructor(data) {
|
|
553
|
+
super(data.message);
|
|
554
|
+
this.name = "DatabaseError";
|
|
555
|
+
this.statusCode = data.statusCode ?? 500;
|
|
556
|
+
this.details = data.details;
|
|
557
|
+
Error.captureStackTrace(this, this.constructor);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Whether this error's message/details are derived from the raw database
|
|
561
|
+
* driver (SQL text, table/column/constraint names, parameter values) and
|
|
562
|
+
* therefore must NOT be exposed to clients in production. Defined as a
|
|
563
|
+
* prototype getter so it is never serialized into the response by toJSON().
|
|
564
|
+
* Subclasses with a safe, constructed message override this to `false`.
|
|
565
|
+
*/
|
|
566
|
+
get internal() {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
var ConnectionError = class extends DatabaseError {
|
|
571
|
+
constructor(data) {
|
|
572
|
+
super({ message: data.message, statusCode: 503, details: data.details });
|
|
573
|
+
this.name = "ConnectionError";
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
var QueryError = class extends DatabaseError {
|
|
577
|
+
constructor(data) {
|
|
578
|
+
super({
|
|
579
|
+
message: data.message,
|
|
580
|
+
statusCode: data.statusCode ?? 500,
|
|
581
|
+
details: data.details
|
|
582
|
+
});
|
|
583
|
+
this.name = "QueryError";
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
var EntityNotFoundError = class extends QueryError {
|
|
587
|
+
resource;
|
|
588
|
+
id;
|
|
589
|
+
constructor(data) {
|
|
590
|
+
super({
|
|
591
|
+
message: `${data.resource} with id ${data.id} not found`,
|
|
592
|
+
statusCode: 404,
|
|
593
|
+
details: { resource: data.resource, id: data.id }
|
|
594
|
+
});
|
|
595
|
+
this.name = "EntityNotFoundError";
|
|
596
|
+
this.resource = data.resource;
|
|
597
|
+
this.id = data.id;
|
|
598
|
+
}
|
|
599
|
+
// Message/details are constructed from the resource + id, not driver text
|
|
600
|
+
get internal() {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
var ConstraintViolationError = class extends QueryError {
|
|
605
|
+
constructor(data) {
|
|
606
|
+
super({ message: data.message, statusCode: 400, details: data.details });
|
|
607
|
+
this.name = "ConstraintViolationError";
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
var TransactionError = class extends DatabaseError {
|
|
611
|
+
constructor(data) {
|
|
612
|
+
super({
|
|
613
|
+
message: data.message,
|
|
614
|
+
statusCode: data.statusCode ?? 500,
|
|
615
|
+
details: data.details
|
|
616
|
+
});
|
|
617
|
+
this.name = "TransactionError";
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
var DeadlockError = class extends TransactionError {
|
|
621
|
+
constructor(data) {
|
|
622
|
+
super({ message: data.message, statusCode: 409, details: data.details });
|
|
623
|
+
this.name = "DeadlockError";
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
var DuplicateEntryError = class extends QueryError {
|
|
627
|
+
field;
|
|
628
|
+
value;
|
|
629
|
+
constructor(data) {
|
|
630
|
+
super({
|
|
631
|
+
message: `${data.field} already exists`,
|
|
632
|
+
statusCode: 409,
|
|
633
|
+
details: { field: data.field }
|
|
634
|
+
});
|
|
635
|
+
this.name = "DuplicateEntryError";
|
|
636
|
+
this.field = data.field;
|
|
637
|
+
this.value = data.value;
|
|
638
|
+
}
|
|
639
|
+
// Field-only message/details — safe to surface to the client (not internal)
|
|
640
|
+
get internal() {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// src/errors/index.ts
|
|
646
|
+
var errorRegistry = new ErrorRegistry();
|
|
647
|
+
errorRegistry.append([
|
|
648
|
+
HttpError,
|
|
649
|
+
BadRequestError,
|
|
650
|
+
ValidationError,
|
|
651
|
+
UnauthorizedError,
|
|
652
|
+
ForbiddenError,
|
|
653
|
+
NotFoundError,
|
|
654
|
+
ConflictError,
|
|
655
|
+
GoneError,
|
|
656
|
+
TooManyRequestsError,
|
|
657
|
+
UnsupportedMediaTypeError,
|
|
658
|
+
UnprocessableEntityError,
|
|
659
|
+
InternalServerError,
|
|
660
|
+
ServiceUnavailableError
|
|
661
|
+
]);
|
|
662
|
+
errorRegistry.append([
|
|
663
|
+
DatabaseError,
|
|
664
|
+
ConnectionError,
|
|
665
|
+
QueryError,
|
|
666
|
+
EntityNotFoundError,
|
|
667
|
+
ConstraintViolationError,
|
|
668
|
+
TransactionError,
|
|
669
|
+
DeadlockError,
|
|
670
|
+
DuplicateEntryError
|
|
671
|
+
]);
|
|
672
|
+
|
|
673
|
+
// src/logger/types.ts
|
|
674
|
+
var LOG_LEVEL_PRIORITY = {
|
|
675
|
+
debug: 0,
|
|
676
|
+
info: 1,
|
|
677
|
+
warn: 2,
|
|
678
|
+
error: 3,
|
|
679
|
+
fatal: 4
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/logger/formatters.ts
|
|
683
|
+
var SENSITIVE_KEYS = [
|
|
684
|
+
"password",
|
|
685
|
+
"passwd",
|
|
686
|
+
"pwd",
|
|
687
|
+
"secret",
|
|
688
|
+
"token",
|
|
689
|
+
"apikey",
|
|
690
|
+
"api_key",
|
|
691
|
+
"accesstoken",
|
|
692
|
+
"access_token",
|
|
693
|
+
"refreshtoken",
|
|
694
|
+
"refresh_token",
|
|
695
|
+
"authorization",
|
|
696
|
+
"auth",
|
|
697
|
+
"cookie",
|
|
698
|
+
"session",
|
|
699
|
+
"sessionid",
|
|
700
|
+
"session_id",
|
|
701
|
+
"privatekey",
|
|
702
|
+
"private_key",
|
|
703
|
+
"creditcard",
|
|
704
|
+
"credit_card",
|
|
705
|
+
"cardnumber",
|
|
706
|
+
"card_number",
|
|
707
|
+
"cvv",
|
|
708
|
+
"ssn",
|
|
709
|
+
"pin"
|
|
710
|
+
];
|
|
711
|
+
var MASKED_VALUE = "***MASKED***";
|
|
712
|
+
function isSensitiveKey(key) {
|
|
713
|
+
const lowerKey = key.toLowerCase();
|
|
714
|
+
return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
|
|
715
|
+
}
|
|
716
|
+
function maskSensitiveData(data, seen = /* @__PURE__ */ new WeakSet()) {
|
|
717
|
+
if (data === null || data === void 0) {
|
|
718
|
+
return data;
|
|
719
|
+
}
|
|
720
|
+
if (typeof data !== "object") {
|
|
721
|
+
return data;
|
|
722
|
+
}
|
|
723
|
+
if (seen.has(data)) {
|
|
724
|
+
return "[Circular]";
|
|
725
|
+
}
|
|
726
|
+
seen.add(data);
|
|
727
|
+
if (Array.isArray(data)) {
|
|
728
|
+
return data.map((item) => maskSensitiveData(item, seen));
|
|
729
|
+
}
|
|
730
|
+
const masked = {};
|
|
731
|
+
for (const [key, value] of Object.entries(data)) {
|
|
732
|
+
if (isSensitiveKey(key)) {
|
|
733
|
+
masked[key] = MASKED_VALUE;
|
|
734
|
+
} else if (typeof value === "object" && value !== null) {
|
|
735
|
+
masked[key] = maskSensitiveData(value, seen);
|
|
736
|
+
} else {
|
|
737
|
+
masked[key] = value;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return masked;
|
|
741
|
+
}
|
|
742
|
+
var COLORS = {
|
|
743
|
+
reset: "\x1B[0m",
|
|
744
|
+
bright: "\x1B[1m",
|
|
745
|
+
dim: "\x1B[2m",
|
|
746
|
+
// 로그 레벨 컬러
|
|
747
|
+
debug: "\x1B[36m",
|
|
748
|
+
// cyan
|
|
749
|
+
info: "\x1B[32m",
|
|
750
|
+
// green
|
|
751
|
+
warn: "\x1B[33m",
|
|
752
|
+
// yellow
|
|
753
|
+
error: "\x1B[31m",
|
|
754
|
+
// red
|
|
755
|
+
fatal: "\x1B[35m",
|
|
756
|
+
// magenta
|
|
757
|
+
// 추가 컬러
|
|
758
|
+
gray: "\x1B[90m"
|
|
759
|
+
};
|
|
760
|
+
function formatTimestampHuman(date) {
|
|
761
|
+
const year = date.getFullYear();
|
|
762
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
763
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
764
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
765
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
766
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
767
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
768
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
769
|
+
}
|
|
770
|
+
function formatError(error) {
|
|
771
|
+
const lines = [];
|
|
772
|
+
lines.push(`${error.name}: ${error.message}`);
|
|
773
|
+
if (error.stack) {
|
|
774
|
+
const stackLines = error.stack.split("\n").slice(1);
|
|
775
|
+
lines.push(...stackLines);
|
|
776
|
+
}
|
|
777
|
+
if (error.cause instanceof Error) {
|
|
778
|
+
lines.push(`Caused by: ${formatError(error.cause)}`);
|
|
779
|
+
} else if (error.cause !== void 0) {
|
|
780
|
+
lines.push(`Caused by: ${String(error.cause)}`);
|
|
781
|
+
}
|
|
782
|
+
return lines.join("\n");
|
|
783
|
+
}
|
|
784
|
+
function formatConsole(metadata, colorize = true) {
|
|
785
|
+
const parts = [];
|
|
786
|
+
const timestamp = formatTimestampHuman(metadata.timestamp);
|
|
787
|
+
if (colorize) {
|
|
788
|
+
parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
|
|
789
|
+
} else {
|
|
790
|
+
parts.push(`[${timestamp}]`);
|
|
791
|
+
}
|
|
792
|
+
const pid = process.pid;
|
|
793
|
+
if (colorize) {
|
|
794
|
+
parts.push(`${COLORS.dim}[pid=${pid}]${COLORS.reset}`);
|
|
795
|
+
} else {
|
|
796
|
+
parts.push(`[pid=${pid}]`);
|
|
797
|
+
}
|
|
798
|
+
if (metadata.module) {
|
|
799
|
+
if (colorize) {
|
|
800
|
+
parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
|
|
801
|
+
} else {
|
|
802
|
+
parts.push(`[module=${metadata.module}]`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
806
|
+
Object.entries(metadata.context).forEach(([key, value]) => {
|
|
807
|
+
let valueStr;
|
|
808
|
+
if (typeof value === "string") {
|
|
809
|
+
valueStr = value;
|
|
810
|
+
} else if (typeof value === "object" && value !== null) {
|
|
811
|
+
try {
|
|
812
|
+
valueStr = JSON.stringify(value);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
valueStr = "[circular]";
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
valueStr = String(value);
|
|
818
|
+
}
|
|
819
|
+
if (colorize) {
|
|
820
|
+
parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
|
|
821
|
+
} else {
|
|
822
|
+
parts.push(`[${key}=${valueStr}]`);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
const levelStr = metadata.level.toUpperCase();
|
|
827
|
+
if (colorize) {
|
|
828
|
+
const color = COLORS[metadata.level];
|
|
829
|
+
parts.push(`${color}(${levelStr})${COLORS.reset}:`);
|
|
830
|
+
} else {
|
|
831
|
+
parts.push(`(${levelStr}):`);
|
|
832
|
+
}
|
|
833
|
+
if (colorize) {
|
|
834
|
+
parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
|
|
835
|
+
} else {
|
|
836
|
+
parts.push(metadata.message);
|
|
837
|
+
}
|
|
838
|
+
let output = parts.join(" ");
|
|
839
|
+
if (metadata.error) {
|
|
840
|
+
output += "\n" + formatError(metadata.error);
|
|
841
|
+
}
|
|
842
|
+
return output;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/logger/logger.ts
|
|
846
|
+
var FORMAT_PATTERN = /%[sdifjoOc%]/;
|
|
847
|
+
var Logger = class _Logger {
|
|
848
|
+
config;
|
|
849
|
+
module;
|
|
850
|
+
constructor(config) {
|
|
851
|
+
this.config = config;
|
|
852
|
+
this.module = config.module;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Convert unknown error to Error object
|
|
856
|
+
*/
|
|
857
|
+
toError(error) {
|
|
858
|
+
if (error instanceof Error) return error;
|
|
859
|
+
if (typeof error === "string") return new Error(error);
|
|
860
|
+
if (typeof error === "object" && error !== null) {
|
|
861
|
+
return new Error(JSON.stringify(error));
|
|
862
|
+
}
|
|
863
|
+
return new Error(String(error));
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Check if value is a context object (not an error)
|
|
867
|
+
*/
|
|
868
|
+
isContext(value) {
|
|
869
|
+
if (typeof value !== "object" || value === null) return false;
|
|
870
|
+
if (value instanceof Error) return false;
|
|
871
|
+
const hasStack = "stack" in value && typeof value.stack === "string";
|
|
872
|
+
if (hasStack) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Get current log level
|
|
879
|
+
*/
|
|
880
|
+
get level() {
|
|
881
|
+
return this.config.level;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Create child logger (per module)
|
|
885
|
+
*/
|
|
886
|
+
child(module) {
|
|
887
|
+
return new _Logger({
|
|
888
|
+
...this.config,
|
|
889
|
+
module
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Common log method with error/context detection
|
|
894
|
+
*/
|
|
895
|
+
logWithLevel(level, message, errorOrContext, context) {
|
|
896
|
+
if (errorOrContext !== void 0 && FORMAT_PATTERN.test(message)) {
|
|
897
|
+
this.log(level, format(message, errorOrContext), void 0, context);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (errorOrContext instanceof Error) {
|
|
901
|
+
this.log(level, message, errorOrContext, context);
|
|
902
|
+
} else if (errorOrContext !== void 0 && typeof errorOrContext === "object" && !this.isContext(errorOrContext)) {
|
|
903
|
+
this.log(level, message, this.toError(errorOrContext), context);
|
|
904
|
+
} else if (typeof errorOrContext === "string" || typeof errorOrContext === "number" || typeof errorOrContext === "boolean") {
|
|
905
|
+
this.log(level, message, this.toError(errorOrContext), context);
|
|
906
|
+
} else {
|
|
907
|
+
this.log(level, message, void 0, errorOrContext);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
debug(message, errorOrContext, context) {
|
|
911
|
+
this.logWithLevel("debug", message, errorOrContext, context);
|
|
912
|
+
}
|
|
913
|
+
info(message, errorOrContext, context) {
|
|
914
|
+
this.logWithLevel("info", message, errorOrContext, context);
|
|
915
|
+
}
|
|
916
|
+
warn(message, errorOrContext, context) {
|
|
917
|
+
this.logWithLevel("warn", message, errorOrContext, context);
|
|
918
|
+
}
|
|
919
|
+
error(message, errorOrContext, context) {
|
|
920
|
+
this.logWithLevel("error", message, errorOrContext, context);
|
|
921
|
+
}
|
|
922
|
+
fatal(message, errorOrContext, context) {
|
|
923
|
+
this.logWithLevel("fatal", message, errorOrContext, context);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Log processing (internal)
|
|
927
|
+
*/
|
|
928
|
+
log(level, message, error, context) {
|
|
929
|
+
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const metadata = {
|
|
933
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
934
|
+
level,
|
|
935
|
+
message,
|
|
936
|
+
module: this.module,
|
|
937
|
+
error,
|
|
938
|
+
// Mask sensitive information in context to prevent credential leaks
|
|
939
|
+
context: context ? maskSensitiveData(context) : void 0
|
|
940
|
+
};
|
|
941
|
+
this.processTransports(metadata);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Process Transports
|
|
945
|
+
*/
|
|
946
|
+
processTransports(metadata) {
|
|
947
|
+
const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
|
|
948
|
+
Promise.all(promises).catch((error) => {
|
|
949
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
950
|
+
process.stderr.write(`[Logger] Transport error: ${errorMessage}
|
|
951
|
+
`);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Transport log (error-safe)
|
|
956
|
+
*/
|
|
957
|
+
async safeTransportLog(transport, metadata) {
|
|
958
|
+
try {
|
|
959
|
+
await transport.log(metadata);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
962
|
+
process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
|
|
963
|
+
`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Close all Transports
|
|
968
|
+
*/
|
|
969
|
+
async close() {
|
|
970
|
+
const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
|
|
971
|
+
await Promise.all(closePromises);
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// src/logger/transports/console.ts
|
|
976
|
+
var ConsoleTransport = class {
|
|
977
|
+
name = "console";
|
|
978
|
+
level;
|
|
979
|
+
enabled;
|
|
980
|
+
colorize;
|
|
981
|
+
constructor(config) {
|
|
982
|
+
this.level = config.level;
|
|
983
|
+
this.enabled = config.enabled;
|
|
984
|
+
this.colorize = config.colorize ?? true;
|
|
985
|
+
}
|
|
986
|
+
async log(metadata) {
|
|
987
|
+
if (!this.enabled) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const message = formatConsole(metadata, this.colorize);
|
|
994
|
+
if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
|
|
995
|
+
console.error(message);
|
|
996
|
+
} else {
|
|
997
|
+
console.log(message);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// src/logger/config.ts
|
|
1003
|
+
function getConsoleConfig() {
|
|
1004
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
1005
|
+
return {
|
|
1006
|
+
level: "debug",
|
|
1007
|
+
enabled: true,
|
|
1008
|
+
colorize: !isProduction
|
|
1009
|
+
// Dev: colored output, Production: plain text
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
function validateEnvironment() {
|
|
1013
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
1014
|
+
if (!nodeEnv) {
|
|
1015
|
+
process.stderr.write(
|
|
1016
|
+
"[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function validateConfig() {
|
|
1021
|
+
validateEnvironment();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/logger/factory.ts
|
|
1025
|
+
function initializeTransports() {
|
|
1026
|
+
const transports = [];
|
|
1027
|
+
const consoleConfig = getConsoleConfig();
|
|
1028
|
+
transports.push(new ConsoleTransport(consoleConfig));
|
|
1029
|
+
return transports;
|
|
1030
|
+
}
|
|
1031
|
+
function getLogLevel() {
|
|
1032
|
+
const envLevel = process.env.SPFN_LOG_LEVEL || process.env.NEXT_PUBLIC_SPFN_LOG_LEVEL || "info";
|
|
1033
|
+
if (envLevel in LOG_LEVEL_PRIORITY) {
|
|
1034
|
+
return envLevel;
|
|
1035
|
+
}
|
|
1036
|
+
process.stderr.write(
|
|
1037
|
+
`[Logger] Invalid log level "${envLevel}", defaulting to "info"
|
|
1038
|
+
`
|
|
1039
|
+
);
|
|
1040
|
+
return "info";
|
|
1041
|
+
}
|
|
1042
|
+
function initializeLogger() {
|
|
1043
|
+
validateConfig();
|
|
1044
|
+
return new Logger({
|
|
1045
|
+
level: getLogLevel(),
|
|
1046
|
+
transports: initializeTransports()
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
var logger4 = initializeLogger();
|
|
1050
|
+
|
|
1051
|
+
// src/middleware/rate-limit.ts
|
|
1052
|
+
var rateLimitLogger = logger4.child("@spfn/core:rate-limit");
|
|
1053
|
+
var FIXED_WINDOW_LUA = `
|
|
1054
|
+
local count = redis.call('INCR', KEYS[1])
|
|
1055
|
+
if count == 1 then
|
|
1056
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
1057
|
+
end
|
|
1058
|
+
return { count, redis.call('PTTL', KEYS[1]) }
|
|
1059
|
+
`;
|
|
1060
|
+
function getClientIp(c) {
|
|
1061
|
+
const clientType = c.get("clientType");
|
|
1062
|
+
if (clientType && clientType !== "untrusted") {
|
|
1063
|
+
const forwarded = c.req.header(PROXY_CLIENT_IP_HEADER);
|
|
1064
|
+
if (forwarded) {
|
|
1065
|
+
return forwarded;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
const forwardedFor = c.req.header("x-forwarded-for");
|
|
1069
|
+
return forwardedFor?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
|
|
1070
|
+
}
|
|
1071
|
+
var rateLimit = defineMiddlewareFactory(
|
|
1072
|
+
"rateLimit",
|
|
1073
|
+
(options) => {
|
|
1074
|
+
const { limit, windowMs, scope, by, failClosed = false, message } = options;
|
|
1075
|
+
return async (c, next) => {
|
|
1076
|
+
const cache = getCache();
|
|
1077
|
+
if (!cache || isCacheDisabled()) {
|
|
1078
|
+
if (failClosed) {
|
|
1079
|
+
throw new TooManyRequestsError({ message: message || "Rate limiter unavailable" });
|
|
1080
|
+
}
|
|
1081
|
+
rateLimitLogger.warn("Cache unavailable \u2014 rate limit not enforced (fail-open)", {
|
|
1082
|
+
path: c.req.path
|
|
1083
|
+
});
|
|
1084
|
+
return next();
|
|
1085
|
+
}
|
|
1086
|
+
const dimensions = (by ? await by(c) : [getClientIp(c)]).filter((d) => Boolean(d));
|
|
1087
|
+
const ns = scope || `${c.req.method} ${c.req.routePath || c.req.path}`;
|
|
1088
|
+
for (const dimension of dimensions) {
|
|
1089
|
+
const [count, pttl] = await cache.eval(
|
|
1090
|
+
FIXED_WINDOW_LUA,
|
|
1091
|
+
1,
|
|
1092
|
+
`ratelimit:${ns}:${dimension}`,
|
|
1093
|
+
String(windowMs)
|
|
1094
|
+
);
|
|
1095
|
+
if (count > limit) {
|
|
1096
|
+
const retryAfter = Math.max(1, Math.ceil((pttl > 0 ? pttl : windowMs) / 1e3));
|
|
1097
|
+
c.header("Retry-After", String(retryAfter));
|
|
1098
|
+
throw new TooManyRequestsError({
|
|
1099
|
+
message: message || "Too many requests, please try again later",
|
|
1100
|
+
retryAfter
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return next();
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
// src/middleware/request-logger.ts
|
|
116
1110
|
var DEFAULT_CONFIG = {
|
|
117
1111
|
excludePaths: ["/health", "/ping", "/favicon.ico"],
|
|
118
1112
|
sensitiveFields: ["password", "token", "apiKey", "secret", "authorization"],
|
|
@@ -123,7 +1117,7 @@ function generateRequestId() {
|
|
|
123
1117
|
const randomPart = randomBytes(6).toString("hex");
|
|
124
1118
|
return `req_${timestamp}_${randomPart}`;
|
|
125
1119
|
}
|
|
126
|
-
function
|
|
1120
|
+
function maskSensitiveData2(obj, sensitiveFields, seen = /* @__PURE__ */ new WeakSet()) {
|
|
127
1121
|
if (!obj || typeof obj !== "object") return obj;
|
|
128
1122
|
if (seen.has(obj)) return "[Circular]";
|
|
129
1123
|
seen.add(obj);
|
|
@@ -134,7 +1128,7 @@ function maskSensitiveData(obj, sensitiveFields, seen = /* @__PURE__ */ new Weak
|
|
|
134
1128
|
if (lowerFields.some((field) => lowerKey.includes(field))) {
|
|
135
1129
|
masked[key] = "***MASKED***";
|
|
136
1130
|
} else if (typeof masked[key] === "object" && masked[key] !== null) {
|
|
137
|
-
masked[key] =
|
|
1131
|
+
masked[key] = maskSensitiveData2(masked[key], sensitiveFields, seen);
|
|
138
1132
|
}
|
|
139
1133
|
}
|
|
140
1134
|
return masked;
|
|
@@ -154,8 +1148,7 @@ function RequestLogger(options) {
|
|
|
154
1148
|
c.set("requestId", requestId);
|
|
155
1149
|
const method = c.req.method;
|
|
156
1150
|
const userAgent = c.req.header("user-agent");
|
|
157
|
-
const
|
|
158
|
-
const ip = forwardedFor?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
|
|
1151
|
+
const ip = getClientIp(c);
|
|
159
1152
|
const startTime = Date.now();
|
|
160
1153
|
apiLogger.info("Request received", {
|
|
161
1154
|
requestId,
|
|
@@ -187,7 +1180,7 @@ function RequestLogger(options) {
|
|
|
187
1180
|
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
188
1181
|
try {
|
|
189
1182
|
const requestBody = await c.req.json();
|
|
190
|
-
logData.request =
|
|
1183
|
+
logData.request = maskSensitiveData2(requestBody, cfg.sensitiveFields);
|
|
191
1184
|
} catch {
|
|
192
1185
|
}
|
|
193
1186
|
}
|
|
@@ -206,93 +1199,6 @@ function RequestLogger(options) {
|
|
|
206
1199
|
}
|
|
207
1200
|
};
|
|
208
1201
|
}
|
|
209
|
-
var PROXY_SIGNATURE_HEADER = "x-spfn-proxy-signature";
|
|
210
|
-
var PROXY_TIMESTAMP_HEADER = "x-spfn-proxy-timestamp";
|
|
211
|
-
var PROXY_NONCE_HEADER = "x-spfn-proxy-nonce";
|
|
212
|
-
var PROXY_KEY_ID_HEADER = "x-spfn-proxy-key-id";
|
|
213
|
-
var DEFAULT_KEY_ID = "default";
|
|
214
|
-
function parseProxyKey(raw) {
|
|
215
|
-
const idx = raw.indexOf(":");
|
|
216
|
-
if (idx === -1) {
|
|
217
|
-
return { keyId: DEFAULT_KEY_ID, secret: raw };
|
|
218
|
-
}
|
|
219
|
-
return { keyId: raw.slice(0, idx) || DEFAULT_KEY_ID, secret: raw.slice(idx + 1) };
|
|
220
|
-
}
|
|
221
|
-
function parseProxyKeySet(raws) {
|
|
222
|
-
const keys = [];
|
|
223
|
-
const seen = /* @__PURE__ */ new Set();
|
|
224
|
-
for (const raw of raws) {
|
|
225
|
-
if (!raw) {
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
for (const part of raw.split(",")) {
|
|
229
|
-
const trimmed = part.trim();
|
|
230
|
-
if (!trimmed) {
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const key = parseProxyKey(trimmed);
|
|
234
|
-
if (!key.secret || seen.has(key.keyId)) {
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
seen.add(key.keyId);
|
|
238
|
-
keys.push(key);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return keys;
|
|
242
|
-
}
|
|
243
|
-
function buildCanonicalString(parts) {
|
|
244
|
-
return [parts.method.toUpperCase(), parts.path, parts.query, parts.timestamp, parts.nonce, parts.bodyHash].join("\n");
|
|
245
|
-
}
|
|
246
|
-
function hashBody(body) {
|
|
247
|
-
if (!body || body.length === 0) {
|
|
248
|
-
return "";
|
|
249
|
-
}
|
|
250
|
-
return typeof body === "string" ? createHash("sha256").update(body, "utf8").digest("hex") : createHash("sha256").update(body).digest("hex");
|
|
251
|
-
}
|
|
252
|
-
function computeSignature(secret, parts) {
|
|
253
|
-
return createHmac("sha256", secret).update(buildCanonicalString(parts)).digest("hex");
|
|
254
|
-
}
|
|
255
|
-
function safeEqualHex(a, b) {
|
|
256
|
-
const bufA = Buffer.from(a, "hex");
|
|
257
|
-
const bufB = Buffer.from(b, "hex");
|
|
258
|
-
if (bufA.length === 0 || bufA.length !== bufB.length) {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
return timingSafeEqual(bufA, bufB);
|
|
262
|
-
}
|
|
263
|
-
function verifyProxyRequest(input) {
|
|
264
|
-
const { signature, timestamp, nonce, keyId } = input;
|
|
265
|
-
if (!signature || !timestamp || !nonce || !keyId) {
|
|
266
|
-
return { valid: false, reason: "missing-headers" };
|
|
267
|
-
}
|
|
268
|
-
const ts = Number(timestamp);
|
|
269
|
-
if (!Number.isFinite(ts)) {
|
|
270
|
-
return { valid: false, reason: "bad-timestamp" };
|
|
271
|
-
}
|
|
272
|
-
const now = input.now ?? Date.now();
|
|
273
|
-
const windowMs = input.windowMs ?? 3e4;
|
|
274
|
-
if (Math.abs(now - ts) > windowMs) {
|
|
275
|
-
return { valid: false, reason: "stale-timestamp" };
|
|
276
|
-
}
|
|
277
|
-
const key = input.keys.find((k) => k.keyId === keyId);
|
|
278
|
-
if (!key) {
|
|
279
|
-
return { valid: false, reason: "unknown-key" };
|
|
280
|
-
}
|
|
281
|
-
const expected = computeSignature(key.secret, {
|
|
282
|
-
method: input.method,
|
|
283
|
-
path: input.path,
|
|
284
|
-
query: input.query ?? "",
|
|
285
|
-
timestamp,
|
|
286
|
-
nonce,
|
|
287
|
-
bodyHash: hashBody(input.body)
|
|
288
|
-
});
|
|
289
|
-
if (!safeEqualHex(signature, expected)) {
|
|
290
|
-
return { valid: false, reason: "signature-mismatch" };
|
|
291
|
-
}
|
|
292
|
-
return { valid: true, nonce, keyId };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// src/middleware/proxy-guard.ts
|
|
296
1202
|
var guardLogger = logger.child("@spfn/core:proxy-guard");
|
|
297
1203
|
var BODY_OVERSIZE = Symbol("proxy-guard:body-oversize");
|
|
298
1204
|
var BODY_READ_ERROR = Symbol("proxy-guard:body-read-error");
|
|
@@ -346,6 +1252,7 @@ function createProxyGuard(config = {}) {
|
|
|
346
1252
|
const windowMs = config.windowMs ?? 3e4;
|
|
347
1253
|
const allowedOrigins = config.allowedOrigins;
|
|
348
1254
|
const nonceStore = config.nonceStore;
|
|
1255
|
+
const nonceFailClosed = config.nonceFailClosed ?? false;
|
|
349
1256
|
const skipPaths = new Set(config.skipPaths ?? []);
|
|
350
1257
|
const maxBodyBytes = config.maxBodyBytes;
|
|
351
1258
|
const activeRaw = config.secret ?? env.SPFN_PROXY_SECRET;
|
|
@@ -422,6 +1329,12 @@ function createProxyGuard(config = {}) {
|
|
|
422
1329
|
return reject(c, mode, next, "nonce-replay");
|
|
423
1330
|
}
|
|
424
1331
|
} catch (err) {
|
|
1332
|
+
if (nonceFailClosed) {
|
|
1333
|
+
guardLogger.warn("Nonce store unavailable \u2014 rejecting (fail-closed)", {
|
|
1334
|
+
error: err.message
|
|
1335
|
+
});
|
|
1336
|
+
return reject(c, mode, next, "nonce-store-unavailable");
|
|
1337
|
+
}
|
|
425
1338
|
guardLogger.warn("Nonce store unavailable \u2014 falling back to timestamp window", {
|
|
426
1339
|
error: err.message
|
|
427
1340
|
});
|
|
@@ -439,6 +1352,27 @@ function createCacheNonceStore(cache, prefix = "spfn:proxy-nonce:") {
|
|
|
439
1352
|
}
|
|
440
1353
|
};
|
|
441
1354
|
}
|
|
1355
|
+
function createInMemoryNonceStore() {
|
|
1356
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1357
|
+
let lastSweep = 0;
|
|
1358
|
+
return {
|
|
1359
|
+
async checkAndSet(nonce, ttlMs) {
|
|
1360
|
+
const now = Date.now();
|
|
1361
|
+
if (now - lastSweep > 6e4) {
|
|
1362
|
+
for (const [n, exp] of seen) {
|
|
1363
|
+
if (exp <= now) seen.delete(n);
|
|
1364
|
+
}
|
|
1365
|
+
lastSweep = now;
|
|
1366
|
+
}
|
|
1367
|
+
const existing = seen.get(nonce);
|
|
1368
|
+
if (existing !== void 0 && existing > now) {
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
seen.set(nonce, now + ttlMs);
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
442
1376
|
function reject(c, mode, next, reason) {
|
|
443
1377
|
if (mode === "strict") {
|
|
444
1378
|
guardLogger.warn("Rejected unverified request", { reason, path: c.req.path, method: c.req.method });
|
|
@@ -449,6 +1383,6 @@ function reject(c, mode, next, reason) {
|
|
|
449
1383
|
return next();
|
|
450
1384
|
}
|
|
451
1385
|
|
|
452
|
-
export { ErrorHandler, RequestLogger, createCacheNonceStore, createProxyGuard, maskSensitiveData };
|
|
1386
|
+
export { ErrorHandler, RequestLogger, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, maskSensitiveData2 as maskSensitiveData, rateLimit };
|
|
453
1387
|
//# sourceMappingURL=index.js.map
|
|
454
1388
|
//# sourceMappingURL=index.js.map
|