@stacksjs/bun-router 0.0.18 → 0.0.19
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/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-a8p83068.js +103 -0
- package/dist/{chunk-cgptvjdf.js → chunk-y3xq6wq6.js} +1 -1
- package/dist/cli.js +4 -3
- package/dist/container/index.js +1149 -211
- package/dist/index.js +369 -2209
- package/package.json +6 -5
- package/dist/chunk-1ahs68ys.js +0 -18
- package/dist/chunk-g3ybefhg.js +0 -1923
package/dist/index.js
CHANGED
|
@@ -64,48 +64,11 @@ import {
|
|
|
64
64
|
signRequest,
|
|
65
65
|
singleFileUpload,
|
|
66
66
|
verifySignature
|
|
67
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-y3xq6wq6.js";
|
|
68
68
|
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
BusinessLogicException,
|
|
73
|
-
CircuitBreakerOpenException,
|
|
74
|
-
CircularDependencyException,
|
|
75
|
-
ConfigurationException,
|
|
76
|
-
Container,
|
|
77
|
-
ContextualContainer,
|
|
78
|
-
DatabaseException,
|
|
79
|
-
DefaultServiceProviderManager,
|
|
80
|
-
EnvironmentManager,
|
|
81
|
-
ErrorFactory,
|
|
82
|
-
ExternalServiceException,
|
|
83
|
-
FileSystemException,
|
|
84
|
-
HandlerResolutionException,
|
|
85
|
-
InternalServerException,
|
|
86
|
-
MethodNotAllowedException,
|
|
87
|
-
MiddlewareConfigurationException,
|
|
88
|
-
ModelBindingException,
|
|
89
|
-
NotFoundException,
|
|
90
|
-
RateLimitException,
|
|
91
|
-
RouteConflictException,
|
|
92
|
-
RouteNotFoundException,
|
|
93
|
-
RouterErrorFactory,
|
|
94
|
-
RouterException,
|
|
95
|
-
ServiceUnavailableException,
|
|
96
|
-
TimeoutException,
|
|
97
|
-
ValidationException,
|
|
98
|
-
createContainer,
|
|
99
|
-
detectRouteConflicts,
|
|
100
|
-
findSimilarRoutes,
|
|
101
|
-
getContainer,
|
|
102
|
-
setContainer
|
|
103
|
-
} from "./chunk-g3ybefhg.js";
|
|
104
|
-
import {
|
|
105
|
-
__export,
|
|
106
|
-
__require
|
|
107
|
-
} from "./chunk-1ahs68ys.js";
|
|
108
|
-
|
|
69
|
+
ValidationException
|
|
70
|
+
} from "./chunk-a8p83068.js";
|
|
71
|
+
import"./chunk-2mx7fq49.js";
|
|
109
72
|
// src/auth.ts
|
|
110
73
|
import { timingSafeEqual } from "crypto";
|
|
111
74
|
function base64UrlEncode(str) {
|
|
@@ -247,1805 +210,128 @@ class ApiKeyManager {
|
|
|
247
210
|
this.keys = new Map;
|
|
248
211
|
this.options = {
|
|
249
212
|
source: "header",
|
|
250
|
-
keyName: "X-API-Key",
|
|
251
|
-
keyPrefix: "bun_",
|
|
252
|
-
...options
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
generateKey(owner, scopes = [], expiresIn) {
|
|
256
|
-
const key = `${this.options.keyPrefix}${this.randomString(32)}`;
|
|
257
|
-
this.keys.set(key, {
|
|
258
|
-
owner,
|
|
259
|
-
scopes,
|
|
260
|
-
expiresAt: expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined
|
|
261
|
-
});
|
|
262
|
-
return key;
|
|
263
|
-
}
|
|
264
|
-
validateKey(key, requiredScopes = []) {
|
|
265
|
-
const keyInfo = this.keys.get(key);
|
|
266
|
-
if (!keyInfo) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
if (keyInfo.expiresAt && keyInfo.expiresAt < new Date) {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
if (requiredScopes.length > 0) {
|
|
273
|
-
return requiredScopes.every((scope) => keyInfo.scopes.includes(scope));
|
|
274
|
-
}
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
getKeyInfo(key) {
|
|
278
|
-
return this.keys.get(key) || null;
|
|
279
|
-
}
|
|
280
|
-
revokeKey(key) {
|
|
281
|
-
return this.keys.delete(key);
|
|
282
|
-
}
|
|
283
|
-
extractFromRequest(req) {
|
|
284
|
-
const { source, keyName } = this.options;
|
|
285
|
-
switch (source) {
|
|
286
|
-
case "header":
|
|
287
|
-
return req.headers.get(keyName || "X-API-Key") || null;
|
|
288
|
-
case "query":
|
|
289
|
-
return new URL(req.url).searchParams.get(keyName || "api_key") || null;
|
|
290
|
-
case "cookie":
|
|
291
|
-
if (req.cookies) {
|
|
292
|
-
const key = keyName || "api_key";
|
|
293
|
-
if (typeof req.cookies.get === "function") {
|
|
294
|
-
return req.cookies.get(key) || null;
|
|
295
|
-
}
|
|
296
|
-
return req.cookies[key] || null;
|
|
297
|
-
}
|
|
298
|
-
return null;
|
|
299
|
-
default:
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
randomString(length) {
|
|
304
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
305
|
-
let result = "";
|
|
306
|
-
for (let i = 0;i < length; i++) {
|
|
307
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
308
|
-
}
|
|
309
|
-
return result;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
class OAuth2Helper {
|
|
314
|
-
config;
|
|
315
|
-
constructor(config2) {
|
|
316
|
-
this.config = config2;
|
|
317
|
-
}
|
|
318
|
-
getAuthorizationUrl(additionalParams = {}) {
|
|
319
|
-
const params = new URLSearchParams({
|
|
320
|
-
client_id: this.config.clientId,
|
|
321
|
-
redirect_uri: this.config.redirectUri,
|
|
322
|
-
response_type: "code",
|
|
323
|
-
...additionalParams
|
|
324
|
-
});
|
|
325
|
-
if (this.config.scope) {
|
|
326
|
-
params.set("scope", this.config.scope);
|
|
327
|
-
}
|
|
328
|
-
if (this.config.state) {
|
|
329
|
-
params.set("state", this.config.state);
|
|
330
|
-
}
|
|
331
|
-
return `${this.config.authorizeUrl}?${params.toString()}`;
|
|
332
|
-
}
|
|
333
|
-
async exchangeCodeForToken(code) {
|
|
334
|
-
const params = new URLSearchParams({
|
|
335
|
-
grant_type: "authorization_code",
|
|
336
|
-
code,
|
|
337
|
-
redirect_uri: this.config.redirectUri,
|
|
338
|
-
client_id: this.config.clientId,
|
|
339
|
-
client_secret: this.config.clientSecret
|
|
340
|
-
});
|
|
341
|
-
const response = await fetch(this.config.tokenUrl, {
|
|
342
|
-
method: "POST",
|
|
343
|
-
headers: {
|
|
344
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
345
|
-
},
|
|
346
|
-
body: params.toString()
|
|
347
|
-
});
|
|
348
|
-
if (!response.ok) {
|
|
349
|
-
throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`);
|
|
350
|
-
}
|
|
351
|
-
return await response.json();
|
|
352
|
-
}
|
|
353
|
-
async refreshToken(refreshToken) {
|
|
354
|
-
const params = new URLSearchParams({
|
|
355
|
-
grant_type: "refresh_token",
|
|
356
|
-
refresh_token: refreshToken,
|
|
357
|
-
client_id: this.config.clientId,
|
|
358
|
-
client_secret: this.config.clientSecret
|
|
359
|
-
});
|
|
360
|
-
const response = await fetch(this.config.tokenUrl, {
|
|
361
|
-
method: "POST",
|
|
362
|
-
headers: {
|
|
363
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
364
|
-
},
|
|
365
|
-
body: params.toString()
|
|
366
|
-
});
|
|
367
|
-
if (!response.ok) {
|
|
368
|
-
throw new Error(`OAuth token refresh failed: ${response.status} ${response.statusText}`);
|
|
369
|
-
}
|
|
370
|
-
return await response.json();
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
var Auth = {
|
|
374
|
-
JWT,
|
|
375
|
-
ApiKeyManager,
|
|
376
|
-
OAuth2Helper
|
|
377
|
-
};
|
|
378
|
-
var auth_default = Auth;
|
|
379
|
-
// src/errors/index.ts
|
|
380
|
-
var exports_errors = {};
|
|
381
|
-
__export(exports_errors, {
|
|
382
|
-
withCircuitBreaker: () => withCircuitBreaker,
|
|
383
|
-
globalCircuitBreakerRegistry: () => globalCircuitBreakerRegistry,
|
|
384
|
-
findSimilarRoutes: () => findSimilarRoutes,
|
|
385
|
-
detectRouteConflicts: () => detectRouteConflicts,
|
|
386
|
-
createGracefulDegradationMiddleware: () => createGracefulDegradationMiddleware,
|
|
387
|
-
createErrorReporting: () => createErrorReporting,
|
|
388
|
-
createCircuitBreakerMiddleware: () => createCircuitBreakerMiddleware,
|
|
389
|
-
createCircuitBreaker: () => createCircuitBreaker,
|
|
390
|
-
createAdvancedErrorHandler: () => createAdvancedErrorHandler,
|
|
391
|
-
circuitBreaker: () => circuitBreaker,
|
|
392
|
-
ValidationException: () => ValidationException,
|
|
393
|
-
TimeoutException: () => TimeoutException,
|
|
394
|
-
ServiceUnavailableException: () => ServiceUnavailableException,
|
|
395
|
-
ServiceHealthMonitor: () => ServiceHealthMonitor,
|
|
396
|
-
SentryReporter: () => SentryReporter,
|
|
397
|
-
RouterException: () => RouterException,
|
|
398
|
-
RouterErrorFactory: () => RouterErrorFactory,
|
|
399
|
-
RouteNotFoundException: () => RouteNotFoundException,
|
|
400
|
-
RouteConflictException: () => RouteConflictException,
|
|
401
|
-
RateLimitException: () => RateLimitException,
|
|
402
|
-
NotFoundException: () => NotFoundException,
|
|
403
|
-
ModelBindingException: () => ModelBindingException,
|
|
404
|
-
MiddlewareConfigurationException: () => MiddlewareConfigurationException,
|
|
405
|
-
MethodNotAllowedException: () => MethodNotAllowedException,
|
|
406
|
-
InternalServerException: () => InternalServerException,
|
|
407
|
-
HandlerResolutionException: () => HandlerResolutionException,
|
|
408
|
-
GracefulDegradationManager: () => GracefulDegradationManager,
|
|
409
|
-
FileSystemException: () => FileSystemException,
|
|
410
|
-
ExternalServiceException: () => ExternalServiceException,
|
|
411
|
-
ErrorReportingPresets: () => ErrorReportingPresets,
|
|
412
|
-
ErrorReportingManager: () => ErrorReportingManager,
|
|
413
|
-
ErrorFactory: () => ErrorFactory,
|
|
414
|
-
DegradationStrategies: () => DegradationStrategies,
|
|
415
|
-
DatabaseException: () => DatabaseException,
|
|
416
|
-
CustomReporter: () => CustomReporter,
|
|
417
|
-
ConfigurationException: () => ConfigurationException,
|
|
418
|
-
CircularDependencyException: () => CircularDependencyException,
|
|
419
|
-
CircuitBreakerRegistry: () => CircuitBreakerRegistry,
|
|
420
|
-
CircuitBreakerPresets: () => CircuitBreakerPresets,
|
|
421
|
-
CircuitBreakerOpenException: () => CircuitBreakerOpenException,
|
|
422
|
-
CircuitBreakerHttpClient: () => CircuitBreakerHttpClient,
|
|
423
|
-
CircuitBreaker: () => CircuitBreaker,
|
|
424
|
-
BusinessLogicException: () => BusinessLogicException,
|
|
425
|
-
BugsnagReporter: () => BugsnagReporter,
|
|
426
|
-
AuthorizationException: () => AuthorizationException,
|
|
427
|
-
AuthenticationException: () => AuthenticationException
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// src/errors/circuit-breaker.ts
|
|
431
|
-
class CircuitBreaker {
|
|
432
|
-
config;
|
|
433
|
-
state = "CLOSED";
|
|
434
|
-
metrics;
|
|
435
|
-
nextAttemptTime;
|
|
436
|
-
halfOpenCallCount = 0;
|
|
437
|
-
recentRequests = [];
|
|
438
|
-
constructor(config2) {
|
|
439
|
-
const defaults = {
|
|
440
|
-
failureThreshold: 5,
|
|
441
|
-
recoveryTimeout: 60000,
|
|
442
|
-
timeout: 30000,
|
|
443
|
-
monitoringPeriod: 60000,
|
|
444
|
-
minimumRequests: 10,
|
|
445
|
-
errorThresholdPercentage: 50,
|
|
446
|
-
halfOpenMaxCalls: 3,
|
|
447
|
-
resetTimeout: 60000
|
|
448
|
-
};
|
|
449
|
-
this.config = {
|
|
450
|
-
...defaults,
|
|
451
|
-
...config2
|
|
452
|
-
};
|
|
453
|
-
this.metrics = {
|
|
454
|
-
totalRequests: 0,
|
|
455
|
-
successfulRequests: 0,
|
|
456
|
-
failedRequests: 0,
|
|
457
|
-
timeouts: 0,
|
|
458
|
-
averageResponseTime: 0,
|
|
459
|
-
state: this.state,
|
|
460
|
-
consecutiveFailures: 0,
|
|
461
|
-
consecutiveSuccesses: 0
|
|
462
|
-
};
|
|
463
|
-
setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4).unref?.();
|
|
464
|
-
}
|
|
465
|
-
async execute(call) {
|
|
466
|
-
if (this.state === "OPEN") {
|
|
467
|
-
if (this.nextAttemptTime && new Date < this.nextAttemptTime) {
|
|
468
|
-
const error2 = new CircuitBreakerOpenException(this.config.name, this.nextAttemptTime, call.context);
|
|
469
|
-
if (call.fallback) {
|
|
470
|
-
try {
|
|
471
|
-
return await call.fallback();
|
|
472
|
-
} catch (fallbackError) {
|
|
473
|
-
console.error(fallbackError);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
throw error2;
|
|
477
|
-
} else {
|
|
478
|
-
this.transitionToHalfOpen();
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
if (this.state === "HALF_OPEN" && this.halfOpenCallCount >= this.config.halfOpenMaxCalls) {
|
|
482
|
-
const error2 = new CircuitBreakerOpenException(this.config.name, this.nextAttemptTime || new Date(Date.now() + this.config.recoveryTimeout), call.context);
|
|
483
|
-
if (call.fallback) {
|
|
484
|
-
try {
|
|
485
|
-
return await call.fallback();
|
|
486
|
-
} catch (fallbackError) {
|
|
487
|
-
console.error(fallbackError);
|
|
488
|
-
throw error2;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
throw error2;
|
|
492
|
-
}
|
|
493
|
-
const startTime = Date.now();
|
|
494
|
-
let result;
|
|
495
|
-
let error = null;
|
|
496
|
-
try {
|
|
497
|
-
if (this.state === "HALF_OPEN") {
|
|
498
|
-
this.halfOpenCallCount++;
|
|
499
|
-
}
|
|
500
|
-
const timeout = call.timeout || this.config.timeout;
|
|
501
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
502
|
-
setTimeout(() => {
|
|
503
|
-
reject(new TimeoutException(`Circuit breaker timeout for ${this.config.name}`, timeout, call.context));
|
|
504
|
-
}, timeout);
|
|
505
|
-
});
|
|
506
|
-
result = await Promise.race([call.execute(), timeoutPromise]);
|
|
507
|
-
this.onSuccess(Date.now() - startTime);
|
|
508
|
-
return result;
|
|
509
|
-
} catch (err) {
|
|
510
|
-
error = err;
|
|
511
|
-
this.onFailure(error, Date.now() - startTime);
|
|
512
|
-
if (call.fallback) {
|
|
513
|
-
try {
|
|
514
|
-
return await call.fallback();
|
|
515
|
-
} catch (fallbackError) {
|
|
516
|
-
console.error(fallbackError);
|
|
517
|
-
throw error;
|
|
518
|
-
}
|
|
519
|
-
} else {
|
|
520
|
-
throw error;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
getState() {
|
|
525
|
-
return this.state;
|
|
526
|
-
}
|
|
527
|
-
getMetrics() {
|
|
528
|
-
return {
|
|
529
|
-
...this.metrics,
|
|
530
|
-
state: this.state,
|
|
531
|
-
nextAttemptTime: this.nextAttemptTime
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
forceOpen() {
|
|
535
|
-
this.transitionToOpen();
|
|
536
|
-
}
|
|
537
|
-
forceClosed() {
|
|
538
|
-
this.transitionToClosed();
|
|
539
|
-
}
|
|
540
|
-
reset() {
|
|
541
|
-
this.metrics = {
|
|
542
|
-
totalRequests: 0,
|
|
543
|
-
successfulRequests: 0,
|
|
544
|
-
failedRequests: 0,
|
|
545
|
-
timeouts: 0,
|
|
546
|
-
averageResponseTime: 0,
|
|
547
|
-
state: this.state,
|
|
548
|
-
consecutiveFailures: 0,
|
|
549
|
-
consecutiveSuccesses: 0
|
|
550
|
-
};
|
|
551
|
-
this.recentRequests = [];
|
|
552
|
-
}
|
|
553
|
-
onSuccess(responseTime) {
|
|
554
|
-
this.metrics.totalRequests++;
|
|
555
|
-
this.metrics.successfulRequests++;
|
|
556
|
-
this.metrics.consecutiveSuccesses++;
|
|
557
|
-
this.metrics.consecutiveFailures = 0;
|
|
558
|
-
this.metrics.lastSuccessTime = new Date;
|
|
559
|
-
this.updateAverageResponseTime(responseTime);
|
|
560
|
-
this.recentRequests.push({
|
|
561
|
-
timestamp: new Date,
|
|
562
|
-
success: true,
|
|
563
|
-
responseTime
|
|
564
|
-
});
|
|
565
|
-
if (this.state === "HALF_OPEN") {
|
|
566
|
-
if (this.metrics.consecutiveSuccesses >= this.config.halfOpenMaxCalls) {
|
|
567
|
-
this.transitionToClosed();
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (this.config.onSuccess) {
|
|
571
|
-
this.config.onSuccess(this.config.name);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
onFailure(error, responseTime) {
|
|
575
|
-
this.metrics.totalRequests++;
|
|
576
|
-
this.metrics.failedRequests++;
|
|
577
|
-
this.metrics.consecutiveFailures++;
|
|
578
|
-
this.metrics.consecutiveSuccesses = 0;
|
|
579
|
-
this.metrics.lastFailureTime = new Date;
|
|
580
|
-
if (error instanceof TimeoutException) {
|
|
581
|
-
this.metrics.timeouts++;
|
|
582
|
-
}
|
|
583
|
-
this.updateAverageResponseTime(responseTime);
|
|
584
|
-
this.recentRequests.push({
|
|
585
|
-
timestamp: new Date,
|
|
586
|
-
success: false,
|
|
587
|
-
responseTime
|
|
588
|
-
});
|
|
589
|
-
if (this.shouldTrip(error)) {
|
|
590
|
-
if (this.state === "CLOSED") {
|
|
591
|
-
this.transitionToOpen();
|
|
592
|
-
} else if (this.state === "HALF_OPEN") {
|
|
593
|
-
this.transitionToOpen();
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
if (this.config.onFailure) {
|
|
597
|
-
this.config.onFailure(error, this.config.name);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
shouldTrip(error) {
|
|
601
|
-
if (this.config.shouldTripOnError && !this.config.shouldTripOnError(error)) {
|
|
602
|
-
return false;
|
|
603
|
-
}
|
|
604
|
-
if (this.metrics.consecutiveFailures >= this.config.failureThreshold) {
|
|
605
|
-
return true;
|
|
606
|
-
}
|
|
607
|
-
const recentRequests = this.getRecentRequests();
|
|
608
|
-
if (recentRequests.length >= this.config.minimumRequests) {
|
|
609
|
-
const failedRequests = recentRequests.filter((r) => !r.success).length;
|
|
610
|
-
const errorRate = failedRequests / recentRequests.length * 100;
|
|
611
|
-
if (errorRate >= this.config.errorThresholdPercentage) {
|
|
612
|
-
return true;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
return false;
|
|
616
|
-
}
|
|
617
|
-
transitionToClosed() {
|
|
618
|
-
this.state = "CLOSED";
|
|
619
|
-
this.nextAttemptTime = undefined;
|
|
620
|
-
this.halfOpenCallCount = 0;
|
|
621
|
-
this.metrics.state = this.state;
|
|
622
|
-
if (this.config.onStateChange) {
|
|
623
|
-
this.config.onStateChange(this.state, this.config.name);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
transitionToOpen() {
|
|
627
|
-
this.state = "OPEN";
|
|
628
|
-
this.nextAttemptTime = new Date(Date.now() + this.config.recoveryTimeout);
|
|
629
|
-
this.halfOpenCallCount = 0;
|
|
630
|
-
this.metrics.state = this.state;
|
|
631
|
-
this.metrics.nextAttemptTime = this.nextAttemptTime;
|
|
632
|
-
if (this.config.onStateChange) {
|
|
633
|
-
this.config.onStateChange(this.state, this.config.name);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
transitionToHalfOpen() {
|
|
637
|
-
this.state = "HALF_OPEN";
|
|
638
|
-
this.halfOpenCallCount = 0;
|
|
639
|
-
this.metrics.state = this.state;
|
|
640
|
-
if (this.config.onStateChange) {
|
|
641
|
-
this.config.onStateChange(this.state, this.config.name);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
updateAverageResponseTime(responseTime) {
|
|
645
|
-
if (this.metrics.totalRequests === 1) {
|
|
646
|
-
this.metrics.averageResponseTime = responseTime;
|
|
647
|
-
} else {
|
|
648
|
-
const alpha = 0.1;
|
|
649
|
-
this.metrics.averageResponseTime = alpha * responseTime + (1 - alpha) * this.metrics.averageResponseTime;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
getRecentRequests() {
|
|
653
|
-
const cutoff = new Date(Date.now() - this.config.monitoringPeriod);
|
|
654
|
-
return this.recentRequests.filter((r) => r.timestamp > cutoff);
|
|
655
|
-
}
|
|
656
|
-
cleanupOldRequests() {
|
|
657
|
-
const cutoff = new Date(Date.now() - this.config.monitoringPeriod);
|
|
658
|
-
this.recentRequests = this.recentRequests.filter((r) => r.timestamp > cutoff);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
class CircuitBreakerRegistry {
|
|
663
|
-
breakers = new Map;
|
|
664
|
-
configs = new Map;
|
|
665
|
-
register(config2) {
|
|
666
|
-
const breaker = new CircuitBreaker(config2);
|
|
667
|
-
this.breakers.set(config2.name, breaker);
|
|
668
|
-
this.configs.set(config2.name, config2);
|
|
669
|
-
return breaker;
|
|
670
|
-
}
|
|
671
|
-
get(name) {
|
|
672
|
-
return this.breakers.get(name);
|
|
673
|
-
}
|
|
674
|
-
async execute(name, call) {
|
|
675
|
-
const breaker = this.breakers.get(name);
|
|
676
|
-
if (!breaker) {
|
|
677
|
-
throw new Error(`Circuit breaker '${name}' not found`);
|
|
678
|
-
}
|
|
679
|
-
return breaker.execute(call);
|
|
680
|
-
}
|
|
681
|
-
getAllMetrics() {
|
|
682
|
-
const metrics = {};
|
|
683
|
-
for (const [name, breaker] of this.breakers) {
|
|
684
|
-
metrics[name] = breaker.getMetrics();
|
|
685
|
-
}
|
|
686
|
-
return metrics;
|
|
687
|
-
}
|
|
688
|
-
getBreakersByState(state) {
|
|
689
|
-
const names = [];
|
|
690
|
-
for (const [name, breaker] of this.breakers) {
|
|
691
|
-
if (breaker.getState() === state) {
|
|
692
|
-
names.push(name);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
return names;
|
|
696
|
-
}
|
|
697
|
-
forceAllClosed() {
|
|
698
|
-
for (const breaker of this.breakers.values()) {
|
|
699
|
-
breaker.forceClosed();
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
resetAll() {
|
|
703
|
-
for (const breaker of this.breakers.values()) {
|
|
704
|
-
breaker.reset();
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
remove(name) {
|
|
708
|
-
this.configs.delete(name);
|
|
709
|
-
return this.breakers.delete(name);
|
|
710
|
-
}
|
|
711
|
-
getNames() {
|
|
712
|
-
return Array.from(this.breakers.keys());
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
function circuitBreaker(config2) {
|
|
716
|
-
const breaker = new CircuitBreaker(config2);
|
|
717
|
-
return function(target, propertyName, descriptor) {
|
|
718
|
-
const method = descriptor.value;
|
|
719
|
-
descriptor.value = async function(...args) {
|
|
720
|
-
return breaker.execute({
|
|
721
|
-
execute: () => method.apply(this, args),
|
|
722
|
-
context: {
|
|
723
|
-
method: propertyName,
|
|
724
|
-
timestamp: new Date
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
};
|
|
728
|
-
return descriptor;
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
class CircuitBreakerHttpClient {
|
|
733
|
-
registry;
|
|
734
|
-
constructor(registry) {
|
|
735
|
-
this.registry = registry || new CircuitBreakerRegistry;
|
|
736
|
-
}
|
|
737
|
-
async fetch(url, options = {}) {
|
|
738
|
-
const { circuitBreaker: circuitBreaker2, fallback, context, ...fetchOptions } = options;
|
|
739
|
-
let breakerName;
|
|
740
|
-
if (typeof circuitBreaker2 === "string") {
|
|
741
|
-
breakerName = circuitBreaker2;
|
|
742
|
-
} else if (circuitBreaker2) {
|
|
743
|
-
breakerName = circuitBreaker2.name;
|
|
744
|
-
if (!this.registry.get(breakerName)) {
|
|
745
|
-
this.registry.register(circuitBreaker2);
|
|
746
|
-
}
|
|
747
|
-
} else {
|
|
748
|
-
const hostname = new URL(url).hostname;
|
|
749
|
-
breakerName = `http-${hostname}`;
|
|
750
|
-
if (!this.registry.get(breakerName)) {
|
|
751
|
-
this.registry.register({
|
|
752
|
-
name: breakerName,
|
|
753
|
-
failureThreshold: 5,
|
|
754
|
-
recoveryTimeout: 60000,
|
|
755
|
-
timeout: 30000,
|
|
756
|
-
monitoringPeriod: 60000,
|
|
757
|
-
minimumRequests: 10,
|
|
758
|
-
errorThresholdPercentage: 50,
|
|
759
|
-
halfOpenMaxCalls: 3,
|
|
760
|
-
resetTimeout: 60000
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return this.registry.execute(breakerName, {
|
|
765
|
-
execute: async () => {
|
|
766
|
-
const response = await fetch(url, fetchOptions);
|
|
767
|
-
if (response.status >= 500) {
|
|
768
|
-
throw new ExternalServiceException(`HTTP ${response.status}: ${response.statusText}`, breakerName, url, response.status, undefined, context);
|
|
769
|
-
}
|
|
770
|
-
return response;
|
|
771
|
-
},
|
|
772
|
-
fallback,
|
|
773
|
-
context
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
getRegistry() {
|
|
777
|
-
return this.registry;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
function createCircuitBreakerMiddleware(registry) {
|
|
781
|
-
return async (req, next) => {
|
|
782
|
-
const serviceName = req.context?.serviceName;
|
|
783
|
-
if (!serviceName) {
|
|
784
|
-
return next();
|
|
785
|
-
}
|
|
786
|
-
const breaker = registry.get(serviceName);
|
|
787
|
-
if (!breaker) {
|
|
788
|
-
return next();
|
|
789
|
-
}
|
|
790
|
-
return breaker.execute({
|
|
791
|
-
execute: next,
|
|
792
|
-
context: {
|
|
793
|
-
requestId: req.requestId,
|
|
794
|
-
route: req.route,
|
|
795
|
-
method: req.method,
|
|
796
|
-
url: req.url
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
var CircuitBreakerPresets = {
|
|
802
|
-
critical: (name) => ({
|
|
803
|
-
name,
|
|
804
|
-
failureThreshold: 3,
|
|
805
|
-
recoveryTimeout: 30000,
|
|
806
|
-
timeout: 5000,
|
|
807
|
-
monitoringPeriod: 30000,
|
|
808
|
-
minimumRequests: 5,
|
|
809
|
-
errorThresholdPercentage: 30,
|
|
810
|
-
halfOpenMaxCalls: 2,
|
|
811
|
-
resetTimeout: 30000
|
|
812
|
-
}),
|
|
813
|
-
standard: (name) => ({
|
|
814
|
-
name,
|
|
815
|
-
failureThreshold: 5,
|
|
816
|
-
recoveryTimeout: 60000,
|
|
817
|
-
timeout: 30000,
|
|
818
|
-
monitoringPeriod: 60000,
|
|
819
|
-
minimumRequests: 10,
|
|
820
|
-
errorThresholdPercentage: 50,
|
|
821
|
-
halfOpenMaxCalls: 3,
|
|
822
|
-
resetTimeout: 60000
|
|
823
|
-
}),
|
|
824
|
-
tolerant: (name) => ({
|
|
825
|
-
name,
|
|
826
|
-
failureThreshold: 10,
|
|
827
|
-
recoveryTimeout: 120000,
|
|
828
|
-
timeout: 60000,
|
|
829
|
-
monitoringPeriod: 120000,
|
|
830
|
-
minimumRequests: 20,
|
|
831
|
-
errorThresholdPercentage: 70,
|
|
832
|
-
halfOpenMaxCalls: 5,
|
|
833
|
-
resetTimeout: 120000
|
|
834
|
-
}),
|
|
835
|
-
database: (name) => ({
|
|
836
|
-
name,
|
|
837
|
-
failureThreshold: 3,
|
|
838
|
-
recoveryTimeout: 60000,
|
|
839
|
-
timeout: 1e4,
|
|
840
|
-
monitoringPeriod: 60000,
|
|
841
|
-
minimumRequests: 5,
|
|
842
|
-
errorThresholdPercentage: 40,
|
|
843
|
-
halfOpenMaxCalls: 2,
|
|
844
|
-
resetTimeout: 60000,
|
|
845
|
-
shouldTripOnError: (error) => {
|
|
846
|
-
return !(error.message.includes("validation") || error.message.includes("constraint"));
|
|
847
|
-
}
|
|
848
|
-
}),
|
|
849
|
-
externalApi: (name) => ({
|
|
850
|
-
name,
|
|
851
|
-
failureThreshold: 5,
|
|
852
|
-
recoveryTimeout: 90000,
|
|
853
|
-
timeout: 15000,
|
|
854
|
-
monitoringPeriod: 90000,
|
|
855
|
-
minimumRequests: 8,
|
|
856
|
-
errorThresholdPercentage: 60,
|
|
857
|
-
halfOpenMaxCalls: 3,
|
|
858
|
-
resetTimeout: 90000,
|
|
859
|
-
shouldTripOnError: (error) => {
|
|
860
|
-
if (error instanceof ExternalServiceException) {
|
|
861
|
-
return error.statusCode >= 500;
|
|
862
|
-
}
|
|
863
|
-
return true;
|
|
864
|
-
}
|
|
865
|
-
})
|
|
866
|
-
};
|
|
867
|
-
var globalCircuitBreakerRegistry = new CircuitBreakerRegistry;
|
|
868
|
-
function createCircuitBreaker(config2) {
|
|
869
|
-
return globalCircuitBreakerRegistry.register(config2);
|
|
870
|
-
}
|
|
871
|
-
async function withCircuitBreaker(name, call) {
|
|
872
|
-
return globalCircuitBreakerRegistry.execute(name, call);
|
|
873
|
-
}
|
|
874
|
-
// src/errors/error-reporting.ts
|
|
875
|
-
class SentryReporter {
|
|
876
|
-
config;
|
|
877
|
-
sentry;
|
|
878
|
-
constructor(config2) {
|
|
879
|
-
this.config = config2;
|
|
880
|
-
this.initializeSentry();
|
|
881
|
-
}
|
|
882
|
-
async initializeSentry() {
|
|
883
|
-
try {
|
|
884
|
-
const Sentry = await import("@sentry/node");
|
|
885
|
-
Sentry.init({
|
|
886
|
-
dsn: this.config.dsn,
|
|
887
|
-
environment: this.config.environment || "production",
|
|
888
|
-
release: this.config.release,
|
|
889
|
-
sampleRate: this.config.sampleRate || 1,
|
|
890
|
-
beforeSend: (event, hint) => {
|
|
891
|
-
const error = hint.originalException;
|
|
892
|
-
if (error && this.config.beforeSend) {
|
|
893
|
-
const filteredError = this.config.beforeSend(error, error.context);
|
|
894
|
-
return filteredError ? event : null;
|
|
895
|
-
}
|
|
896
|
-
return event;
|
|
897
|
-
},
|
|
898
|
-
ignoreErrors: this.config.filters?.ignoreErrors,
|
|
899
|
-
denyUrls: this.config.filters?.ignoreUrls,
|
|
900
|
-
allowUrls: this.config.filters?.allowUrls,
|
|
901
|
-
initialScope: {
|
|
902
|
-
tags: this.config.tags,
|
|
903
|
-
extra: this.config.extra,
|
|
904
|
-
user: this.config.user
|
|
905
|
-
},
|
|
906
|
-
maxBreadcrumbs: this.config.breadcrumbs?.maxBreadcrumbs || 100,
|
|
907
|
-
tracesSampleRate: this.config.performance?.tracesSampleRate || 0.1
|
|
908
|
-
});
|
|
909
|
-
this.sentry = Sentry;
|
|
910
|
-
} catch (error) {
|
|
911
|
-
console.warn("Failed to initialize Sentry:", error);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
async report(error, context) {
|
|
915
|
-
if (!this.sentry || !this.config.enabled) {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
try {
|
|
919
|
-
if (this.shouldIgnoreError(error)) {
|
|
920
|
-
return null;
|
|
921
|
-
}
|
|
922
|
-
const mergedContext = { ...error.context, ...context };
|
|
923
|
-
return this.sentry.withScope((scope) => {
|
|
924
|
-
if (mergedContext.requestId) {
|
|
925
|
-
scope.setTag("requestId", mergedContext.requestId);
|
|
926
|
-
}
|
|
927
|
-
if (mergedContext.userId) {
|
|
928
|
-
scope.setUser({ id: mergedContext.userId });
|
|
929
|
-
}
|
|
930
|
-
if (mergedContext.traceId) {
|
|
931
|
-
scope.setTag("traceId", mergedContext.traceId);
|
|
932
|
-
}
|
|
933
|
-
if (mergedContext.route) {
|
|
934
|
-
scope.setTag("route", mergedContext.route);
|
|
935
|
-
}
|
|
936
|
-
if (mergedContext.method) {
|
|
937
|
-
scope.setTag("method", mergedContext.method);
|
|
938
|
-
}
|
|
939
|
-
scope.setTag("errorCode", error.code);
|
|
940
|
-
scope.setTag("severity", error.severity);
|
|
941
|
-
scope.setTag("category", error.category);
|
|
942
|
-
scope.setLevel(this.mapSeverityToLevel(error.severity));
|
|
943
|
-
scope.setExtra("statusCode", error.statusCode);
|
|
944
|
-
scope.setExtra("retryable", error.retryable);
|
|
945
|
-
scope.setExtra("timestamp", error.timestamp);
|
|
946
|
-
scope.setExtra("context", mergedContext);
|
|
947
|
-
const fingerprint = this.generateFingerprint(error);
|
|
948
|
-
if (fingerprint.length > 0) {
|
|
949
|
-
scope.setFingerprint(fingerprint);
|
|
950
|
-
}
|
|
951
|
-
return this.sentry.captureException(error);
|
|
952
|
-
});
|
|
953
|
-
} catch (reportingError) {
|
|
954
|
-
console.error("Failed to report error to Sentry:", reportingError);
|
|
955
|
-
return null;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
addBreadcrumb(message, category = "default", level = "info", data) {
|
|
959
|
-
if (!this.sentry || !this.config.breadcrumbs?.enabled) {
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
this.sentry.addBreadcrumb({
|
|
963
|
-
message,
|
|
964
|
-
category,
|
|
965
|
-
level,
|
|
966
|
-
data,
|
|
967
|
-
timestamp: Date.now() / 1000
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
setUser(user) {
|
|
971
|
-
if (!this.sentry)
|
|
972
|
-
return;
|
|
973
|
-
this.sentry.setUser(user);
|
|
974
|
-
}
|
|
975
|
-
setTag(key, value) {
|
|
976
|
-
if (!this.sentry)
|
|
977
|
-
return;
|
|
978
|
-
this.sentry.setTag(key, value);
|
|
979
|
-
}
|
|
980
|
-
setExtra(key, value) {
|
|
981
|
-
if (!this.sentry)
|
|
982
|
-
return;
|
|
983
|
-
this.sentry.setExtra(key, value);
|
|
984
|
-
}
|
|
985
|
-
setContext(key, context) {
|
|
986
|
-
if (!this.sentry)
|
|
987
|
-
return;
|
|
988
|
-
this.sentry.setContext(key, context);
|
|
989
|
-
}
|
|
990
|
-
async close(timeout = 2000) {
|
|
991
|
-
if (!this.sentry)
|
|
992
|
-
return true;
|
|
993
|
-
return this.sentry.close(timeout);
|
|
994
|
-
}
|
|
995
|
-
shouldIgnoreError(error) {
|
|
996
|
-
const filters = this.config.filters;
|
|
997
|
-
if (!filters)
|
|
998
|
-
return false;
|
|
999
|
-
if (filters.ignoreCodes?.includes(error.code)) {
|
|
1000
|
-
return true;
|
|
1001
|
-
}
|
|
1002
|
-
if (filters.ignoreErrors?.some((pattern) => {
|
|
1003
|
-
if (typeof pattern === "string") {
|
|
1004
|
-
return error.message.includes(pattern);
|
|
1005
|
-
}
|
|
1006
|
-
return pattern.test(error.message);
|
|
1007
|
-
})) {
|
|
1008
|
-
return true;
|
|
1009
|
-
}
|
|
1010
|
-
return false;
|
|
1011
|
-
}
|
|
1012
|
-
mapSeverityToLevel(severity) {
|
|
1013
|
-
switch (severity) {
|
|
1014
|
-
case "low":
|
|
1015
|
-
return "info";
|
|
1016
|
-
case "medium":
|
|
1017
|
-
return "warning";
|
|
1018
|
-
case "high":
|
|
1019
|
-
return "error";
|
|
1020
|
-
case "critical":
|
|
1021
|
-
return "fatal";
|
|
1022
|
-
default:
|
|
1023
|
-
return "error";
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
generateFingerprint(error) {
|
|
1027
|
-
const fingerprint = [];
|
|
1028
|
-
fingerprint.push(error.code);
|
|
1029
|
-
fingerprint.push(error.category);
|
|
1030
|
-
if (error.context.route) {
|
|
1031
|
-
fingerprint.push(error.context.route);
|
|
1032
|
-
}
|
|
1033
|
-
return fingerprint;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
class BugsnagReporter {
|
|
1038
|
-
config;
|
|
1039
|
-
bugsnag;
|
|
1040
|
-
constructor(config2) {
|
|
1041
|
-
this.config = config2;
|
|
1042
|
-
this.initializeBugsnag();
|
|
1043
|
-
}
|
|
1044
|
-
async initializeBugsnag() {
|
|
1045
|
-
try {
|
|
1046
|
-
const Bugsnag = await import("@bugsnag/js");
|
|
1047
|
-
Bugsnag.start({
|
|
1048
|
-
apiKey: this.config.apiKey,
|
|
1049
|
-
appVersion: this.config.release,
|
|
1050
|
-
releaseStage: this.config.environment || "production",
|
|
1051
|
-
enabledReleaseStages: ["production", "staging"],
|
|
1052
|
-
maxBreadcrumbs: this.config.breadcrumbs?.maxBreadcrumbs || 25,
|
|
1053
|
-
onError: (event) => {
|
|
1054
|
-
const error = event.originalError;
|
|
1055
|
-
if (error && this.config.beforeSend) {
|
|
1056
|
-
const filteredError = this.config.beforeSend(error, error.context);
|
|
1057
|
-
return filteredError !== null;
|
|
1058
|
-
}
|
|
1059
|
-
return !this.shouldIgnoreError(error);
|
|
1060
|
-
}
|
|
1061
|
-
});
|
|
1062
|
-
this.bugsnag = Bugsnag;
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
console.warn("Failed to initialize Bugsnag:", error);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
async report(error, context) {
|
|
1068
|
-
if (!this.bugsnag || !this.config.enabled) {
|
|
1069
|
-
return null;
|
|
1070
|
-
}
|
|
1071
|
-
try {
|
|
1072
|
-
if (this.shouldIgnoreError(error)) {
|
|
1073
|
-
return null;
|
|
1074
|
-
}
|
|
1075
|
-
const mergedContext = { ...error.context, ...context };
|
|
1076
|
-
this.bugsnag.notify(error, (event) => {
|
|
1077
|
-
event.severity = this.mapSeverityToBugsnag(error.severity);
|
|
1078
|
-
event.context = mergedContext.route || mergedContext.url;
|
|
1079
|
-
if (mergedContext.userId) {
|
|
1080
|
-
event.setUser(mergedContext.userId, undefined, undefined);
|
|
1081
|
-
}
|
|
1082
|
-
event.addMetadata("error", {
|
|
1083
|
-
code: error.code,
|
|
1084
|
-
category: error.category,
|
|
1085
|
-
statusCode: error.statusCode,
|
|
1086
|
-
retryable: error.retryable,
|
|
1087
|
-
timestamp: error.timestamp
|
|
1088
|
-
});
|
|
1089
|
-
event.addMetadata("request", {
|
|
1090
|
-
requestId: mergedContext.requestId,
|
|
1091
|
-
method: mergedContext.method,
|
|
1092
|
-
url: mergedContext.url,
|
|
1093
|
-
userAgent: mergedContext.userAgent,
|
|
1094
|
-
ip: mergedContext.ip
|
|
1095
|
-
});
|
|
1096
|
-
event.groupingHash = this.generateGroupingHash(error);
|
|
1097
|
-
});
|
|
1098
|
-
return "reported";
|
|
1099
|
-
} catch (reportingError) {
|
|
1100
|
-
console.error("Failed to report error to Bugsnag:", reportingError);
|
|
1101
|
-
return null;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
addBreadcrumb(message, category = "manual", _level = "info", data) {
|
|
1105
|
-
if (!this.bugsnag || !this.config.breadcrumbs?.enabled) {
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
this.bugsnag.leaveBreadcrumb(message, data, category);
|
|
1109
|
-
}
|
|
1110
|
-
setUser(user) {
|
|
1111
|
-
if (!this.bugsnag)
|
|
1112
|
-
return;
|
|
1113
|
-
this.bugsnag.setUser(user.id, user.email, user.username);
|
|
1114
|
-
}
|
|
1115
|
-
setTag(key, value) {
|
|
1116
|
-
if (!this.bugsnag)
|
|
1117
|
-
return;
|
|
1118
|
-
this.bugsnag.addMetadata("tags", { [key]: value });
|
|
1119
|
-
}
|
|
1120
|
-
setExtra(key, value) {
|
|
1121
|
-
if (!this.bugsnag)
|
|
1122
|
-
return;
|
|
1123
|
-
this.bugsnag.addMetadata("extra", { [key]: value });
|
|
1124
|
-
}
|
|
1125
|
-
setContext(key, context) {
|
|
1126
|
-
if (!this.bugsnag)
|
|
1127
|
-
return;
|
|
1128
|
-
this.bugsnag.addMetadata(key, context);
|
|
1129
|
-
}
|
|
1130
|
-
async close() {
|
|
1131
|
-
return true;
|
|
1132
|
-
}
|
|
1133
|
-
shouldIgnoreError(error) {
|
|
1134
|
-
const filters = this.config.filters;
|
|
1135
|
-
if (!filters)
|
|
1136
|
-
return false;
|
|
1137
|
-
if (filters.ignoreCodes?.includes(error.code)) {
|
|
1138
|
-
return true;
|
|
1139
|
-
}
|
|
1140
|
-
if (filters.ignoreErrors?.some((pattern) => {
|
|
1141
|
-
if (typeof pattern === "string") {
|
|
1142
|
-
return error.message.includes(pattern);
|
|
1143
|
-
}
|
|
1144
|
-
return pattern.test(error.message);
|
|
1145
|
-
})) {
|
|
1146
|
-
return true;
|
|
1147
|
-
}
|
|
1148
|
-
return false;
|
|
1149
|
-
}
|
|
1150
|
-
mapSeverityToBugsnag(severity) {
|
|
1151
|
-
switch (severity) {
|
|
1152
|
-
case "low":
|
|
1153
|
-
return "info";
|
|
1154
|
-
case "medium":
|
|
1155
|
-
return "warning";
|
|
1156
|
-
case "high":
|
|
1157
|
-
return "error";
|
|
1158
|
-
case "critical":
|
|
1159
|
-
return "error";
|
|
1160
|
-
default:
|
|
1161
|
-
return "warning";
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
generateGroupingHash(error) {
|
|
1165
|
-
return `${error.code}-${error.category}-${error.context.route || "unknown"}`;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
class CustomReporter {
|
|
1170
|
-
config;
|
|
1171
|
-
reportHandler;
|
|
1172
|
-
constructor(config2, reportHandler) {
|
|
1173
|
-
this.config = config2;
|
|
1174
|
-
this.reportHandler = reportHandler;
|
|
1175
|
-
}
|
|
1176
|
-
async report(error, context) {
|
|
1177
|
-
if (!this.config.enabled) {
|
|
1178
|
-
return null;
|
|
1179
|
-
}
|
|
1180
|
-
try {
|
|
1181
|
-
const mergedContext = { ...error.context, ...context };
|
|
1182
|
-
if (this.config.beforeSend) {
|
|
1183
|
-
const filteredError = this.config.beforeSend(error, mergedContext);
|
|
1184
|
-
if (!filteredError) {
|
|
1185
|
-
return null;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
const report = {
|
|
1189
|
-
error,
|
|
1190
|
-
context: mergedContext,
|
|
1191
|
-
level: this.mapSeverityToLevel(error.severity),
|
|
1192
|
-
tags: this.config.tags,
|
|
1193
|
-
extra: this.config.extra,
|
|
1194
|
-
user: this.config.user
|
|
1195
|
-
};
|
|
1196
|
-
return await this.reportHandler(report);
|
|
1197
|
-
} catch (reportingError) {
|
|
1198
|
-
console.error("Failed to report error with custom handler:", reportingError);
|
|
1199
|
-
return null;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
addBreadcrumb() {}
|
|
1203
|
-
setUser() {}
|
|
1204
|
-
setTag() {}
|
|
1205
|
-
setExtra() {}
|
|
1206
|
-
setContext() {}
|
|
1207
|
-
async close() {
|
|
1208
|
-
return true;
|
|
1209
|
-
}
|
|
1210
|
-
mapSeverityToLevel(severity) {
|
|
1211
|
-
switch (severity) {
|
|
1212
|
-
case "low":
|
|
1213
|
-
return "info";
|
|
1214
|
-
case "medium":
|
|
1215
|
-
return "warning";
|
|
1216
|
-
case "high":
|
|
1217
|
-
return "error";
|
|
1218
|
-
case "critical":
|
|
1219
|
-
return "fatal";
|
|
1220
|
-
default:
|
|
1221
|
-
return "error";
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
class ErrorReportingManager {
|
|
1227
|
-
reporters = [];
|
|
1228
|
-
breadcrumbs = [];
|
|
1229
|
-
maxBreadcrumbs = 50;
|
|
1230
|
-
constructor(configs) {
|
|
1231
|
-
this.initializeReporters(configs);
|
|
1232
|
-
}
|
|
1233
|
-
initializeReporters(configs) {
|
|
1234
|
-
for (const config2 of configs) {
|
|
1235
|
-
if (!config2.enabled)
|
|
1236
|
-
continue;
|
|
1237
|
-
switch (config2.service) {
|
|
1238
|
-
case "sentry":
|
|
1239
|
-
this.reporters.push(new SentryReporter(config2));
|
|
1240
|
-
break;
|
|
1241
|
-
case "bugsnag":
|
|
1242
|
-
this.reporters.push(new BugsnagReporter(config2));
|
|
1243
|
-
break;
|
|
1244
|
-
case "custom":
|
|
1245
|
-
break;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
addReporter(reporter) {
|
|
1250
|
-
this.reporters.push(reporter);
|
|
1251
|
-
}
|
|
1252
|
-
async report(error, context) {
|
|
1253
|
-
const reportIds = [];
|
|
1254
|
-
this.addBreadcrumb(`Error occurred: ${error.message}`, "error", {
|
|
1255
|
-
code: error.code,
|
|
1256
|
-
severity: error.severity
|
|
1257
|
-
});
|
|
1258
|
-
const promises = this.reporters.map(async (reporter) => {
|
|
1259
|
-
try {
|
|
1260
|
-
const reportId = await reporter.report(error, context);
|
|
1261
|
-
if (reportId) {
|
|
1262
|
-
reportIds.push(reportId);
|
|
1263
|
-
}
|
|
1264
|
-
} catch (reportingError) {
|
|
1265
|
-
console.error("Reporter failed:", reportingError);
|
|
1266
|
-
}
|
|
1267
|
-
});
|
|
1268
|
-
await Promise.allSettled(promises);
|
|
1269
|
-
return reportIds;
|
|
1270
|
-
}
|
|
1271
|
-
addBreadcrumb(message, category = "default", data) {
|
|
1272
|
-
this.breadcrumbs.push({
|
|
1273
|
-
message,
|
|
1274
|
-
timestamp: new Date,
|
|
1275
|
-
data: { category, ...data }
|
|
1276
|
-
});
|
|
1277
|
-
if (this.breadcrumbs.length > this.maxBreadcrumbs) {
|
|
1278
|
-
this.breadcrumbs = this.breadcrumbs.slice(-this.maxBreadcrumbs);
|
|
1279
|
-
}
|
|
1280
|
-
this.reporters.forEach((reporter) => {
|
|
1281
|
-
reporter.addBreadcrumb(message, category, "info", data);
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
setUser(user) {
|
|
1285
|
-
this.reporters.forEach((reporter) => reporter.setUser(user));
|
|
1286
|
-
}
|
|
1287
|
-
setTag(key, value) {
|
|
1288
|
-
this.reporters.forEach((reporter) => reporter.setTag(key, value));
|
|
1289
|
-
}
|
|
1290
|
-
setExtra(key, value) {
|
|
1291
|
-
this.reporters.forEach((reporter) => reporter.setExtra(key, value));
|
|
1292
|
-
}
|
|
1293
|
-
setContext(key, context) {
|
|
1294
|
-
this.reporters.forEach((reporter) => reporter.setContext(key, context));
|
|
1295
|
-
}
|
|
1296
|
-
async close(timeout = 5000) {
|
|
1297
|
-
const promises = this.reporters.map((reporter) => reporter.close(timeout));
|
|
1298
|
-
const results = await Promise.allSettled(promises);
|
|
1299
|
-
return results.every((result) => result.status === "fulfilled" && result.value);
|
|
1300
|
-
}
|
|
1301
|
-
getBreadcrumbs() {
|
|
1302
|
-
return [...this.breadcrumbs];
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
function createErrorReporting(configs) {
|
|
1306
|
-
return new ErrorReportingManager(configs);
|
|
1307
|
-
}
|
|
1308
|
-
var ErrorReportingPresets = {
|
|
1309
|
-
sentry: (dsn, environment = "production") => ({
|
|
1310
|
-
enabled: true,
|
|
1311
|
-
service: "sentry",
|
|
1312
|
-
dsn,
|
|
1313
|
-
environment,
|
|
1314
|
-
sampleRate: 1,
|
|
1315
|
-
breadcrumbs: {
|
|
1316
|
-
enabled: true,
|
|
1317
|
-
maxBreadcrumbs: 100
|
|
1318
|
-
},
|
|
1319
|
-
performance: {
|
|
1320
|
-
enabled: true,
|
|
1321
|
-
tracesSampleRate: 0.1
|
|
1322
|
-
},
|
|
1323
|
-
filters: {
|
|
1324
|
-
ignoreErrors: [
|
|
1325
|
-
"Non-Error promise rejection captured",
|
|
1326
|
-
"ResizeObserver loop limit exceeded"
|
|
1327
|
-
],
|
|
1328
|
-
ignoreCodes: ["VALIDATION_ERROR"]
|
|
1329
|
-
}
|
|
1330
|
-
}),
|
|
1331
|
-
bugsnag: (apiKey, environment = "production") => ({
|
|
1332
|
-
enabled: true,
|
|
1333
|
-
service: "bugsnag",
|
|
1334
|
-
apiKey,
|
|
1335
|
-
environment,
|
|
1336
|
-
breadcrumbs: {
|
|
1337
|
-
enabled: true,
|
|
1338
|
-
maxBreadcrumbs: 25
|
|
1339
|
-
},
|
|
1340
|
-
filters: {
|
|
1341
|
-
ignoreCodes: ["VALIDATION_ERROR"]
|
|
1342
|
-
}
|
|
1343
|
-
}),
|
|
1344
|
-
development: () => ({
|
|
1345
|
-
enabled: false,
|
|
1346
|
-
service: "custom",
|
|
1347
|
-
environment: "development"
|
|
1348
|
-
})
|
|
1349
|
-
};
|
|
1350
|
-
|
|
1351
|
-
// src/errors/graceful-degradation.ts
|
|
1352
|
-
class ServiceHealthMonitor {
|
|
1353
|
-
healthStatus = new Map;
|
|
1354
|
-
healthCheckIntervals = new Map;
|
|
1355
|
-
config;
|
|
1356
|
-
constructor(config2) {
|
|
1357
|
-
this.config = config2;
|
|
1358
|
-
this.initializeHealthChecks();
|
|
1359
|
-
}
|
|
1360
|
-
initializeHealthChecks() {
|
|
1361
|
-
for (const [serviceName, healthConfig] of Object.entries(this.config.healthChecks)) {
|
|
1362
|
-
if (!healthConfig.enabled)
|
|
1363
|
-
continue;
|
|
1364
|
-
this.healthStatus.set(serviceName, {
|
|
1365
|
-
name: serviceName,
|
|
1366
|
-
status: "healthy",
|
|
1367
|
-
lastCheck: new Date,
|
|
1368
|
-
responseTime: 0,
|
|
1369
|
-
errorRate: 0,
|
|
1370
|
-
availability: 100,
|
|
1371
|
-
consecutiveFailures: 0
|
|
1372
|
-
});
|
|
1373
|
-
const interval = setInterval(() => this.performHealthCheck(serviceName, healthConfig), healthConfig.interval);
|
|
1374
|
-
interval.unref?.();
|
|
1375
|
-
this.healthCheckIntervals.set(serviceName, interval);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
async performHealthCheck(serviceName, config2) {
|
|
1379
|
-
const startTime = Date.now();
|
|
1380
|
-
let isHealthy = false;
|
|
1381
|
-
try {
|
|
1382
|
-
const controller = new AbortController;
|
|
1383
|
-
const timeoutId = setTimeout(() => controller.abort(), config2.timeout);
|
|
1384
|
-
const response = await fetch(config2.endpoint, {
|
|
1385
|
-
signal: controller.signal,
|
|
1386
|
-
headers: config2.headers,
|
|
1387
|
-
method: "GET"
|
|
1388
|
-
});
|
|
1389
|
-
clearTimeout(timeoutId);
|
|
1390
|
-
isHealthy = config2.expectedStatus.includes(response.status);
|
|
1391
|
-
if (isHealthy && config2.expectedBody) {
|
|
1392
|
-
const body = await response.text();
|
|
1393
|
-
if (typeof config2.expectedBody === "string") {
|
|
1394
|
-
isHealthy = body.includes(config2.expectedBody);
|
|
1395
|
-
} else {
|
|
1396
|
-
isHealthy = config2.expectedBody.test(body);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
} catch (error) {
|
|
1400
|
-
console.error(error);
|
|
1401
|
-
isHealthy = false;
|
|
1402
|
-
}
|
|
1403
|
-
const responseTime = Date.now() - startTime;
|
|
1404
|
-
this.updateHealthStatus(serviceName, isHealthy, responseTime);
|
|
1405
|
-
if (isHealthy && config2.onHealthy) {
|
|
1406
|
-
config2.onHealthy();
|
|
1407
|
-
} else if (!isHealthy && config2.onUnhealthy) {
|
|
1408
|
-
config2.onUnhealthy(new Error(`Health check failed for ${serviceName}`));
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
updateHealthStatus(serviceName, isHealthy, responseTime) {
|
|
1412
|
-
const current = this.healthStatus.get(serviceName);
|
|
1413
|
-
if (!current)
|
|
1414
|
-
return;
|
|
1415
|
-
const updated = {
|
|
1416
|
-
...current,
|
|
1417
|
-
lastCheck: new Date,
|
|
1418
|
-
responseTime,
|
|
1419
|
-
consecutiveFailures: isHealthy ? 0 : current.consecutiveFailures + 1
|
|
1420
|
-
};
|
|
1421
|
-
if (updated.consecutiveFailures === 0) {
|
|
1422
|
-
updated.status = "healthy";
|
|
1423
|
-
} else if (updated.consecutiveFailures < 3) {
|
|
1424
|
-
updated.status = "degraded";
|
|
1425
|
-
} else {
|
|
1426
|
-
updated.status = "unhealthy";
|
|
1427
|
-
}
|
|
1428
|
-
const healthyCount = isHealthy ? 1 : 0;
|
|
1429
|
-
updated.availability = current.availability * 0.9 + healthyCount * 10;
|
|
1430
|
-
this.healthStatus.set(serviceName, updated);
|
|
1431
|
-
}
|
|
1432
|
-
getServiceHealth(serviceName) {
|
|
1433
|
-
return this.healthStatus.get(serviceName);
|
|
1434
|
-
}
|
|
1435
|
-
getAllServiceHealth() {
|
|
1436
|
-
return Array.from(this.healthStatus.values());
|
|
1437
|
-
}
|
|
1438
|
-
isServiceHealthy(serviceName) {
|
|
1439
|
-
const health = this.healthStatus.get(serviceName);
|
|
1440
|
-
return health?.status === "healthy";
|
|
1441
|
-
}
|
|
1442
|
-
isServiceDegraded(serviceName) {
|
|
1443
|
-
const health = this.healthStatus.get(serviceName);
|
|
1444
|
-
return health?.status === "degraded";
|
|
1445
|
-
}
|
|
1446
|
-
stop() {
|
|
1447
|
-
for (const interval of this.healthCheckIntervals.values()) {
|
|
1448
|
-
clearInterval(interval);
|
|
1449
|
-
}
|
|
1450
|
-
this.healthCheckIntervals.clear();
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
class GracefulDegradationManager {
|
|
1455
|
-
config;
|
|
1456
|
-
healthMonitor;
|
|
1457
|
-
cache = new Map;
|
|
1458
|
-
metrics = new Map;
|
|
1459
|
-
constructor(config2) {
|
|
1460
|
-
this.config = config2;
|
|
1461
|
-
this.healthMonitor = new ServiceHealthMonitor(config2);
|
|
1462
|
-
}
|
|
1463
|
-
async handleFailure(serviceName, error, request, originalHandler) {
|
|
1464
|
-
if (!this.config.enabled) {
|
|
1465
|
-
throw error;
|
|
1466
|
-
}
|
|
1467
|
-
const strategy = this.config.fallbackStrategies[serviceName];
|
|
1468
|
-
if (!strategy) {
|
|
1469
|
-
throw error;
|
|
1470
|
-
}
|
|
1471
|
-
const context = {
|
|
1472
|
-
request,
|
|
1473
|
-
serviceName,
|
|
1474
|
-
error,
|
|
1475
|
-
attemptCount: 1,
|
|
1476
|
-
startTime: new Date
|
|
1477
|
-
};
|
|
1478
|
-
return this.executeFallbackStrategy(strategy, context, originalHandler);
|
|
1479
|
-
}
|
|
1480
|
-
async executeFallbackStrategy(strategy, context, originalHandler) {
|
|
1481
|
-
if (context.attemptCount <= strategy.retries) {
|
|
1482
|
-
try {
|
|
1483
|
-
if (context.attemptCount > 1) {
|
|
1484
|
-
const delay = this.calculateBackoffDelay(strategy.backoff, context.attemptCount);
|
|
1485
|
-
await this.sleep(delay);
|
|
1486
|
-
}
|
|
1487
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
1488
|
-
setTimeout(() => reject(new Error("Timeout")), strategy.timeout);
|
|
1489
|
-
});
|
|
1490
|
-
const result = await Promise.race([originalHandler(), timeoutPromise]);
|
|
1491
|
-
this.updateMetrics(context.serviceName, true, Date.now() - context.startTime.getTime());
|
|
1492
|
-
return result;
|
|
1493
|
-
} catch (retryError) {
|
|
1494
|
-
console.error(retryError);
|
|
1495
|
-
context.attemptCount++;
|
|
1496
|
-
if (context.attemptCount <= strategy.retries) {
|
|
1497
|
-
return this.executeFallbackStrategy(strategy, context, originalHandler);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
this.updateMetrics(context.serviceName, false, Date.now() - context.startTime.getTime());
|
|
1502
|
-
switch (strategy.type) {
|
|
1503
|
-
case "cache":
|
|
1504
|
-
return this.handleCacheFallback(strategy, context);
|
|
1505
|
-
case "static":
|
|
1506
|
-
return this.handleStaticFallback(strategy, context);
|
|
1507
|
-
case "simplified":
|
|
1508
|
-
return this.handleSimplifiedFallback(strategy, context);
|
|
1509
|
-
case "redirect":
|
|
1510
|
-
return this.handleRedirectFallback(strategy, context);
|
|
1511
|
-
case "custom":
|
|
1512
|
-
return this.handleCustomFallback(strategy, context);
|
|
1513
|
-
default:
|
|
1514
|
-
throw context.error;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
async handleCacheFallback(strategy, context) {
|
|
1518
|
-
if (!strategy.cacheConfig) {
|
|
1519
|
-
throw context.error;
|
|
1520
|
-
}
|
|
1521
|
-
const cacheKey = this.generateCacheKey(strategy.cacheConfig.key, context);
|
|
1522
|
-
const cached = this.cache.get(cacheKey);
|
|
1523
|
-
if (cached && (cached.expires > new Date || strategy.cacheConfig.staleWhileRevalidate)) {
|
|
1524
|
-
const headers = new Headers({
|
|
1525
|
-
"Content-Type": "application/json",
|
|
1526
|
-
"X-Cache": "HIT",
|
|
1527
|
-
"X-Degraded": "true",
|
|
1528
|
-
"X-Service-Status": this.healthMonitor.getServiceHealth(context.serviceName)?.status || "unknown"
|
|
1529
|
-
});
|
|
1530
|
-
if (strategy.cacheConfig.staleWhileRevalidate && cached.expires <= new Date) {
|
|
1531
|
-
headers.set("X-Cache", "STALE");
|
|
1532
|
-
}
|
|
1533
|
-
return new Response(JSON.stringify(cached.data), {
|
|
1534
|
-
status: 200,
|
|
1535
|
-
headers
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
throw context.error;
|
|
1539
|
-
}
|
|
1540
|
-
async handleStaticFallback(strategy, context) {
|
|
1541
|
-
if (!strategy.staticResponse) {
|
|
1542
|
-
throw context.error;
|
|
1543
|
-
}
|
|
1544
|
-
const headers = new Headers(strategy.staticResponse.headers || {});
|
|
1545
|
-
headers.set("X-Degraded", "true");
|
|
1546
|
-
headers.set("X-Fallback-Type", "static");
|
|
1547
|
-
headers.set("X-Service-Status", this.healthMonitor.getServiceHealth(context.serviceName)?.status || "unknown");
|
|
1548
|
-
return new Response(typeof strategy.staticResponse.body === "string" ? strategy.staticResponse.body : JSON.stringify(strategy.staticResponse.body), {
|
|
1549
|
-
status: strategy.staticResponse.status,
|
|
1550
|
-
headers
|
|
1551
|
-
});
|
|
1552
|
-
}
|
|
1553
|
-
async handleSimplifiedFallback(strategy, context) {
|
|
1554
|
-
const simplifiedData = {
|
|
1555
|
-
message: "Service temporarily unavailable. Showing simplified view.",
|
|
1556
|
-
status: "degraded",
|
|
1557
|
-
timestamp: new Date().toISOString(),
|
|
1558
|
-
requestId: context.request.requestId
|
|
1559
|
-
};
|
|
1560
|
-
return new Response(JSON.stringify(simplifiedData), {
|
|
1561
|
-
status: 200,
|
|
1562
|
-
headers: {
|
|
1563
|
-
"Content-Type": "application/json",
|
|
1564
|
-
"X-Degraded": "true",
|
|
1565
|
-
"X-Fallback-Type": "simplified",
|
|
1566
|
-
"X-Service-Status": this.healthMonitor.getServiceHealth(context.serviceName)?.status || "unknown"
|
|
1567
|
-
}
|
|
1568
|
-
});
|
|
1569
|
-
}
|
|
1570
|
-
async handleRedirectFallback(strategy, context) {
|
|
1571
|
-
if (!strategy.redirectConfig) {
|
|
1572
|
-
throw context.error;
|
|
1573
|
-
}
|
|
1574
|
-
return new Response(null, {
|
|
1575
|
-
status: strategy.redirectConfig.permanent ? 301 : 302,
|
|
1576
|
-
headers: {
|
|
1577
|
-
Location: strategy.redirectConfig.url,
|
|
1578
|
-
"X-Degraded": "true",
|
|
1579
|
-
"X-Fallback-Type": "redirect"
|
|
1580
|
-
}
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
async handleCustomFallback(strategy, context) {
|
|
1584
|
-
if (!strategy.fallbackHandler) {
|
|
1585
|
-
throw context.error;
|
|
1586
|
-
}
|
|
1587
|
-
try {
|
|
1588
|
-
const response = await strategy.fallbackHandler(context.error, context.error.context);
|
|
1589
|
-
response.headers.set("X-Degraded", "true");
|
|
1590
|
-
response.headers.set("X-Fallback-Type", "custom");
|
|
1591
|
-
response.headers.set("X-Service-Status", this.healthMonitor.getServiceHealth(context.serviceName)?.status || "unknown");
|
|
1592
|
-
return response;
|
|
1593
|
-
} catch (fallbackError) {
|
|
1594
|
-
console.error(fallbackError);
|
|
1595
|
-
throw context.error;
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
cacheResponse(serviceName, key, data, ttl) {
|
|
1599
|
-
const strategy = this.config.fallbackStrategies[serviceName];
|
|
1600
|
-
if (strategy?.type === "cache" && strategy.cacheConfig) {
|
|
1601
|
-
const cacheKey = this.generateCacheKey(key, { serviceName });
|
|
1602
|
-
this.cache.set(cacheKey, {
|
|
1603
|
-
data,
|
|
1604
|
-
expires: new Date(Date.now() + ttl * 1000)
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
getServiceMetrics(serviceName) {
|
|
1609
|
-
const metrics = this.metrics.get(serviceName);
|
|
1610
|
-
if (!metrics || metrics.requests === 0) {
|
|
1611
|
-
return { errorRate: 0, avgResponseTime: 0, totalRequests: 0 };
|
|
1612
|
-
}
|
|
1613
|
-
return {
|
|
1614
|
-
errorRate: metrics.errors / metrics.requests * 100,
|
|
1615
|
-
avgResponseTime: metrics.totalTime / metrics.requests,
|
|
1616
|
-
totalRequests: metrics.requests
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
getSystemHealth() {
|
|
1620
|
-
const services = this.healthMonitor.getAllServiceHealth();
|
|
1621
|
-
const unhealthyCount = services.filter((s) => s.status === "unhealthy").length;
|
|
1622
|
-
const degradedCount = services.filter((s) => s.status === "degraded").length;
|
|
1623
|
-
let status = "healthy";
|
|
1624
|
-
if (unhealthyCount > 0) {
|
|
1625
|
-
status = unhealthyCount > services.length / 2 ? "unhealthy" : "degraded";
|
|
1626
|
-
} else if (degradedCount > 0) {
|
|
1627
|
-
status = "degraded";
|
|
1628
|
-
}
|
|
1629
|
-
return { status, services };
|
|
1630
|
-
}
|
|
1631
|
-
calculateBackoffDelay(backoff, attempt) {
|
|
1632
|
-
if (backoff.type === "fixed") {
|
|
1633
|
-
return backoff.delay;
|
|
1634
|
-
}
|
|
1635
|
-
const delay = backoff.delay * 2 ** (attempt - 1);
|
|
1636
|
-
return Math.min(delay, backoff.maxDelay || 30000);
|
|
1637
|
-
}
|
|
1638
|
-
generateCacheKey(template, context) {
|
|
1639
|
-
return template.replace("{service}", context.serviceName).replace("{url}", context.request.url).replace("{method}", context.request.method).replace("{userId}", String(context.request.user?.id || "anonymous"));
|
|
1640
|
-
}
|
|
1641
|
-
updateMetrics(serviceName, success, responseTime) {
|
|
1642
|
-
const current = this.metrics.get(serviceName) || { requests: 0, errors: 0, totalTime: 0 };
|
|
1643
|
-
current.requests++;
|
|
1644
|
-
current.totalTime += responseTime;
|
|
1645
|
-
if (!success) {
|
|
1646
|
-
current.errors++;
|
|
1647
|
-
}
|
|
1648
|
-
this.metrics.set(serviceName, current);
|
|
1649
|
-
}
|
|
1650
|
-
sleep(ms) {
|
|
1651
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1652
|
-
}
|
|
1653
|
-
stop() {
|
|
1654
|
-
this.healthMonitor.stop();
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
function createGracefulDegradationMiddleware(config2) {
|
|
1658
|
-
const manager = new GracefulDegradationManager(config2);
|
|
1659
|
-
return async (req, next) => {
|
|
1660
|
-
try {
|
|
1661
|
-
const response = await next();
|
|
1662
|
-
const serviceName = req.context?.serviceName;
|
|
1663
|
-
if (serviceName && response.ok) {
|
|
1664
|
-
const strategy = config2.fallbackStrategies[serviceName];
|
|
1665
|
-
if (strategy?.type === "cache" && strategy.cacheConfig) {
|
|
1666
|
-
try {
|
|
1667
|
-
const data = await response.clone().json();
|
|
1668
|
-
manager.cacheResponse(serviceName, strategy.cacheConfig.key, data, strategy.cacheConfig.ttl);
|
|
1669
|
-
} catch {}
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
return response;
|
|
1673
|
-
} catch (error) {
|
|
1674
|
-
const serviceName = req.context?.serviceName;
|
|
1675
|
-
if (serviceName && error instanceof Error) {
|
|
1676
|
-
const routerError = error;
|
|
1677
|
-
try {
|
|
1678
|
-
return await manager.handleFailure(serviceName, routerError, req, next);
|
|
1679
|
-
} catch (fallbackError) {
|
|
1680
|
-
console.error(fallbackError);
|
|
1681
|
-
throw error;
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
throw error;
|
|
1685
|
-
}
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
var DegradationStrategies = {
|
|
1689
|
-
cacheFirst: (ttl = 3600, staleWhileRevalidate = true) => ({
|
|
1690
|
-
type: "cache",
|
|
1691
|
-
priority: 1,
|
|
1692
|
-
timeout: 5000,
|
|
1693
|
-
retries: 2,
|
|
1694
|
-
backoff: { type: "exponential", delay: 1000, maxDelay: 5000 },
|
|
1695
|
-
cacheConfig: {
|
|
1696
|
-
key: "{service}:{url}:{method}",
|
|
1697
|
-
ttl,
|
|
1698
|
-
staleWhileRevalidate
|
|
1699
|
-
}
|
|
1700
|
-
}),
|
|
1701
|
-
staticFallback: (body, status = 200) => ({
|
|
1702
|
-
type: "static",
|
|
1703
|
-
priority: 2,
|
|
1704
|
-
timeout: 1000,
|
|
1705
|
-
retries: 0,
|
|
1706
|
-
backoff: { type: "fixed", delay: 0 },
|
|
1707
|
-
staticResponse: {
|
|
1708
|
-
status,
|
|
1709
|
-
body,
|
|
1710
|
-
headers: { "Content-Type": "application/json" }
|
|
1711
|
-
}
|
|
1712
|
-
}),
|
|
1713
|
-
simplifiedView: () => ({
|
|
1714
|
-
type: "simplified",
|
|
1715
|
-
priority: 3,
|
|
1716
|
-
timeout: 1000,
|
|
1717
|
-
retries: 0,
|
|
1718
|
-
backoff: { type: "fixed", delay: 0 }
|
|
1719
|
-
}),
|
|
1720
|
-
redirectToMaintenance: (url) => ({
|
|
1721
|
-
type: "redirect",
|
|
1722
|
-
priority: 4,
|
|
1723
|
-
timeout: 1000,
|
|
1724
|
-
retries: 0,
|
|
1725
|
-
backoff: { type: "fixed", delay: 0 },
|
|
1726
|
-
redirectConfig: {
|
|
1727
|
-
url,
|
|
1728
|
-
permanent: false
|
|
1729
|
-
}
|
|
1730
|
-
})
|
|
1731
|
-
};
|
|
1732
|
-
|
|
1733
|
-
// src/errors/error-handler.ts
|
|
1734
|
-
class AdvancedErrorHandler {
|
|
1735
|
-
errorReporting;
|
|
1736
|
-
gracefulDegradation;
|
|
1737
|
-
circuitBreakerRegistry;
|
|
1738
|
-
config;
|
|
1739
|
-
constructor(config2 = {}) {
|
|
1740
|
-
this.config = {
|
|
1741
|
-
development: false,
|
|
1742
|
-
includeStackTrace: false,
|
|
1743
|
-
sanitizeErrors: true,
|
|
1744
|
-
logErrors: true,
|
|
1745
|
-
...config2
|
|
1746
|
-
};
|
|
1747
|
-
this.initializeComponents();
|
|
1748
|
-
}
|
|
1749
|
-
initializeComponents() {
|
|
1750
|
-
if (this.config.errorReporting?.length) {
|
|
1751
|
-
this.errorReporting = new ErrorReportingManager(this.config.errorReporting);
|
|
1752
|
-
}
|
|
1753
|
-
if (this.config.gracefulDegradation?.enabled) {
|
|
1754
|
-
this.gracefulDegradation = new GracefulDegradationManager(this.config.gracefulDegradation);
|
|
1755
|
-
}
|
|
1756
|
-
if (this.config.circuitBreakers?.length) {
|
|
1757
|
-
this.circuitBreakerRegistry = new CircuitBreakerRegistry;
|
|
1758
|
-
for (const config2 of this.config.circuitBreakers) {
|
|
1759
|
-
this.circuitBreakerRegistry.register(config2);
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
middleware() {
|
|
1764
|
-
return async (req, next) => {
|
|
1765
|
-
try {
|
|
1766
|
-
const errorContext = {
|
|
1767
|
-
requestId: req.requestId,
|
|
1768
|
-
userId: req.user?.id,
|
|
1769
|
-
traceId: req.traceId,
|
|
1770
|
-
spanId: req.spanId,
|
|
1771
|
-
route: req.params?.route || req.url,
|
|
1772
|
-
method: req.method,
|
|
1773
|
-
url: req.url,
|
|
1774
|
-
userAgent: req.userAgent?.() || undefined,
|
|
1775
|
-
ip: req.ip?.() || undefined,
|
|
1776
|
-
timestamp: new Date
|
|
1777
|
-
};
|
|
1778
|
-
req.context = { ...req.context, errorContext };
|
|
1779
|
-
if (this.circuitBreakerRegistry && req.context?.serviceName) {
|
|
1780
|
-
const breaker = this.circuitBreakerRegistry.get(req.context.serviceName);
|
|
1781
|
-
if (breaker) {
|
|
1782
|
-
return await breaker.execute({
|
|
1783
|
-
execute: next,
|
|
1784
|
-
context: errorContext
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
return await next();
|
|
1789
|
-
} catch (error) {
|
|
1790
|
-
return this.handleError(error, req);
|
|
1791
|
-
}
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
async handleError(error, req) {
|
|
1795
|
-
const routerError = this.normalizeError(error, req);
|
|
1796
|
-
if (this.errorReporting) {
|
|
1797
|
-
this.errorReporting.addBreadcrumb(`Error in ${req.method} ${req.url}`, "error", {
|
|
1798
|
-
code: routerError.code,
|
|
1799
|
-
severity: routerError.severity,
|
|
1800
|
-
statusCode: routerError.statusCode
|
|
1801
|
-
});
|
|
1802
|
-
}
|
|
1803
|
-
if (this.config.logErrors) {
|
|
1804
|
-
this.logError(routerError, req);
|
|
1805
|
-
}
|
|
1806
|
-
if (this.gracefulDegradation && req.context?.serviceName) {
|
|
1807
|
-
try {
|
|
1808
|
-
return await this.gracefulDegradation.handleFailure(req.context.serviceName, routerError, req, async () => {
|
|
1809
|
-
throw routerError;
|
|
1810
|
-
});
|
|
1811
|
-
} catch {}
|
|
1812
|
-
}
|
|
1813
|
-
if (this.errorReporting) {
|
|
1814
|
-
try {
|
|
1815
|
-
await this.errorReporting.report(routerError, routerError.context);
|
|
1816
|
-
} catch (reportingError) {
|
|
1817
|
-
console.error("Failed to report error:", reportingError);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
if (this.config.globalErrorHandler) {
|
|
1821
|
-
try {
|
|
1822
|
-
return await this.config.globalErrorHandler(routerError, req);
|
|
1823
|
-
} catch (handlerError) {
|
|
1824
|
-
console.error("Global error handler failed:", handlerError);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
const customPage = this.config.customErrorPages?.[routerError.statusCode];
|
|
1828
|
-
if (customPage) {
|
|
1829
|
-
try {
|
|
1830
|
-
if (typeof customPage === "string") {
|
|
1831
|
-
return new Response(customPage, {
|
|
1832
|
-
status: routerError.statusCode,
|
|
1833
|
-
headers: { "Content-Type": "text/html" }
|
|
1834
|
-
});
|
|
1835
|
-
} else {
|
|
1836
|
-
return await customPage(routerError, req);
|
|
1837
|
-
}
|
|
1838
|
-
} catch (pageError) {
|
|
1839
|
-
console.error("Custom error page failed:", pageError);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
return this.createErrorResponse(routerError, req);
|
|
1843
|
-
}
|
|
1844
|
-
normalizeError(error, req) {
|
|
1845
|
-
const errorContext = {
|
|
1846
|
-
requestId: req.requestId,
|
|
1847
|
-
userId: req.user?.id,
|
|
1848
|
-
traceId: req.traceId,
|
|
1849
|
-
spanId: req.spanId,
|
|
1850
|
-
route: req.params?.route || req.url,
|
|
1851
|
-
method: req.method,
|
|
1852
|
-
url: req.url,
|
|
1853
|
-
userAgent: req.userAgent?.() || undefined,
|
|
1854
|
-
ip: req.ip?.() || undefined,
|
|
1855
|
-
timestamp: new Date,
|
|
1856
|
-
metadata: req.context
|
|
213
|
+
keyName: "X-API-Key",
|
|
214
|
+
keyPrefix: "bun_",
|
|
215
|
+
...options
|
|
1857
216
|
};
|
|
1858
|
-
if (error instanceof RouterException) {
|
|
1859
|
-
return error;
|
|
1860
|
-
}
|
|
1861
|
-
if (error instanceof Error) {
|
|
1862
|
-
if (error.name === "ValidationError") {
|
|
1863
|
-
return ErrorFactory.validation(error.message, {}, errorContext);
|
|
1864
|
-
}
|
|
1865
|
-
if (error.name === "UnauthorizedError" || error.message.includes("unauthorized")) {
|
|
1866
|
-
return ErrorFactory.authentication(error.message, errorContext);
|
|
1867
|
-
}
|
|
1868
|
-
if (error.name === "ForbiddenError" || error.message.includes("forbidden")) {
|
|
1869
|
-
return ErrorFactory.authorization(error.message, [], errorContext);
|
|
1870
|
-
}
|
|
1871
|
-
if (error.name === "NotFoundError" || error.message.includes("not found")) {
|
|
1872
|
-
return ErrorFactory.notFound(undefined, undefined, errorContext);
|
|
1873
|
-
}
|
|
1874
|
-
if (error.name === "TimeoutError" || error.message.includes("timeout")) {
|
|
1875
|
-
return ErrorFactory.timeout(30000, error.message, errorContext);
|
|
1876
|
-
}
|
|
1877
|
-
return ErrorFactory.internalServer(error.message, error, errorContext);
|
|
1878
|
-
}
|
|
1879
|
-
if (typeof error === "string") {
|
|
1880
|
-
return ErrorFactory.internalServer(error, undefined, errorContext);
|
|
1881
|
-
}
|
|
1882
|
-
return ErrorFactory.internalServer("An unknown error occurred", undefined, errorContext);
|
|
1883
217
|
}
|
|
1884
|
-
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
message = "An internal server error occurred";
|
|
1891
|
-
}
|
|
1892
|
-
const errorResponse = {
|
|
1893
|
-
error: {
|
|
1894
|
-
code: error.code,
|
|
1895
|
-
message,
|
|
1896
|
-
statusCode: error.statusCode,
|
|
1897
|
-
timestamp: error.timestamp.toISOString(),
|
|
1898
|
-
requestId: error.context.requestId,
|
|
1899
|
-
traceId: error.context.traceId
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
if (isDevelopment) {
|
|
1903
|
-
errorResponse.error.category = error.category;
|
|
1904
|
-
errorResponse.error.severity = error.severity;
|
|
1905
|
-
errorResponse.error.retryable = error.retryable;
|
|
1906
|
-
errorResponse.error.context = error.context;
|
|
1907
|
-
}
|
|
1908
|
-
if (includeStackTrace) {
|
|
1909
|
-
errorResponse.error.stack = error.stack;
|
|
1910
|
-
if (error.cause) {
|
|
1911
|
-
errorResponse.error.cause = {
|
|
1912
|
-
name: error.cause.name,
|
|
1913
|
-
message: error.cause.message,
|
|
1914
|
-
stack: error.cause.stack
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
if ("fields" in error) {
|
|
1919
|
-
errorResponse.error.fields = error.fields;
|
|
1920
|
-
}
|
|
1921
|
-
if ("allowedMethods" in error) {
|
|
1922
|
-
errorResponse.error.allowedMethods = error.allowedMethods;
|
|
1923
|
-
}
|
|
1924
|
-
if ("retryAfter" in error) {
|
|
1925
|
-
errorResponse.error.retryAfter = error.retryAfter;
|
|
1926
|
-
}
|
|
1927
|
-
const headers = new Headers({
|
|
1928
|
-
"Content-Type": "application/json",
|
|
1929
|
-
"X-Error-Code": error.code,
|
|
1930
|
-
"X-Request-ID": error.context.requestId || "",
|
|
1931
|
-
"X-Trace-ID": error.context.traceId || ""
|
|
218
|
+
generateKey(owner, scopes = [], expiresIn) {
|
|
219
|
+
const key = `${this.options.keyPrefix}${this.randomString(32)}`;
|
|
220
|
+
this.keys.set(key, {
|
|
221
|
+
owner,
|
|
222
|
+
scopes,
|
|
223
|
+
expiresAt: expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined
|
|
1932
224
|
});
|
|
1933
|
-
|
|
1934
|
-
|
|
225
|
+
return key;
|
|
226
|
+
}
|
|
227
|
+
validateKey(key, requiredScopes = []) {
|
|
228
|
+
const keyInfo = this.keys.get(key);
|
|
229
|
+
if (!keyInfo) {
|
|
230
|
+
return false;
|
|
1935
231
|
}
|
|
1936
|
-
if (
|
|
1937
|
-
|
|
232
|
+
if (keyInfo.expiresAt && keyInfo.expiresAt < new Date) {
|
|
233
|
+
return false;
|
|
1938
234
|
}
|
|
1939
|
-
if (
|
|
1940
|
-
|
|
1941
|
-
headers.set("X-RateLimit-Remaining", error.remaining.toString());
|
|
235
|
+
if (requiredScopes.length > 0) {
|
|
236
|
+
return requiredScopes.every((scope) => keyInfo.scopes.includes(scope));
|
|
1942
237
|
}
|
|
1943
|
-
return
|
|
1944
|
-
status: error.statusCode,
|
|
1945
|
-
headers
|
|
1946
|
-
});
|
|
238
|
+
return true;
|
|
1947
239
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
error: error.toJSON(),
|
|
1951
|
-
request: {
|
|
1952
|
-
method: req.method,
|
|
1953
|
-
url: req.url,
|
|
1954
|
-
headers: Object.fromEntries(req.headers.entries()),
|
|
1955
|
-
userAgent: req.userAgent?.() || undefined,
|
|
1956
|
-
ip: req.ip?.() || undefined,
|
|
1957
|
-
requestId: req.requestId,
|
|
1958
|
-
traceId: req.traceId
|
|
1959
|
-
}
|
|
1960
|
-
};
|
|
1961
|
-
switch (error.severity) {
|
|
1962
|
-
case "critical":
|
|
1963
|
-
console.error("[CRITICAL ERROR]", logData);
|
|
1964
|
-
break;
|
|
1965
|
-
case "high":
|
|
1966
|
-
console.error("[ERROR]", logData);
|
|
1967
|
-
break;
|
|
1968
|
-
case "medium":
|
|
1969
|
-
console.warn("[WARNING]", logData);
|
|
1970
|
-
break;
|
|
1971
|
-
case "low":
|
|
1972
|
-
console.warn("[INFO]", logData);
|
|
1973
|
-
break;
|
|
1974
|
-
default:
|
|
1975
|
-
console.error("[ERROR]", logData);
|
|
1976
|
-
}
|
|
240
|
+
getKeyInfo(key) {
|
|
241
|
+
return this.keys.get(key) || null;
|
|
1977
242
|
}
|
|
1978
|
-
|
|
1979
|
-
return
|
|
1980
|
-
errorReporting: this.errorReporting ? {
|
|
1981
|
-
breadcrumbs: this.errorReporting.getBreadcrumbs().length
|
|
1982
|
-
} : undefined,
|
|
1983
|
-
gracefulDegradation: this.gracefulDegradation ? this.gracefulDegradation.getSystemHealth() : undefined,
|
|
1984
|
-
circuitBreakers: this.circuitBreakerRegistry ? this.circuitBreakerRegistry.getAllMetrics() : undefined
|
|
1985
|
-
};
|
|
243
|
+
revokeKey(key) {
|
|
244
|
+
return this.keys.delete(key);
|
|
1986
245
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
246
|
+
extractFromRequest(req) {
|
|
247
|
+
const { source, keyName } = this.options;
|
|
248
|
+
switch (source) {
|
|
249
|
+
case "header":
|
|
250
|
+
return req.headers.get(keyName || "X-API-Key") || null;
|
|
251
|
+
case "query":
|
|
252
|
+
return new URL(req.url).searchParams.get(keyName || "api_key") || null;
|
|
253
|
+
case "cookie":
|
|
254
|
+
if (req.cookies) {
|
|
255
|
+
const key = keyName || "api_key";
|
|
256
|
+
if (typeof req.cookies.get === "function") {
|
|
257
|
+
return req.cookies.get(key) || null;
|
|
258
|
+
}
|
|
259
|
+
return req.cookies[key] || null;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
default:
|
|
263
|
+
return null;
|
|
1993
264
|
}
|
|
1994
265
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
getStats: () => handler.getStats(),
|
|
2001
|
-
cleanup: () => handler.cleanup()
|
|
2002
|
-
};
|
|
2003
|
-
}
|
|
2004
|
-
// src/request/context.ts
|
|
2005
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
2006
|
-
var storage = new AsyncLocalStorage;
|
|
2007
|
-
var contextEnabled = false;
|
|
2008
|
-
var syncCurrent = null;
|
|
2009
|
-
function enableRequestContext() {
|
|
2010
|
-
contextEnabled = true;
|
|
2011
|
-
}
|
|
2012
|
-
function runWithRequest(initial, fn) {
|
|
2013
|
-
if (!contextEnabled) {
|
|
2014
|
-
syncCurrent = initial;
|
|
2015
|
-
const result = fn();
|
|
2016
|
-
if (result instanceof Promise) {
|
|
2017
|
-
return result.finally(() => {
|
|
2018
|
-
syncCurrent = null;
|
|
2019
|
-
});
|
|
266
|
+
randomString(length) {
|
|
267
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
268
|
+
let result = "";
|
|
269
|
+
for (let i = 0;i < length; i++) {
|
|
270
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
2020
271
|
}
|
|
2021
|
-
syncCurrent = null;
|
|
2022
272
|
return result;
|
|
2023
273
|
}
|
|
2024
|
-
return storage.run({ current: initial }, fn);
|
|
2025
274
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
275
|
+
|
|
276
|
+
class OAuth2Helper {
|
|
277
|
+
config;
|
|
278
|
+
constructor(config2) {
|
|
279
|
+
this.config = config2;
|
|
2031
280
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
281
|
+
getAuthorizationUrl(additionalParams = {}) {
|
|
282
|
+
const params = new URLSearchParams({
|
|
283
|
+
client_id: this.config.clientId,
|
|
284
|
+
redirect_uri: this.config.redirectUri,
|
|
285
|
+
response_type: "code",
|
|
286
|
+
...additionalParams
|
|
287
|
+
});
|
|
288
|
+
if (this.config.scope) {
|
|
289
|
+
params.set("scope", this.config.scope);
|
|
290
|
+
}
|
|
291
|
+
if (this.config.state) {
|
|
292
|
+
params.set("state", this.config.state);
|
|
293
|
+
}
|
|
294
|
+
return `${this.config.authorizeUrl}?${params.toString()}`;
|
|
2034
295
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
296
|
+
async exchangeCodeForToken(code) {
|
|
297
|
+
const params = new URLSearchParams({
|
|
298
|
+
grant_type: "authorization_code",
|
|
299
|
+
code,
|
|
300
|
+
redirect_uri: this.config.redirectUri,
|
|
301
|
+
client_id: this.config.clientId,
|
|
302
|
+
client_secret: this.config.clientSecret
|
|
303
|
+
});
|
|
304
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: {
|
|
307
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
308
|
+
},
|
|
309
|
+
body: params.toString()
|
|
310
|
+
});
|
|
311
|
+
if (!response.ok) {
|
|
312
|
+
throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`);
|
|
313
|
+
}
|
|
314
|
+
return await response.json();
|
|
2040
315
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
316
|
+
async refreshToken(refreshToken) {
|
|
317
|
+
const params = new URLSearchParams({
|
|
318
|
+
grant_type: "refresh_token",
|
|
319
|
+
refresh_token: refreshToken,
|
|
320
|
+
client_id: this.config.clientId,
|
|
321
|
+
client_secret: this.config.clientSecret
|
|
322
|
+
});
|
|
323
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: {
|
|
326
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
327
|
+
},
|
|
328
|
+
body: params.toString()
|
|
329
|
+
});
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
throw new Error(`OAuth token refresh failed: ${response.status} ${response.statusText}`);
|
|
332
|
+
}
|
|
333
|
+
return await response.json();
|
|
2047
334
|
}
|
|
2048
|
-
return req;
|
|
2049
335
|
}
|
|
2050
336
|
// src/response/response-factory.ts
|
|
2051
337
|
var response = {
|
|
@@ -2143,8 +429,8 @@ var response = {
|
|
|
2143
429
|
redirectTemporary: (url) => {
|
|
2144
430
|
return response.redirect(url, 302);
|
|
2145
431
|
},
|
|
2146
|
-
back: (
|
|
2147
|
-
const referer =
|
|
432
|
+
back: (request, fallback = "/") => {
|
|
433
|
+
const referer = request.headers.get("referer") || fallback;
|
|
2148
434
|
return response.redirect(referer);
|
|
2149
435
|
},
|
|
2150
436
|
view: (html, status = 200, headers = {}) => {
|
|
@@ -2389,7 +675,7 @@ class MiddlewarePipeline {
|
|
|
2389
675
|
}
|
|
2390
676
|
this.compiledPipelines.set(routeKey, compiled);
|
|
2391
677
|
}
|
|
2392
|
-
async execute(routeKey,
|
|
678
|
+
async execute(routeKey, request, finalHandler) {
|
|
2393
679
|
const startTime = performance.now();
|
|
2394
680
|
this.stats.totalExecutions++;
|
|
2395
681
|
const pipeline = this.compiledPipelines.get(routeKey);
|
|
@@ -2399,7 +685,7 @@ class MiddlewarePipeline {
|
|
|
2399
685
|
}
|
|
2400
686
|
this.stats.cacheHits++;
|
|
2401
687
|
const context = {
|
|
2402
|
-
request
|
|
688
|
+
request,
|
|
2403
689
|
dependencies: new Map,
|
|
2404
690
|
metadata: {}
|
|
2405
691
|
};
|
|
@@ -2414,7 +700,7 @@ class MiddlewarePipeline {
|
|
|
2414
700
|
this.stats.skippedMiddleware++;
|
|
2415
701
|
return await next();
|
|
2416
702
|
}
|
|
2417
|
-
const result2 = await middleware2.handler(
|
|
703
|
+
const result2 = await middleware2.handler(request, next);
|
|
2418
704
|
if (result2 instanceof Response && middleware2.canShortCircuit) {
|
|
2419
705
|
this.stats.shortCircuits++;
|
|
2420
706
|
return result2;
|
|
@@ -3492,12 +1778,12 @@ class StreamHandler {
|
|
|
3492
1778
|
}
|
|
3493
1779
|
}
|
|
3494
1780
|
class FileStreamHandler {
|
|
3495
|
-
static async streamFile(filePath,
|
|
1781
|
+
static async streamFile(filePath, request, options = {}) {
|
|
3496
1782
|
const { contentType, enableRanges = true, chunkSize: _chunkSize = 64 * 1024 } = options;
|
|
3497
1783
|
try {
|
|
3498
1784
|
const file = Bun.file(filePath);
|
|
3499
1785
|
const fileSize = file.size;
|
|
3500
|
-
const range = enableRanges ?
|
|
1786
|
+
const range = enableRanges ? request.headers.get("range") : null;
|
|
3501
1787
|
if (range && enableRanges) {
|
|
3502
1788
|
return FileStreamHandler.handleRangeRequest(file, range, contentType);
|
|
3503
1789
|
}
|
|
@@ -3576,8 +1862,8 @@ function closeReadableStreamAfterError(controller, error) {
|
|
|
3576
1862
|
function registerFileStreaming(RouterClass) {
|
|
3577
1863
|
Object.defineProperties(RouterClass.prototype, {
|
|
3578
1864
|
streamFile: {
|
|
3579
|
-
value(filePath,
|
|
3580
|
-
return FileStreamHandler.streamFile(filePath,
|
|
1865
|
+
value(filePath, request, options) {
|
|
1866
|
+
return FileStreamHandler.streamFile(filePath, request, options);
|
|
3581
1867
|
},
|
|
3582
1868
|
writable: true,
|
|
3583
1869
|
configurable: true
|
|
@@ -4932,7 +3218,6 @@ var SubdomainPatterns = {
|
|
|
4932
3218
|
};
|
|
4933
3219
|
|
|
4934
3220
|
// src/url.ts
|
|
4935
|
-
import process3 from "process";
|
|
4936
3221
|
var namedRouteRegistry = new Map;
|
|
4937
3222
|
function registerNamedRoute(name, path) {
|
|
4938
3223
|
namedRouteRegistry.set(name, path);
|
|
@@ -4940,54 +3225,6 @@ function registerNamedRoute(name, path) {
|
|
|
4940
3225
|
function getNamedRoutePath(name) {
|
|
4941
3226
|
return namedRouteRegistry.get(name);
|
|
4942
3227
|
}
|
|
4943
|
-
function getNamedRoutes() {
|
|
4944
|
-
return new Map(namedRouteRegistry);
|
|
4945
|
-
}
|
|
4946
|
-
function clearNamedRoutes() {
|
|
4947
|
-
namedRouteRegistry.clear();
|
|
4948
|
-
}
|
|
4949
|
-
function url(name, params = {}, options = {}) {
|
|
4950
|
-
const path = namedRouteRegistry.get(name);
|
|
4951
|
-
if (!path) {
|
|
4952
|
-
const known = [...namedRouteRegistry.keys()].sort().join(", ") || "(none registered)";
|
|
4953
|
-
throw new Error(`Named route '${name}' is not defined. Known routes: ${known}`);
|
|
4954
|
-
}
|
|
4955
|
-
const consumed = new Set;
|
|
4956
|
-
let resolved = path;
|
|
4957
|
-
for (const [key, value] of Object.entries(params)) {
|
|
4958
|
-
const required = `{${key}}`;
|
|
4959
|
-
const optional = `{${key}?}`;
|
|
4960
|
-
const encoded = encodeURIComponent(String(value));
|
|
4961
|
-
if (resolved.includes(required)) {
|
|
4962
|
-
resolved = resolved.replaceAll(required, encoded);
|
|
4963
|
-
consumed.add(key);
|
|
4964
|
-
} else if (resolved.includes(optional)) {
|
|
4965
|
-
resolved = resolved.replaceAll(optional, encoded);
|
|
4966
|
-
consumed.add(key);
|
|
4967
|
-
}
|
|
4968
|
-
}
|
|
4969
|
-
if (resolved.includes("?}")) {
|
|
4970
|
-
resolved = resolved.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
4971
|
-
if (resolved.length > 1 && resolved.endsWith("/"))
|
|
4972
|
-
resolved = resolved.slice(0, -1);
|
|
4973
|
-
}
|
|
4974
|
-
const missing = resolved.match(/\{([^}?]+)\}/g);
|
|
4975
|
-
if (missing && missing.length > 0)
|
|
4976
|
-
throw new Error(`Missing required params for route '${name}': ${missing.join(", ")}`);
|
|
4977
|
-
const leftover = Object.entries(params).filter(([k]) => !consumed.has(k));
|
|
4978
|
-
if (leftover.length > 0) {
|
|
4979
|
-
const qs = new URLSearchParams;
|
|
4980
|
-
for (const [k, v] of leftover)
|
|
4981
|
-
qs.set(k, String(v));
|
|
4982
|
-
resolved += `?${qs.toString()}`;
|
|
4983
|
-
}
|
|
4984
|
-
if (options.absolute) {
|
|
4985
|
-
const base = (options.base ?? process3.env.APP_URL ?? "http://localhost").replace(/\/$/, "");
|
|
4986
|
-
const prefixed = base.startsWith("http") ? base : `https://${base}`;
|
|
4987
|
-
return `${prefixed}${resolved.startsWith("/") ? resolved : `/${resolved}`}`;
|
|
4988
|
-
}
|
|
4989
|
-
return resolved;
|
|
4990
|
-
}
|
|
4991
3228
|
|
|
4992
3229
|
// src/utils.ts
|
|
4993
3230
|
import { join as join3 } from "path";
|
|
@@ -5541,24 +3778,24 @@ class FluentRouter {
|
|
|
5541
3778
|
getRoutes() {
|
|
5542
3779
|
return [...this.routes, ...this.subdomainRouter.getAllRoutes()];
|
|
5543
3780
|
}
|
|
5544
|
-
async handle(
|
|
5545
|
-
const
|
|
5546
|
-
const domain =
|
|
3781
|
+
async handle(request) {
|
|
3782
|
+
const url = new URL(request.url);
|
|
3783
|
+
const domain = url.hostname;
|
|
5547
3784
|
const match = this.subdomainRouter.findDomainGroup(domain);
|
|
5548
3785
|
if (match) {
|
|
5549
|
-
const enhancedReq =
|
|
3786
|
+
const enhancedReq = request;
|
|
5550
3787
|
enhancedReq.domainParams = match.parameters;
|
|
5551
3788
|
for (const route of match.group.getRoutes()) {
|
|
5552
|
-
const params = this.matchRouteParams(route,
|
|
3789
|
+
const params = this.matchRouteParams(route, request.method, url.pathname);
|
|
5553
3790
|
if (params) {
|
|
5554
3791
|
return await this.executeRoute(route, enhancedReq, params);
|
|
5555
3792
|
}
|
|
5556
3793
|
}
|
|
5557
3794
|
}
|
|
5558
3795
|
for (const route of this.routes) {
|
|
5559
|
-
const params = this.matchRouteParams(route,
|
|
3796
|
+
const params = this.matchRouteParams(route, request.method, url.pathname);
|
|
5560
3797
|
if (params) {
|
|
5561
|
-
return await this.executeRoute(route,
|
|
3798
|
+
return await this.executeRoute(route, request, params);
|
|
5562
3799
|
}
|
|
5563
3800
|
}
|
|
5564
3801
|
return null;
|
|
@@ -5570,11 +3807,11 @@ class FluentRouter {
|
|
|
5570
3807
|
const params = {};
|
|
5571
3808
|
return matchPath(route.path, pathname, params) ? params : null;
|
|
5572
3809
|
}
|
|
5573
|
-
async executeRoute(route,
|
|
5574
|
-
|
|
3810
|
+
async executeRoute(route, request, params) {
|
|
3811
|
+
request.params = params;
|
|
5575
3812
|
const allMiddleware = [...this.globalMiddleware];
|
|
5576
3813
|
for (const conditional of this.conditionalMiddleware) {
|
|
5577
|
-
if (await conditional.condition(
|
|
3814
|
+
if (await conditional.condition(request)) {
|
|
5578
3815
|
allMiddleware.push(...conditional.middleware);
|
|
5579
3816
|
}
|
|
5580
3817
|
}
|
|
@@ -5583,10 +3820,10 @@ class FluentRouter {
|
|
|
5583
3820
|
const next = async () => {
|
|
5584
3821
|
if (index < allMiddleware.length) {
|
|
5585
3822
|
const middleware2 = allMiddleware[index++];
|
|
5586
|
-
const result = await middleware2(
|
|
3823
|
+
const result = await middleware2(request, next);
|
|
5587
3824
|
return result ?? new Response(null);
|
|
5588
3825
|
}
|
|
5589
|
-
return await route.handler(
|
|
3826
|
+
return await route.handler(request);
|
|
5590
3827
|
};
|
|
5591
3828
|
return await next();
|
|
5592
3829
|
}
|
|
@@ -5624,27 +3861,27 @@ var RouteFactory = {
|
|
|
5624
3861
|
};
|
|
5625
3862
|
var RouterUtils = {
|
|
5626
3863
|
route: (name, params = {}, query = {}) => {
|
|
5627
|
-
let
|
|
3864
|
+
let url = getNamedRoutePath(name) ?? `/${name}`;
|
|
5628
3865
|
for (const [key, value] of Object.entries(params)) {
|
|
5629
3866
|
const encoded = encodeURIComponent(value);
|
|
5630
|
-
|
|
5631
|
-
|
|
3867
|
+
url = url.replace(`{${key}}`, encoded);
|
|
3868
|
+
url = url.replace(`{${key}?}`, encoded);
|
|
5632
3869
|
}
|
|
5633
|
-
if (
|
|
5634
|
-
|
|
5635
|
-
if (
|
|
5636
|
-
|
|
3870
|
+
if (url.includes("?}")) {
|
|
3871
|
+
url = url.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
3872
|
+
if (url.length > 1 && url.endsWith("/"))
|
|
3873
|
+
url = url.slice(0, -1);
|
|
5637
3874
|
}
|
|
5638
3875
|
const queryString = new URLSearchParams(query).toString();
|
|
5639
3876
|
if (queryString) {
|
|
5640
|
-
|
|
3877
|
+
url += `?${queryString}`;
|
|
5641
3878
|
}
|
|
5642
|
-
return
|
|
3879
|
+
return url;
|
|
5643
3880
|
},
|
|
5644
|
-
redirect: (
|
|
3881
|
+
redirect: (url, status = 302) => {
|
|
5645
3882
|
return new Response(null, {
|
|
5646
3883
|
status,
|
|
5647
|
-
headers: { Location:
|
|
3884
|
+
headers: { Location: url }
|
|
5648
3885
|
});
|
|
5649
3886
|
},
|
|
5650
3887
|
json: (data, status = 200) => {
|
|
@@ -5842,10 +4079,10 @@ function registerHttpMethods(RouterClass) {
|
|
|
5842
4079
|
route.constraints = constraints;
|
|
5843
4080
|
}
|
|
5844
4081
|
route.pattern = {
|
|
5845
|
-
exec: (
|
|
4082
|
+
exec: (url) => {
|
|
5846
4083
|
const params = {};
|
|
5847
4084
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
5848
|
-
const isMatch = matchPath(routePath,
|
|
4085
|
+
const isMatch = matchPath(routePath, url.pathname, params, constraintsRecord);
|
|
5849
4086
|
if (!isMatch) {
|
|
5850
4087
|
return null;
|
|
5851
4088
|
}
|
|
@@ -5972,18 +4209,18 @@ function registerHttpMethods(RouterClass) {
|
|
|
5972
4209
|
if (!route) {
|
|
5973
4210
|
throw new Error(`Route with name "${name}" not found`);
|
|
5974
4211
|
}
|
|
5975
|
-
let
|
|
4212
|
+
let url = route.path;
|
|
5976
4213
|
for (const [param, value] of Object.entries(params)) {
|
|
5977
|
-
|
|
5978
|
-
|
|
4214
|
+
url = url.replace(`{${param}}`, encodeURIComponent(value));
|
|
4215
|
+
url = url.replace(`{${param}?}`, encodeURIComponent(value));
|
|
5979
4216
|
}
|
|
5980
|
-
if (
|
|
5981
|
-
|
|
5982
|
-
if (
|
|
5983
|
-
|
|
4217
|
+
if (url.includes("{")) {
|
|
4218
|
+
url = url.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
4219
|
+
if (url.length > 1 && url.endsWith("/")) {
|
|
4220
|
+
url = url.slice(0, -1);
|
|
5984
4221
|
}
|
|
5985
4222
|
}
|
|
5986
|
-
return
|
|
4223
|
+
return url;
|
|
5987
4224
|
},
|
|
5988
4225
|
writable: true,
|
|
5989
4226
|
configurable: true
|
|
@@ -6001,9 +4238,9 @@ function registerHttpMethods(RouterClass) {
|
|
|
6001
4238
|
function registerRedirectMethods(RouterClass) {
|
|
6002
4239
|
Object.defineProperties(RouterClass.prototype, {
|
|
6003
4240
|
redirect: {
|
|
6004
|
-
value(
|
|
4241
|
+
value(url, status = 302) {
|
|
6005
4242
|
const headers = new Headers;
|
|
6006
|
-
headers.set("Location",
|
|
4243
|
+
headers.set("Location", url);
|
|
6007
4244
|
return new Response(null, {
|
|
6008
4245
|
status,
|
|
6009
4246
|
headers
|
|
@@ -6013,8 +4250,8 @@ function registerRedirectMethods(RouterClass) {
|
|
|
6013
4250
|
configurable: true
|
|
6014
4251
|
},
|
|
6015
4252
|
permanentRedirect: {
|
|
6016
|
-
value(
|
|
6017
|
-
return this.redirect(
|
|
4253
|
+
value(url) {
|
|
4254
|
+
return this.redirect(url, 301);
|
|
6018
4255
|
},
|
|
6019
4256
|
writable: true,
|
|
6020
4257
|
configurable: true
|
|
@@ -7012,8 +5249,8 @@ class RouteCompiler {
|
|
|
7012
5249
|
}
|
|
7013
5250
|
const pattern = this.convertToURLPattern(route.path, route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined);
|
|
7014
5251
|
const urlPatternAdapter = {
|
|
7015
|
-
exec: (
|
|
7016
|
-
const pathname =
|
|
5252
|
+
exec: (url) => {
|
|
5253
|
+
const pathname = url.pathname;
|
|
7017
5254
|
const result = pattern.exec(pathname);
|
|
7018
5255
|
if (!result)
|
|
7019
5256
|
return null;
|
|
@@ -7384,8 +5621,8 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
7384
5621
|
},
|
|
7385
5622
|
fallbackMatchRoute: {
|
|
7386
5623
|
value(path, method, domain) {
|
|
7387
|
-
const
|
|
7388
|
-
const cacheKey = `${domain || ""}:${method}:${
|
|
5624
|
+
const url = new URL(path, "http://localhost");
|
|
5625
|
+
const cacheKey = `${domain || ""}:${method}:${url.pathname}`;
|
|
7389
5626
|
if (this.routeCache.has(cacheKey)) {
|
|
7390
5627
|
return this.routeCache.get(cacheKey);
|
|
7391
5628
|
}
|
|
@@ -7393,7 +5630,7 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
7393
5630
|
this.routeCache.clear();
|
|
7394
5631
|
}
|
|
7395
5632
|
if (this.staticRoutes.has(method)) {
|
|
7396
|
-
const staticRoute = this.staticRoutes.get(method).get(
|
|
5633
|
+
const staticRoute = this.staticRoutes.get(method).get(url.pathname);
|
|
7397
5634
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
7398
5635
|
const result = {
|
|
7399
5636
|
route: staticRoute,
|
|
@@ -7406,7 +5643,7 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
7406
5643
|
const potentialRoutes = domain && this.domains[domain] ? this.domains[domain] : this.routes;
|
|
7407
5644
|
const methodRoutes = potentialRoutes.filter((route) => route.method === method);
|
|
7408
5645
|
for (const route of methodRoutes) {
|
|
7409
|
-
if (route.path ===
|
|
5646
|
+
if (route.path === url.pathname) {
|
|
7410
5647
|
const result = {
|
|
7411
5648
|
route,
|
|
7412
5649
|
params: {}
|
|
@@ -7417,7 +5654,7 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
7417
5654
|
}
|
|
7418
5655
|
for (const route of methodRoutes) {
|
|
7419
5656
|
if (route.pattern) {
|
|
7420
|
-
const match = route.pattern.exec(
|
|
5657
|
+
const match = route.pattern.exec(url);
|
|
7421
5658
|
if (match) {
|
|
7422
5659
|
const result = {
|
|
7423
5660
|
route,
|
|
@@ -7435,10 +5672,10 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
7435
5672
|
continue;
|
|
7436
5673
|
}
|
|
7437
5674
|
const basePath = route.path.slice(0, -1);
|
|
7438
|
-
if (
|
|
5675
|
+
if (url.pathname.startsWith(basePath)) {
|
|
7439
5676
|
const result = {
|
|
7440
5677
|
route,
|
|
7441
|
-
params: { wildcard:
|
|
5678
|
+
params: { wildcard: url.pathname.slice(basePath.length) }
|
|
7442
5679
|
};
|
|
7443
5680
|
this.routeCache.set(cacheKey, result);
|
|
7444
5681
|
return result;
|
|
@@ -7560,10 +5797,10 @@ function registerRouteBuilding(RouterClass) {
|
|
|
7560
5797
|
} catch {}
|
|
7561
5798
|
}
|
|
7562
5799
|
bunRoutes[path] = async (req) => {
|
|
7563
|
-
const
|
|
5800
|
+
const url = new URL(req.url);
|
|
7564
5801
|
const params = {};
|
|
7565
5802
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
7566
|
-
matchPath(path,
|
|
5803
|
+
matchPath(path, url.pathname, params, constraintsRecord);
|
|
7567
5804
|
const enhancedReq = this.enhanceRequest(req, params);
|
|
7568
5805
|
const routeHandlerMiddleware = async (req2, _next) => await this.resolveHandler(route.handler, req2);
|
|
7569
5806
|
const middlewareStack = [...this.globalMiddleware, ...route.middleware, routeHandlerMiddleware];
|
|
@@ -7575,10 +5812,10 @@ function registerRouteBuilding(RouterClass) {
|
|
|
7575
5812
|
for (const route of routes) {
|
|
7576
5813
|
const method = route.method;
|
|
7577
5814
|
methodHandlers[method] = async (req) => {
|
|
7578
|
-
const
|
|
5815
|
+
const url = new URL(req.url);
|
|
7579
5816
|
const params = {};
|
|
7580
5817
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
7581
|
-
matchPath(path,
|
|
5818
|
+
matchPath(path, url.pathname, params, constraintsRecord);
|
|
7582
5819
|
const enhancedReq = this.enhanceRequest(req, params);
|
|
7583
5820
|
const routeHandlerMiddleware = async (req2, _next) => await this.resolveHandler(route.handler, req2);
|
|
7584
5821
|
const middlewareStack = [...this.globalMiddleware, ...route.middleware, routeHandlerMiddleware];
|
|
@@ -7672,13 +5909,13 @@ function registerRouteMatching(RouterClass) {
|
|
|
7672
5909
|
Object.defineProperties(RouterClass.prototype, {
|
|
7673
5910
|
matchRoute: {
|
|
7674
5911
|
value(path, method, domain) {
|
|
7675
|
-
const
|
|
7676
|
-
const cacheKey = `${domain || ""}:${method}:${
|
|
5912
|
+
const url = new URL(path, "http://localhost");
|
|
5913
|
+
const cacheKey = `${domain || ""}:${method}:${url.pathname}`;
|
|
7677
5914
|
if (this.routeCache.has(cacheKey)) {
|
|
7678
5915
|
return this.routeCache.get(cacheKey);
|
|
7679
5916
|
}
|
|
7680
5917
|
if (this.staticRoutes.has(method)) {
|
|
7681
|
-
const staticRoute = this.staticRoutes.get(method).get(
|
|
5918
|
+
const staticRoute = this.staticRoutes.get(method).get(url.pathname);
|
|
7682
5919
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
7683
5920
|
const result = {
|
|
7684
5921
|
route: staticRoute,
|
|
@@ -7691,7 +5928,7 @@ function registerRouteMatching(RouterClass) {
|
|
|
7691
5928
|
const potentialRoutes = domain && this.domains[domain] ? this.domains[domain] : this.routes;
|
|
7692
5929
|
const methodRoutes = potentialRoutes.filter((route) => route.method === method);
|
|
7693
5930
|
for (const route of methodRoutes) {
|
|
7694
|
-
if (route.path ===
|
|
5931
|
+
if (route.path === url.pathname) {
|
|
7695
5932
|
const result = {
|
|
7696
5933
|
route,
|
|
7697
5934
|
params: {}
|
|
@@ -7702,7 +5939,7 @@ function registerRouteMatching(RouterClass) {
|
|
|
7702
5939
|
}
|
|
7703
5940
|
for (const route of methodRoutes) {
|
|
7704
5941
|
if (route.pattern) {
|
|
7705
|
-
const match = route.pattern.exec(
|
|
5942
|
+
const match = route.pattern.exec(url);
|
|
7706
5943
|
if (match) {
|
|
7707
5944
|
const result = {
|
|
7708
5945
|
route,
|
|
@@ -7717,11 +5954,11 @@ function registerRouteMatching(RouterClass) {
|
|
|
7717
5954
|
const wildcardRoutes = this.domains[domain].filter((route) => route.method === method && route.path.endsWith("*"));
|
|
7718
5955
|
for (const route of wildcardRoutes) {
|
|
7719
5956
|
const basePath = route.path.slice(0, -1);
|
|
7720
|
-
if (
|
|
5957
|
+
if (url.pathname.startsWith(basePath)) {
|
|
7721
5958
|
const result = {
|
|
7722
5959
|
route,
|
|
7723
5960
|
params: {
|
|
7724
|
-
wildcard:
|
|
5961
|
+
wildcard: url.pathname.slice(basePath.length)
|
|
7725
5962
|
}
|
|
7726
5963
|
};
|
|
7727
5964
|
this.routeCache.set(cacheKey, result);
|
|
@@ -7732,11 +5969,11 @@ function registerRouteMatching(RouterClass) {
|
|
|
7732
5969
|
const globalWildcardRoutes = this.routes.filter((route) => route.method === method && route.path.endsWith("*") && (!domain || !route.domain));
|
|
7733
5970
|
for (const route of globalWildcardRoutes) {
|
|
7734
5971
|
const basePath = route.path.slice(0, -1);
|
|
7735
|
-
if (
|
|
5972
|
+
if (url.pathname.startsWith(basePath)) {
|
|
7736
5973
|
const result = {
|
|
7737
5974
|
route,
|
|
7738
5975
|
params: {
|
|
7739
|
-
wildcard:
|
|
5976
|
+
wildcard: url.pathname.slice(basePath.length)
|
|
7740
5977
|
}
|
|
7741
5978
|
};
|
|
7742
5979
|
this.routeCache.set(cacheKey, result);
|
|
@@ -7837,9 +6074,9 @@ function registerRouteMatching(RouterClass) {
|
|
|
7837
6074
|
const routePath = lastRoute.path;
|
|
7838
6075
|
const constraintsRecord = lastRoute.constraints && !Array.isArray(lastRoute.constraints) ? lastRoute.constraints : undefined;
|
|
7839
6076
|
lastRoute.pattern = {
|
|
7840
|
-
exec: (
|
|
6077
|
+
exec: (url) => {
|
|
7841
6078
|
const params2 = {};
|
|
7842
|
-
const isMatch = matchPath(routePath,
|
|
6079
|
+
const isMatch = matchPath(routePath, url.pathname, params2, constraintsRecord);
|
|
7843
6080
|
if (!isMatch) {
|
|
7844
6081
|
return null;
|
|
7845
6082
|
}
|
|
@@ -8090,10 +6327,10 @@ class Router {
|
|
|
8090
6327
|
route.constraints = constraints;
|
|
8091
6328
|
}
|
|
8092
6329
|
route.pattern = {
|
|
8093
|
-
exec: (
|
|
6330
|
+
exec: (url) => {
|
|
8094
6331
|
const params = {};
|
|
8095
6332
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
8096
|
-
const isMatch = matchPath(routePath,
|
|
6333
|
+
const isMatch = matchPath(routePath, url.pathname, params, constraintsRecord);
|
|
8097
6334
|
if (!isMatch) {
|
|
8098
6335
|
return null;
|
|
8099
6336
|
}
|
|
@@ -8174,27 +6411,27 @@ class Router {
|
|
|
8174
6411
|
if (!route) {
|
|
8175
6412
|
throw new Error(`Route with name "${name}" not found`);
|
|
8176
6413
|
}
|
|
8177
|
-
let
|
|
6414
|
+
let url = route.path;
|
|
8178
6415
|
for (const [param, value] of Object.entries(params)) {
|
|
8179
|
-
|
|
8180
|
-
|
|
6416
|
+
url = url.replace(`{${param}}`, encodeURIComponent(value));
|
|
6417
|
+
url = url.replace(`{${param}?}`, encodeURIComponent(value));
|
|
8181
6418
|
}
|
|
8182
|
-
return
|
|
6419
|
+
return url;
|
|
8183
6420
|
}
|
|
8184
6421
|
onError(handler) {
|
|
8185
6422
|
this.errorHandler = handler;
|
|
8186
6423
|
return this;
|
|
8187
6424
|
}
|
|
8188
|
-
redirect(
|
|
6425
|
+
redirect(url, status = 302) {
|
|
8189
6426
|
const headers = new Headers;
|
|
8190
|
-
headers.set("Location",
|
|
6427
|
+
headers.set("Location", url);
|
|
8191
6428
|
return new Response(null, {
|
|
8192
6429
|
status,
|
|
8193
6430
|
headers
|
|
8194
6431
|
});
|
|
8195
6432
|
}
|
|
8196
|
-
permanentRedirect(
|
|
8197
|
-
return this.redirect(
|
|
6433
|
+
permanentRedirect(url) {
|
|
6434
|
+
return this.redirect(url, 301);
|
|
8198
6435
|
}
|
|
8199
6436
|
redirectRoute(from, to, status = 302) {
|
|
8200
6437
|
this.get(from, (_req) => {
|
|
@@ -8221,13 +6458,13 @@ class Router {
|
|
|
8221
6458
|
}
|
|
8222
6459
|
async handleRequest(req) {
|
|
8223
6460
|
try {
|
|
8224
|
-
const
|
|
6461
|
+
const url = new URL(req.url);
|
|
8225
6462
|
if (this.config.verbose) {
|
|
8226
|
-
console.log(`${req.method} ${
|
|
6463
|
+
console.log(`${req.method} ${url.pathname}`);
|
|
8227
6464
|
}
|
|
8228
6465
|
if (req.method === "OPTIONS") {
|
|
8229
|
-
const hostname2 =
|
|
8230
|
-
const optionsMatch = this.matchRoute(
|
|
6466
|
+
const hostname2 = url.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
6467
|
+
const optionsMatch = this.matchRoute(url.pathname, "OPTIONS", hostname2);
|
|
8231
6468
|
if (!optionsMatch) {
|
|
8232
6469
|
return new Response(null, {
|
|
8233
6470
|
status: 204,
|
|
@@ -8240,8 +6477,8 @@ class Router {
|
|
|
8240
6477
|
});
|
|
8241
6478
|
}
|
|
8242
6479
|
}
|
|
8243
|
-
const hostname =
|
|
8244
|
-
const match = this.matchRoute(
|
|
6480
|
+
const hostname = url.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
6481
|
+
const match = this.matchRoute(url.pathname, req.method, hostname);
|
|
8245
6482
|
const enhancedReq = this.enhanceRequest(req, match?.params || {});
|
|
8246
6483
|
if (match) {
|
|
8247
6484
|
enhancedReq.route = match.route;
|
|
@@ -8259,12 +6496,12 @@ class Router {
|
|
|
8259
6496
|
}
|
|
8260
6497
|
return new Response("No response from middleware chain", { status: 500 });
|
|
8261
6498
|
}
|
|
8262
|
-
const allowedMethods = this.getAllowedMethods(
|
|
6499
|
+
const allowedMethods = this.getAllowedMethods(url.pathname, hostname);
|
|
8263
6500
|
if (allowedMethods.length > 0) {
|
|
8264
6501
|
const methodNotAllowedHandler = async (_req, _next) => {
|
|
8265
6502
|
return new Response(JSON.stringify({
|
|
8266
6503
|
error: "Method Not Allowed",
|
|
8267
|
-
path:
|
|
6504
|
+
path: url.pathname,
|
|
8268
6505
|
method: req.method,
|
|
8269
6506
|
allowed: allowedMethods
|
|
8270
6507
|
}), {
|
|
@@ -8291,7 +6528,7 @@ class Router {
|
|
|
8291
6528
|
}
|
|
8292
6529
|
return new Response(JSON.stringify({
|
|
8293
6530
|
error: "Not Found",
|
|
8294
|
-
path:
|
|
6531
|
+
path: url.pathname,
|
|
8295
6532
|
method: req.method
|
|
8296
6533
|
}), {
|
|
8297
6534
|
status: 404,
|
|
@@ -8310,7 +6547,7 @@ class Router {
|
|
|
8310
6547
|
}
|
|
8311
6548
|
return new Response(JSON.stringify({
|
|
8312
6549
|
error: "Not Found",
|
|
8313
|
-
path:
|
|
6550
|
+
path: url.pathname,
|
|
8314
6551
|
method: req.method
|
|
8315
6552
|
}), {
|
|
8316
6553
|
status: 404,
|
|
@@ -8353,15 +6590,15 @@ class Router {
|
|
|
8353
6590
|
}
|
|
8354
6591
|
}
|
|
8355
6592
|
getAllowedMethods(path, domain) {
|
|
8356
|
-
const
|
|
8357
|
-
const cacheKey = `${domain || ""}:${
|
|
6593
|
+
const url = new URL(path, "http://localhost");
|
|
6594
|
+
const cacheKey = `${domain || ""}:${url.pathname}`;
|
|
8358
6595
|
const cached = this._allowedMethodsCache.get(cacheKey);
|
|
8359
6596
|
if (cached) {
|
|
8360
6597
|
return cached;
|
|
8361
6598
|
}
|
|
8362
6599
|
const methods = new Set;
|
|
8363
6600
|
for (const [method, routesByPath] of this.staticRoutes) {
|
|
8364
|
-
const staticRoute = routesByPath.get(
|
|
6601
|
+
const staticRoute = routesByPath.get(url.pathname);
|
|
8365
6602
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
8366
6603
|
methods.add(method);
|
|
8367
6604
|
}
|
|
@@ -8371,15 +6608,15 @@ class Router {
|
|
|
8371
6608
|
if (methods.has(route.method)) {
|
|
8372
6609
|
continue;
|
|
8373
6610
|
}
|
|
8374
|
-
if (route.path ===
|
|
6611
|
+
if (route.path === url.pathname) {
|
|
8375
6612
|
methods.add(route.method);
|
|
8376
6613
|
continue;
|
|
8377
6614
|
}
|
|
8378
|
-
if (route.pattern && route.pattern.exec(
|
|
6615
|
+
if (route.pattern && route.pattern.exec(url)) {
|
|
8379
6616
|
methods.add(route.method);
|
|
8380
6617
|
continue;
|
|
8381
6618
|
}
|
|
8382
|
-
if (route.path.endsWith("*") &&
|
|
6619
|
+
if (route.path.endsWith("*") && url.pathname.startsWith(route.path.slice(0, -1))) {
|
|
8383
6620
|
methods.add(route.method);
|
|
8384
6621
|
}
|
|
8385
6622
|
}
|
|
@@ -8394,13 +6631,13 @@ class Router {
|
|
|
8394
6631
|
return result;
|
|
8395
6632
|
}
|
|
8396
6633
|
matchRoute(path, method, domain) {
|
|
8397
|
-
const
|
|
8398
|
-
const cacheKey = `${domain || ""}:${method}:${
|
|
6634
|
+
const url = new URL(path, "http://localhost");
|
|
6635
|
+
const cacheKey = `${domain || ""}:${method}:${url.pathname}`;
|
|
8399
6636
|
if (this.routeCache.has(cacheKey)) {
|
|
8400
6637
|
return this.routeCache.get(cacheKey);
|
|
8401
6638
|
}
|
|
8402
6639
|
if (this.staticRoutes.has(method)) {
|
|
8403
|
-
const staticRoute = this.staticRoutes.get(method).get(
|
|
6640
|
+
const staticRoute = this.staticRoutes.get(method).get(url.pathname);
|
|
8404
6641
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
8405
6642
|
const result = {
|
|
8406
6643
|
route: staticRoute,
|
|
@@ -8413,7 +6650,7 @@ class Router {
|
|
|
8413
6650
|
const potentialRoutes = domain && this.domains[domain] ? this.domains[domain] : this.routes;
|
|
8414
6651
|
const methodRoutes = potentialRoutes.filter((route) => route.method === method);
|
|
8415
6652
|
for (const route of methodRoutes) {
|
|
8416
|
-
if (route.path ===
|
|
6653
|
+
if (route.path === url.pathname) {
|
|
8417
6654
|
const result = {
|
|
8418
6655
|
route,
|
|
8419
6656
|
params: {}
|
|
@@ -8424,7 +6661,7 @@ class Router {
|
|
|
8424
6661
|
}
|
|
8425
6662
|
for (const route of methodRoutes) {
|
|
8426
6663
|
if (route.pattern) {
|
|
8427
|
-
const match = route.pattern.exec(
|
|
6664
|
+
const match = route.pattern.exec(url);
|
|
8428
6665
|
if (match) {
|
|
8429
6666
|
const result = {
|
|
8430
6667
|
route,
|
|
@@ -8439,11 +6676,11 @@ class Router {
|
|
|
8439
6676
|
const wildcardRoutes = this.domains[domain].filter((route) => route.method === method && route.path.endsWith("*"));
|
|
8440
6677
|
for (const route of wildcardRoutes) {
|
|
8441
6678
|
const basePath = route.path.slice(0, -1);
|
|
8442
|
-
if (
|
|
6679
|
+
if (url.pathname.startsWith(basePath)) {
|
|
8443
6680
|
const result = {
|
|
8444
6681
|
route,
|
|
8445
6682
|
params: {
|
|
8446
|
-
wildcard:
|
|
6683
|
+
wildcard: url.pathname.slice(basePath.length)
|
|
8447
6684
|
}
|
|
8448
6685
|
};
|
|
8449
6686
|
this.routeCache.set(cacheKey, result);
|
|
@@ -8454,11 +6691,11 @@ class Router {
|
|
|
8454
6691
|
const globalWildcardRoutes = this.routes.filter((route) => route.method === method && route.path.endsWith("*") && (!domain || !route.domain));
|
|
8455
6692
|
for (const route of globalWildcardRoutes) {
|
|
8456
6693
|
const basePath = route.path.slice(0, -1);
|
|
8457
|
-
if (
|
|
6694
|
+
if (url.pathname.startsWith(basePath)) {
|
|
8458
6695
|
const result = {
|
|
8459
6696
|
route,
|
|
8460
6697
|
params: {
|
|
8461
|
-
wildcard:
|
|
6698
|
+
wildcard: url.pathname.slice(basePath.length)
|
|
8462
6699
|
}
|
|
8463
6700
|
};
|
|
8464
6701
|
this.routeCache.set(cacheKey, result);
|
|
@@ -8487,9 +6724,9 @@ class Router {
|
|
|
8487
6724
|
}
|
|
8488
6725
|
return parsedCookies;
|
|
8489
6726
|
};
|
|
8490
|
-
const
|
|
6727
|
+
const url = new URL(req.url);
|
|
8491
6728
|
const query = {};
|
|
8492
|
-
|
|
6729
|
+
url.searchParams.forEach((value, key) => {
|
|
8493
6730
|
query[key] = value;
|
|
8494
6731
|
});
|
|
8495
6732
|
const enhancedReq = {
|
|
@@ -8866,7 +7103,7 @@ class Router {
|
|
|
8866
7103
|
}
|
|
8867
7104
|
return this;
|
|
8868
7105
|
}
|
|
8869
|
-
async executeMiddleware(middleware2,
|
|
7106
|
+
async executeMiddleware(middleware2, request, handler) {
|
|
8870
7107
|
if (middleware2.length === 0) {
|
|
8871
7108
|
return handler();
|
|
8872
7109
|
}
|
|
@@ -8876,7 +7113,7 @@ class Router {
|
|
|
8876
7113
|
return handler();
|
|
8877
7114
|
}
|
|
8878
7115
|
const mw = middleware2[currentIndex++];
|
|
8879
|
-
const result = await mw(
|
|
7116
|
+
const result = await mw(request, next);
|
|
8880
7117
|
return result || new Response("No response from middleware", { status: 500 });
|
|
8881
7118
|
};
|
|
8882
7119
|
return next();
|
|
@@ -9038,16 +7275,16 @@ class RequestWithMacros {
|
|
|
9038
7275
|
static macroAccessor(name, getter) {
|
|
9039
7276
|
requestMacroRegistry.register(name, getter, true);
|
|
9040
7277
|
}
|
|
9041
|
-
static applyMacros(
|
|
7278
|
+
static applyMacros(request) {
|
|
9042
7279
|
const epoch = requestMacroRegistry.getEpoch();
|
|
9043
7280
|
if (epoch !== macroProtoEpoch) {
|
|
9044
7281
|
macroProtoCache = new WeakMap;
|
|
9045
7282
|
macroProtoEpoch = epoch;
|
|
9046
7283
|
}
|
|
9047
|
-
let base = Object.getPrototypeOf(
|
|
7284
|
+
let base = Object.getPrototypeOf(request) ?? Object.prototype;
|
|
9048
7285
|
if (base[MACRO_PROTO]) {
|
|
9049
7286
|
if (macroProtoCache.get(base[MACRO_BASE]) === base) {
|
|
9050
|
-
return
|
|
7287
|
+
return request;
|
|
9051
7288
|
}
|
|
9052
7289
|
base = base[MACRO_BASE];
|
|
9053
7290
|
}
|
|
@@ -9076,8 +7313,8 @@ class RequestWithMacros {
|
|
|
9076
7313
|
Object.defineProperty(proto, MACRO_BASE, { value: base });
|
|
9077
7314
|
macroProtoCache.set(base, proto);
|
|
9078
7315
|
}
|
|
9079
|
-
Object.setPrototypeOf(
|
|
9080
|
-
return
|
|
7316
|
+
Object.setPrototypeOf(request, proto);
|
|
7317
|
+
return request;
|
|
9081
7318
|
}
|
|
9082
7319
|
static hasMacro(name) {
|
|
9083
7320
|
return requestMacroRegistry.has(name);
|
|
@@ -9271,8 +7508,8 @@ var BuiltInRequestMacros = {
|
|
|
9271
7508
|
return this.url;
|
|
9272
7509
|
},
|
|
9273
7510
|
root() {
|
|
9274
|
-
const
|
|
9275
|
-
return `${
|
|
7511
|
+
const url = getParsedURL(this);
|
|
7512
|
+
return `${url.protocol}//${url.host}`;
|
|
9276
7513
|
},
|
|
9277
7514
|
is(pattern) {
|
|
9278
7515
|
const path = this.path();
|
|
@@ -9291,11 +7528,11 @@ var BuiltInRequestMacros = {
|
|
|
9291
7528
|
return this.routeName || null;
|
|
9292
7529
|
},
|
|
9293
7530
|
fingerprint() {
|
|
9294
|
-
const
|
|
7531
|
+
const url = getParsedURL(this);
|
|
9295
7532
|
const data = {
|
|
9296
7533
|
method: this.method,
|
|
9297
|
-
path:
|
|
9298
|
-
query:
|
|
7534
|
+
path: url.pathname,
|
|
7535
|
+
query: url.search,
|
|
9299
7536
|
ip: BuiltInRequestMacros.ip.call(this)
|
|
9300
7537
|
};
|
|
9301
7538
|
return btoa(JSON.stringify(data));
|
|
@@ -9346,6 +7583,36 @@ function registerBuiltInRequestMacros() {
|
|
|
9346
7583
|
}
|
|
9347
7584
|
registerBuiltInRequestMacros();
|
|
9348
7585
|
|
|
7586
|
+
// src/request/context.ts
|
|
7587
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
7588
|
+
var storage = new AsyncLocalStorage;
|
|
7589
|
+
var contextEnabled = false;
|
|
7590
|
+
var syncCurrent = null;
|
|
7591
|
+
function runWithRequest(initial, fn) {
|
|
7592
|
+
if (!contextEnabled) {
|
|
7593
|
+
syncCurrent = initial;
|
|
7594
|
+
const result = fn();
|
|
7595
|
+
if (result instanceof Promise) {
|
|
7596
|
+
return result.finally(() => {
|
|
7597
|
+
syncCurrent = null;
|
|
7598
|
+
});
|
|
7599
|
+
}
|
|
7600
|
+
syncCurrent = null;
|
|
7601
|
+
return result;
|
|
7602
|
+
}
|
|
7603
|
+
return storage.run({ current: initial }, fn);
|
|
7604
|
+
}
|
|
7605
|
+
function setCurrentRequest(req) {
|
|
7606
|
+
const store = storage.getStore();
|
|
7607
|
+
if (store) {
|
|
7608
|
+
store.current = req;
|
|
7609
|
+
return;
|
|
7610
|
+
}
|
|
7611
|
+
if (syncCurrent !== null) {
|
|
7612
|
+
syncCurrent = req;
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7615
|
+
|
|
9349
7616
|
// src/router/server.ts
|
|
9350
7617
|
if (!RequestWithMacros.hasMacro("getParam")) {
|
|
9351
7618
|
RequestWithMacros.macro("getParam", function(name, defaultValue) {
|
|
@@ -9631,10 +7898,10 @@ function registerServerHandling(RouterClass) {
|
|
|
9631
7898
|
handleRequestImpl: {
|
|
9632
7899
|
async value(req) {
|
|
9633
7900
|
try {
|
|
9634
|
-
const
|
|
9635
|
-
req._parsedURL =
|
|
9636
|
-
const hostname =
|
|
9637
|
-
const match = this.matchRoute(
|
|
7901
|
+
const url = new URL(req.url);
|
|
7902
|
+
req._parsedURL = url;
|
|
7903
|
+
const hostname = url.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
7904
|
+
const match = this.matchRoute(url.pathname, req.method, hostname);
|
|
9638
7905
|
if (req.method === "OPTIONS" && !match) {
|
|
9639
7906
|
const origin = req.headers.get("origin");
|
|
9640
7907
|
const preflightHeaders = {
|
|
@@ -9654,7 +7921,7 @@ function registerServerHandling(RouterClass) {
|
|
|
9654
7921
|
}
|
|
9655
7922
|
const enhancedReq = this.enhanceRequest(req, {});
|
|
9656
7923
|
setCurrentRequest(enhancedReq);
|
|
9657
|
-
const allowedMethods = this.getAllowedMethods(
|
|
7924
|
+
const allowedMethods = this.getAllowedMethods(url.pathname, hostname);
|
|
9658
7925
|
const corsHeaders = {
|
|
9659
7926
|
"Content-Type": "application/json",
|
|
9660
7927
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -9666,7 +7933,7 @@ function registerServerHandling(RouterClass) {
|
|
|
9666
7933
|
return new Response(JSON.stringify({
|
|
9667
7934
|
success: false,
|
|
9668
7935
|
message: "Method Not Allowed",
|
|
9669
|
-
path:
|
|
7936
|
+
path: url.pathname,
|
|
9670
7937
|
method: req.method,
|
|
9671
7938
|
allowed: allowedMethods
|
|
9672
7939
|
}), {
|
|
@@ -9690,7 +7957,7 @@ function registerServerHandling(RouterClass) {
|
|
|
9690
7957
|
return new Response(JSON.stringify({
|
|
9691
7958
|
success: false,
|
|
9692
7959
|
message: "Not Found",
|
|
9693
|
-
path:
|
|
7960
|
+
path: url.pathname,
|
|
9694
7961
|
method: req.method
|
|
9695
7962
|
}), { status: 404, headers: corsHeaders });
|
|
9696
7963
|
};
|
|
@@ -9985,34 +8252,34 @@ function registerWebSocketHandling(RouterClass) {
|
|
|
9985
8252
|
configurable: true
|
|
9986
8253
|
},
|
|
9987
8254
|
upgrade: {
|
|
9988
|
-
value(
|
|
8255
|
+
value(request, options) {
|
|
9989
8256
|
if (!this.serverInstance) {
|
|
9990
8257
|
throw new Error("Server not started, cannot upgrade to WebSocket");
|
|
9991
8258
|
}
|
|
9992
8259
|
if (!this.wsConfig) {
|
|
9993
8260
|
throw new Error("WebSocket configuration not set, use router.websocket() to configure");
|
|
9994
8261
|
}
|
|
9995
|
-
return this.serverInstance.upgrade(
|
|
8262
|
+
return this.serverInstance.upgrade(request, options);
|
|
9996
8263
|
},
|
|
9997
8264
|
writable: true,
|
|
9998
8265
|
configurable: true
|
|
9999
8266
|
},
|
|
10000
8267
|
timeout: {
|
|
10001
|
-
value(
|
|
8268
|
+
value(request, seconds) {
|
|
10002
8269
|
if (!this.serverInstance) {
|
|
10003
8270
|
throw new Error("Server not started, cannot set timeout");
|
|
10004
8271
|
}
|
|
10005
|
-
this.serverInstance.timeout(
|
|
8272
|
+
this.serverInstance.timeout(request, seconds);
|
|
10006
8273
|
},
|
|
10007
8274
|
writable: true,
|
|
10008
8275
|
configurable: true
|
|
10009
8276
|
},
|
|
10010
8277
|
requestIP: {
|
|
10011
|
-
value(
|
|
8278
|
+
value(request) {
|
|
10012
8279
|
if (!this.serverInstance) {
|
|
10013
8280
|
throw new Error("Server not started, cannot get request IP");
|
|
10014
8281
|
}
|
|
10015
|
-
return this.serverInstance.requestIP(
|
|
8282
|
+
return this.serverInstance.requestIP(request);
|
|
10016
8283
|
},
|
|
10017
8284
|
writable: true,
|
|
10018
8285
|
configurable: true
|
|
@@ -10020,7 +8287,7 @@ function registerWebSocketHandling(RouterClass) {
|
|
|
10020
8287
|
});
|
|
10021
8288
|
}
|
|
10022
8289
|
// src/response/macros.ts
|
|
10023
|
-
import
|
|
8290
|
+
import process3 from "process";
|
|
10024
8291
|
|
|
10025
8292
|
class ResponseMacroRegistry {
|
|
10026
8293
|
macros = new Map;
|
|
@@ -10201,16 +8468,16 @@ var BuiltInResponseMacros = {
|
|
|
10201
8468
|
}
|
|
10202
8469
|
});
|
|
10203
8470
|
},
|
|
10204
|
-
redirect: (
|
|
8471
|
+
redirect: (url, status = 302) => {
|
|
10205
8472
|
return new Response(null, {
|
|
10206
8473
|
status,
|
|
10207
8474
|
headers: {
|
|
10208
|
-
Location:
|
|
8475
|
+
Location: url
|
|
10209
8476
|
}
|
|
10210
8477
|
});
|
|
10211
8478
|
},
|
|
10212
|
-
permanentRedirect: (
|
|
10213
|
-
return BuiltInResponseMacros.redirect(
|
|
8479
|
+
permanentRedirect: (url) => {
|
|
8480
|
+
return BuiltInResponseMacros.redirect(url, 301);
|
|
10214
8481
|
},
|
|
10215
8482
|
download: (data, filename, contentType = "application/octet-stream") => {
|
|
10216
8483
|
return new Response(data, {
|
|
@@ -10281,7 +8548,7 @@ var BuiltInResponseMacros = {
|
|
|
10281
8548
|
status,
|
|
10282
8549
|
timestamp: new Date().toISOString(),
|
|
10283
8550
|
checks,
|
|
10284
|
-
uptime:
|
|
8551
|
+
uptime: process3.uptime?.() || 0
|
|
10285
8552
|
};
|
|
10286
8553
|
return new Response(JSON.stringify(response2), {
|
|
10287
8554
|
status: statusCode,
|
|
@@ -10345,8 +8612,8 @@ var BuiltInRules = {
|
|
|
10345
8612
|
name: "url",
|
|
10346
8613
|
validate: (value) => {
|
|
10347
8614
|
try {
|
|
10348
|
-
const
|
|
10349
|
-
return Boolean(
|
|
8615
|
+
const url = new URL(value);
|
|
8616
|
+
return Boolean(url);
|
|
10350
8617
|
} catch {
|
|
10351
8618
|
return false;
|
|
10352
8619
|
}
|
|
@@ -11005,119 +9272,6 @@ registerRouteBuilding(Router);
|
|
|
11005
9272
|
registerModelBinding(Router);
|
|
11006
9273
|
registerFileBasedRouting(Router);
|
|
11007
9274
|
registerApiRoutes(Router);
|
|
11008
|
-
// src/router/middleware-groups.ts
|
|
11009
|
-
class MiddlewareGroupRegistry {
|
|
11010
|
-
groups = new Map;
|
|
11011
|
-
globalMiddleware = [];
|
|
11012
|
-
middlewarePriority = new Map;
|
|
11013
|
-
define(name, middleware2) {
|
|
11014
|
-
this.groups.set(name, [...middleware2]);
|
|
11015
|
-
return this;
|
|
11016
|
-
}
|
|
11017
|
-
extend(name, middleware2) {
|
|
11018
|
-
const existing = this.groups.get(name) || [];
|
|
11019
|
-
this.groups.set(name, [...existing, ...middleware2]);
|
|
11020
|
-
return this;
|
|
11021
|
-
}
|
|
11022
|
-
prepend(name, middleware2) {
|
|
11023
|
-
const existing = this.groups.get(name) || [];
|
|
11024
|
-
this.groups.set(name, [...middleware2, ...existing]);
|
|
11025
|
-
return this;
|
|
11026
|
-
}
|
|
11027
|
-
get(name) {
|
|
11028
|
-
return this.groups.get(name) || [];
|
|
11029
|
-
}
|
|
11030
|
-
has(name) {
|
|
11031
|
-
return this.groups.has(name);
|
|
11032
|
-
}
|
|
11033
|
-
remove(name) {
|
|
11034
|
-
this.groups.delete(name);
|
|
11035
|
-
return this;
|
|
11036
|
-
}
|
|
11037
|
-
resolve(middleware2) {
|
|
11038
|
-
if (typeof middleware2 === "function") {
|
|
11039
|
-
return [middleware2];
|
|
11040
|
-
}
|
|
11041
|
-
if (this.groups.has(middleware2)) {
|
|
11042
|
-
return this.get(middleware2);
|
|
11043
|
-
}
|
|
11044
|
-
return [middleware2];
|
|
11045
|
-
}
|
|
11046
|
-
resolveAll(middleware2) {
|
|
11047
|
-
const resolved = [];
|
|
11048
|
-
for (const mw of middleware2) {
|
|
11049
|
-
resolved.push(...this.resolve(mw));
|
|
11050
|
-
}
|
|
11051
|
-
return resolved;
|
|
11052
|
-
}
|
|
11053
|
-
pushGlobal(...middleware2) {
|
|
11054
|
-
this.globalMiddleware.push(...middleware2);
|
|
11055
|
-
return this;
|
|
11056
|
-
}
|
|
11057
|
-
prependGlobal(...middleware2) {
|
|
11058
|
-
this.globalMiddleware.unshift(...middleware2);
|
|
11059
|
-
return this;
|
|
11060
|
-
}
|
|
11061
|
-
removeGlobal(middleware2) {
|
|
11062
|
-
this.globalMiddleware = this.globalMiddleware.filter((mw) => typeof mw === "string" ? mw !== middleware2 : true);
|
|
11063
|
-
return this;
|
|
11064
|
-
}
|
|
11065
|
-
getGlobal() {
|
|
11066
|
-
return [...this.globalMiddleware];
|
|
11067
|
-
}
|
|
11068
|
-
setPriority(name, priority) {
|
|
11069
|
-
this.middlewarePriority.set(name, priority);
|
|
11070
|
-
return this;
|
|
11071
|
-
}
|
|
11072
|
-
sortByPriority(middleware2) {
|
|
11073
|
-
return [...middleware2].sort((a, b) => {
|
|
11074
|
-
const nameA = typeof a === "string" ? a.split(":")[0] : "";
|
|
11075
|
-
const nameB = typeof b === "string" ? b.split(":")[0] : "";
|
|
11076
|
-
const priorityA = this.middlewarePriority.get(nameA) ?? 100;
|
|
11077
|
-
const priorityB = this.middlewarePriority.get(nameB) ?? 100;
|
|
11078
|
-
return priorityA - priorityB;
|
|
11079
|
-
});
|
|
11080
|
-
}
|
|
11081
|
-
filter(middleware2, options) {
|
|
11082
|
-
if (options.only && options.only.length > 0) {
|
|
11083
|
-
return middleware2.filter((mw) => {
|
|
11084
|
-
if (typeof mw === "string") {
|
|
11085
|
-
const name = mw.split(":")[0];
|
|
11086
|
-
return options.only.includes(name);
|
|
11087
|
-
}
|
|
11088
|
-
return false;
|
|
11089
|
-
});
|
|
11090
|
-
}
|
|
11091
|
-
if (options.except && options.except.length > 0) {
|
|
11092
|
-
return middleware2.filter((mw) => {
|
|
11093
|
-
if (typeof mw === "string") {
|
|
11094
|
-
const name = mw.split(":")[0];
|
|
11095
|
-
return !options.except.includes(name);
|
|
11096
|
-
}
|
|
11097
|
-
return true;
|
|
11098
|
-
});
|
|
11099
|
-
}
|
|
11100
|
-
return middleware2;
|
|
11101
|
-
}
|
|
11102
|
-
flush() {
|
|
11103
|
-
this.groups.clear();
|
|
11104
|
-
this.globalMiddleware = [];
|
|
11105
|
-
this.middlewarePriority.clear();
|
|
11106
|
-
}
|
|
11107
|
-
}
|
|
11108
|
-
var middlewareGroups = new MiddlewareGroupRegistry;
|
|
11109
|
-
middlewareGroups.define("web", ["session", "csrf", "cors"]);
|
|
11110
|
-
middlewareGroups.define("api", ["throttle:60,1", "cors"]);
|
|
11111
|
-
middlewareGroups.setPriority("session", 10);
|
|
11112
|
-
middlewareGroups.setPriority("cors", 20);
|
|
11113
|
-
middlewareGroups.setPriority("csrf", 30);
|
|
11114
|
-
middlewareGroups.setPriority("auth", 40);
|
|
11115
|
-
middlewareGroups.setPriority("throttle", 50);
|
|
11116
|
-
middlewareGroups.setPriority("role", 60);
|
|
11117
|
-
middlewareGroups.setPriority("permission", 60);
|
|
11118
|
-
middlewareGroups.setPriority("can", 70);
|
|
11119
|
-
middlewareGroups.setPriority("abilities", 70);
|
|
11120
|
-
middlewareGroups.setPriority("verified", 80);
|
|
11121
9275
|
// src/testing/auth-testing.ts
|
|
11122
9276
|
import { mock } from "bun:test";
|
|
11123
9277
|
|
|
@@ -11336,8 +9490,8 @@ function createCookieAccessor2(cookies) {
|
|
|
11336
9490
|
|
|
11337
9491
|
class AuthTester {
|
|
11338
9492
|
request;
|
|
11339
|
-
constructor(
|
|
11340
|
-
this.request =
|
|
9493
|
+
constructor(request) {
|
|
9494
|
+
this.request = request || createMockRequest();
|
|
11341
9495
|
}
|
|
11342
9496
|
as(user) {
|
|
11343
9497
|
this.request.user = user;
|
|
@@ -11565,16 +9719,16 @@ var authMocks = {
|
|
|
11565
9719
|
})
|
|
11566
9720
|
};
|
|
11567
9721
|
var authHelpers = {
|
|
11568
|
-
authenticatedRequest: (user,
|
|
11569
|
-
return new AuthTester(
|
|
9722
|
+
authenticatedRequest: (user, request) => {
|
|
9723
|
+
return new AuthTester(request).as(user).getRequest();
|
|
11570
9724
|
},
|
|
11571
|
-
jwtRequest: (payload = {},
|
|
9725
|
+
jwtRequest: (payload = {}, request) => {
|
|
11572
9726
|
const jwt = new JWTTester;
|
|
11573
9727
|
const token = jwt.generateToken(payload);
|
|
11574
|
-
return new AuthTester(
|
|
9728
|
+
return new AuthTester(request).withJWT(token).getRequest();
|
|
11575
9729
|
},
|
|
11576
|
-
sessionRequest: (sessionData = {},
|
|
11577
|
-
return new AuthTester(
|
|
9730
|
+
sessionRequest: (sessionData = {}, request) => {
|
|
9731
|
+
return new AuthTester(request).withSession(sessionData).getRequest();
|
|
11578
9732
|
},
|
|
11579
9733
|
verifyJWTStructure: (token) => {
|
|
11580
9734
|
const parts = token.split(".");
|
|
@@ -11589,8 +9743,8 @@ var authHelpers = {
|
|
|
11589
9743
|
}
|
|
11590
9744
|
}
|
|
11591
9745
|
};
|
|
11592
|
-
function createAuthTester(
|
|
11593
|
-
return new AuthTester(
|
|
9746
|
+
function createAuthTester(request) {
|
|
9747
|
+
return new AuthTester(request);
|
|
11594
9748
|
}
|
|
11595
9749
|
function createJWTTester(options) {
|
|
11596
9750
|
return new JWTTester(options);
|
|
@@ -11598,13 +9752,14 @@ function createJWTTester(options) {
|
|
|
11598
9752
|
function createSessionTester(options) {
|
|
11599
9753
|
return new SessionTester(options);
|
|
11600
9754
|
}
|
|
9755
|
+
|
|
11601
9756
|
// src/testing/file-upload-testing.ts
|
|
11602
9757
|
import { mock as mock2 } from "bun:test";
|
|
11603
9758
|
class FileUploadTester {
|
|
11604
9759
|
request;
|
|
11605
9760
|
files;
|
|
11606
|
-
constructor(
|
|
11607
|
-
this.request =
|
|
9761
|
+
constructor(request) {
|
|
9762
|
+
this.request = request || createMockRequest();
|
|
11608
9763
|
this.files = [];
|
|
11609
9764
|
}
|
|
11610
9765
|
addFile(testFile) {
|
|
@@ -11836,8 +9991,8 @@ var fileUploadScenarios = {
|
|
|
11836
9991
|
}
|
|
11837
9992
|
};
|
|
11838
9993
|
var fileUploadAssertions = {
|
|
11839
|
-
hasFiles: (
|
|
11840
|
-
const files =
|
|
9994
|
+
hasFiles: (request, count) => {
|
|
9995
|
+
const files = request.files || [];
|
|
11841
9996
|
if (count !== undefined && files.length !== count) {
|
|
11842
9997
|
throw new Error(`Expected ${count} files, but got ${files.length}`);
|
|
11843
9998
|
}
|
|
@@ -11845,8 +10000,8 @@ var fileUploadAssertions = {
|
|
|
11845
10000
|
throw new Error("Expected request to have files");
|
|
11846
10001
|
}
|
|
11847
10002
|
},
|
|
11848
|
-
hasFileWithField: (
|
|
11849
|
-
const files =
|
|
10003
|
+
hasFileWithField: (request, fieldName) => {
|
|
10004
|
+
const files = request.files || [];
|
|
11850
10005
|
const hasField = files.some((file) => file.fieldName === fieldName);
|
|
11851
10006
|
if (!hasField) {
|
|
11852
10007
|
throw new Error(`Expected file with field name '${fieldName}'`);
|
|
@@ -11870,8 +10025,8 @@ var fileUploadAssertions = {
|
|
|
11870
10025
|
}
|
|
11871
10026
|
}
|
|
11872
10027
|
};
|
|
11873
|
-
function createFileUploadTester(
|
|
11874
|
-
return new FileUploadTester(
|
|
10028
|
+
function createFileUploadTester(request) {
|
|
10029
|
+
return new FileUploadTester(request);
|
|
11875
10030
|
}
|
|
11876
10031
|
var testFiles = {
|
|
11877
10032
|
image: (fieldName = "image", filename = "test.jpg") => ({
|
|
@@ -11905,6 +10060,7 @@ var testFiles = {
|
|
|
11905
10060
|
mimetype: "application/x-executable"
|
|
11906
10061
|
})
|
|
11907
10062
|
};
|
|
10063
|
+
|
|
11908
10064
|
// src/testing/middleware-testing.ts
|
|
11909
10065
|
import { mock as mock3, spyOn } from "bun:test";
|
|
11910
10066
|
class MiddlewareTester {
|
|
@@ -11915,15 +10071,15 @@ class MiddlewareTester {
|
|
|
11915
10071
|
this.context = this.createContext();
|
|
11916
10072
|
}
|
|
11917
10073
|
createContext() {
|
|
11918
|
-
const
|
|
10074
|
+
const request = createMockRequest();
|
|
11919
10075
|
const next = mock3(() => Promise.resolve(new Response("Next called", { status: 200 })));
|
|
11920
10076
|
return {
|
|
11921
|
-
request
|
|
10077
|
+
request,
|
|
11922
10078
|
next
|
|
11923
10079
|
};
|
|
11924
10080
|
}
|
|
11925
|
-
withRequest(
|
|
11926
|
-
this.context.request =
|
|
10081
|
+
withRequest(request) {
|
|
10082
|
+
this.context.request = request;
|
|
11927
10083
|
return this;
|
|
11928
10084
|
}
|
|
11929
10085
|
withNext(nextFn) {
|
|
@@ -12021,8 +10177,8 @@ class MiddlewareChainTester {
|
|
|
12021
10177
|
this.request = createMockRequest();
|
|
12022
10178
|
this.finalHandler = mock3(async () => new Response("Final handler", { status: 200 }));
|
|
12023
10179
|
}
|
|
12024
|
-
withRequest(
|
|
12025
|
-
this.request =
|
|
10180
|
+
withRequest(request) {
|
|
10181
|
+
this.request = request;
|
|
12026
10182
|
return this;
|
|
12027
10183
|
}
|
|
12028
10184
|
withFinalHandler(handler) {
|
|
@@ -12062,11 +10218,11 @@ function createMiddlewareSpy() {
|
|
|
12062
10218
|
}
|
|
12063
10219
|
async function benchmarkMiddleware(middleware2, iterations = 1000) {
|
|
12064
10220
|
const times = [];
|
|
12065
|
-
const
|
|
10221
|
+
const request = createMockRequest();
|
|
12066
10222
|
const next = mock3(async () => new Response("OK"));
|
|
12067
10223
|
for (let i = 0;i < iterations; i++) {
|
|
12068
10224
|
const start = performance.now();
|
|
12069
|
-
await middleware2(
|
|
10225
|
+
await middleware2(request, next);
|
|
12070
10226
|
const end = performance.now();
|
|
12071
10227
|
times.push(end - start);
|
|
12072
10228
|
}
|
|
@@ -12081,6 +10237,7 @@ async function benchmarkMiddleware(middleware2, iterations = 1000) {
|
|
|
12081
10237
|
totalTime
|
|
12082
10238
|
};
|
|
12083
10239
|
}
|
|
10240
|
+
|
|
12084
10241
|
// src/testing/model-binding-testing.ts
|
|
12085
10242
|
import { mock as mock4 } from "bun:test";
|
|
12086
10243
|
class GenericMockModel {
|
|
@@ -12164,8 +10321,8 @@ class ModelBindingTester {
|
|
|
12164
10321
|
this.options = options;
|
|
12165
10322
|
this.request = createMockRequest();
|
|
12166
10323
|
}
|
|
12167
|
-
withRequest(
|
|
12168
|
-
this.request =
|
|
10324
|
+
withRequest(request) {
|
|
10325
|
+
this.request = request;
|
|
12169
10326
|
return this;
|
|
12170
10327
|
}
|
|
12171
10328
|
withParams(params) {
|
|
@@ -12303,18 +10460,18 @@ var modelMocks = {
|
|
|
12303
10460
|
};
|
|
12304
10461
|
var constraintHelpers = {
|
|
12305
10462
|
belongsTo: (userIdField = "userId") => {
|
|
12306
|
-
const fn = async (model,
|
|
12307
|
-
if (!
|
|
10463
|
+
const fn = async (model, request) => {
|
|
10464
|
+
if (!request.user)
|
|
12308
10465
|
return false;
|
|
12309
|
-
return model[userIdField] ===
|
|
10466
|
+
return model[userIdField] === request.user.id;
|
|
12310
10467
|
};
|
|
12311
10468
|
return mock4(fn);
|
|
12312
10469
|
},
|
|
12313
10470
|
belongsToUser: () => {
|
|
12314
|
-
const fn = async (model,
|
|
12315
|
-
if (!
|
|
10471
|
+
const fn = async (model, request) => {
|
|
10472
|
+
if (!request.user)
|
|
12316
10473
|
return false;
|
|
12317
|
-
return model.userId ===
|
|
10474
|
+
return model.userId === request.user.id;
|
|
12318
10475
|
};
|
|
12319
10476
|
return mock4(fn);
|
|
12320
10477
|
},
|
|
@@ -12334,9 +10491,9 @@ var constraintHelpers = {
|
|
|
12334
10491
|
return mock4(constraintFn);
|
|
12335
10492
|
},
|
|
12336
10493
|
combine: (...constraints) => {
|
|
12337
|
-
const fn = async (model,
|
|
10494
|
+
const fn = async (model, request) => {
|
|
12338
10495
|
for (const constraint of constraints) {
|
|
12339
|
-
if (!await constraint(model,
|
|
10496
|
+
if (!await constraint(model, request)) {
|
|
12340
10497
|
return false;
|
|
12341
10498
|
}
|
|
12342
10499
|
}
|
|
@@ -12393,24 +10550,24 @@ class ModelResolverTester {
|
|
|
12393
10550
|
}
|
|
12394
10551
|
var routeModelHelpers = {
|
|
12395
10552
|
testRoute: async (path, params, modelBindings) => {
|
|
12396
|
-
const
|
|
12397
|
-
|
|
10553
|
+
const request = createMockRequest("GET", path);
|
|
10554
|
+
request.params = params;
|
|
12398
10555
|
for (const [key, model] of Object.entries(modelBindings)) {
|
|
12399
|
-
|
|
12400
|
-
|
|
10556
|
+
request.context = request.context || {};
|
|
10557
|
+
request.context[key] = model;
|
|
12401
10558
|
}
|
|
12402
|
-
return
|
|
10559
|
+
return request;
|
|
12403
10560
|
},
|
|
12404
10561
|
createHandler: (expectedModels) => {
|
|
12405
|
-
const fn = async (
|
|
12406
|
-
const context =
|
|
10562
|
+
const fn = async (request) => {
|
|
10563
|
+
const context = request.context || {};
|
|
12407
10564
|
for (const modelKey of expectedModels) {
|
|
12408
10565
|
if (!context[modelKey]) {
|
|
12409
10566
|
return new Response(`Model ${modelKey} not found`, { status: 404 });
|
|
12410
10567
|
}
|
|
12411
10568
|
}
|
|
12412
10569
|
return new Response(JSON.stringify({
|
|
12413
|
-
params:
|
|
10570
|
+
params: request.params,
|
|
12414
10571
|
models: Object.fromEntries(expectedModels.map((key) => [key, context[key]]))
|
|
12415
10572
|
}), {
|
|
12416
10573
|
headers: { "Content-Type": "application/json" }
|
|
@@ -12419,16 +10576,16 @@ var routeModelHelpers = {
|
|
|
12419
10576
|
return mock4(fn);
|
|
12420
10577
|
},
|
|
12421
10578
|
testBindingMiddleware: (bindings) => {
|
|
12422
|
-
const fn = async (
|
|
12423
|
-
|
|
10579
|
+
const fn = async (request, next) => {
|
|
10580
|
+
request.context = request.context || {};
|
|
12424
10581
|
for (const [key, resolver] of Object.entries(bindings)) {
|
|
12425
|
-
const paramValue =
|
|
10582
|
+
const paramValue = request.params[key];
|
|
12426
10583
|
if (paramValue) {
|
|
12427
10584
|
try {
|
|
12428
10585
|
if (typeof resolver === "function") {
|
|
12429
|
-
|
|
10586
|
+
request.context[key] = await resolver(paramValue);
|
|
12430
10587
|
} else {
|
|
12431
|
-
|
|
10588
|
+
request.context[key] = resolver;
|
|
12432
10589
|
}
|
|
12433
10590
|
} catch (error) {
|
|
12434
10591
|
console.error(`Error resolving model ${key}:`, error);
|
|
@@ -12463,9 +10620,10 @@ var scopedBindingHelpers = {
|
|
|
12463
10620
|
return requiredPermissions.every((permission) => user.permissions.includes(permission));
|
|
12464
10621
|
}
|
|
12465
10622
|
};
|
|
10623
|
+
|
|
12466
10624
|
// src/testing/performance-testing.ts
|
|
12467
10625
|
import { mock as mock5 } from "bun:test";
|
|
12468
|
-
import
|
|
10626
|
+
import process4 from "process";
|
|
12469
10627
|
class PerformanceTester {
|
|
12470
10628
|
metrics = [];
|
|
12471
10629
|
startTime = 0;
|
|
@@ -12584,10 +10742,10 @@ class LoadTester {
|
|
|
12584
10742
|
var benchmarkTests = {
|
|
12585
10743
|
routeHandler: async (handler, iterations = 1000) => {
|
|
12586
10744
|
const times = [];
|
|
12587
|
-
const
|
|
10745
|
+
const request = createMockRequest();
|
|
12588
10746
|
for (let i = 0;i < iterations; i++) {
|
|
12589
10747
|
const start = performance.now();
|
|
12590
|
-
await handler(
|
|
10748
|
+
await handler(request);
|
|
12591
10749
|
const end = performance.now();
|
|
12592
10750
|
times.push(end - start);
|
|
12593
10751
|
}
|
|
@@ -12606,11 +10764,11 @@ var benchmarkTests = {
|
|
|
12606
10764
|
},
|
|
12607
10765
|
middleware: async (middleware2, iterations = 1000) => {
|
|
12608
10766
|
const times = [];
|
|
12609
|
-
const
|
|
10767
|
+
const request = createMockRequest();
|
|
12610
10768
|
const next = mock5(async () => new Response("OK"));
|
|
12611
10769
|
for (let i = 0;i < iterations; i++) {
|
|
12612
10770
|
const start = performance.now();
|
|
12613
|
-
await middleware2(
|
|
10771
|
+
await middleware2(request, next);
|
|
12614
10772
|
const end = performance.now();
|
|
12615
10773
|
times.push(end - start);
|
|
12616
10774
|
}
|
|
@@ -12683,11 +10841,11 @@ var memoryTests = {
|
|
|
12683
10841
|
if (globalThis.gc) {
|
|
12684
10842
|
globalThis.gc();
|
|
12685
10843
|
}
|
|
12686
|
-
const initialHeap =
|
|
10844
|
+
const initialHeap = process4.memoryUsage().heapUsed;
|
|
12687
10845
|
for (let i = 0;i < iterations; i++) {
|
|
12688
10846
|
const result = await fn();
|
|
12689
10847
|
results.push(result);
|
|
12690
|
-
const memUsage =
|
|
10848
|
+
const memUsage = process4.memoryUsage();
|
|
12691
10849
|
heapUsages.push(memUsage.heapUsed);
|
|
12692
10850
|
if (i % 10 === 0 && globalThis.gc) {
|
|
12693
10851
|
globalThis.gc();
|
|
@@ -12696,7 +10854,7 @@ var memoryTests = {
|
|
|
12696
10854
|
if (globalThis.gc) {
|
|
12697
10855
|
globalThis.gc();
|
|
12698
10856
|
}
|
|
12699
|
-
const finalHeap =
|
|
10857
|
+
const finalHeap = process4.memoryUsage().heapUsed;
|
|
12700
10858
|
const averageHeapUsed = heapUsages.reduce((sum, usage) => sum + usage, 0) / heapUsages.length;
|
|
12701
10859
|
const maxHeapUsed = Math.max(...heapUsages);
|
|
12702
10860
|
const memoryLeakDetected = finalHeap - initialHeap > 1024 * 1024;
|
|
@@ -12710,17 +10868,17 @@ var memoryTests = {
|
|
|
12710
10868
|
testRouteHandlerMemoryLeaks: async (handler, iterations = 1000) => {
|
|
12711
10869
|
if (globalThis.gc)
|
|
12712
10870
|
globalThis.gc();
|
|
12713
|
-
const initialMemory =
|
|
10871
|
+
const initialMemory = process4.memoryUsage().heapUsed;
|
|
12714
10872
|
for (let i = 0;i < iterations; i++) {
|
|
12715
|
-
const
|
|
12716
|
-
await handler(
|
|
10873
|
+
const request = createMockRequest();
|
|
10874
|
+
await handler(request);
|
|
12717
10875
|
if (i % 100 === 0 && globalThis.gc) {
|
|
12718
10876
|
globalThis.gc();
|
|
12719
10877
|
}
|
|
12720
10878
|
}
|
|
12721
10879
|
if (globalThis.gc)
|
|
12722
10880
|
globalThis.gc();
|
|
12723
|
-
const finalMemory =
|
|
10881
|
+
const finalMemory = process4.memoryUsage().heapUsed;
|
|
12724
10882
|
const memoryGrowth = finalMemory - initialMemory;
|
|
12725
10883
|
const memoryLeakDetected = memoryGrowth > 5 * 1024 * 1024;
|
|
12726
10884
|
return {
|
|
@@ -12812,7 +10970,7 @@ function createLoadTester(client, config2) {
|
|
|
12812
10970
|
}
|
|
12813
10971
|
var performanceUtils = {
|
|
12814
10972
|
getCurrentMemoryUsage: () => {
|
|
12815
|
-
const usage =
|
|
10973
|
+
const usage = process4.memoryUsage();
|
|
12816
10974
|
return {
|
|
12817
10975
|
heapUsed: usage.heapUsed,
|
|
12818
10976
|
heapTotal: usage.heapTotal,
|
|
@@ -12821,7 +10979,7 @@ var performanceUtils = {
|
|
|
12821
10979
|
};
|
|
12822
10980
|
},
|
|
12823
10981
|
getCPUUsage: () => {
|
|
12824
|
-
const usage =
|
|
10982
|
+
const usage = process4.cpuUsage();
|
|
12825
10983
|
return {
|
|
12826
10984
|
user: usage.user,
|
|
12827
10985
|
system: usage.system
|
|
@@ -12836,6 +10994,7 @@ var performanceUtils = {
|
|
|
12836
10994
|
return JSON.stringify(results, null, 2);
|
|
12837
10995
|
}
|
|
12838
10996
|
};
|
|
10997
|
+
|
|
12839
10998
|
// src/testing/test-response.ts
|
|
12840
10999
|
class TestResponseWrapper {
|
|
12841
11000
|
status;
|
|
@@ -12956,14 +11115,14 @@ class TestResponseWrapper {
|
|
|
12956
11115
|
}
|
|
12957
11116
|
return this;
|
|
12958
11117
|
}
|
|
12959
|
-
expectRedirect(
|
|
11118
|
+
expectRedirect(url) {
|
|
12960
11119
|
if (this.status < 300 || this.status >= 400) {
|
|
12961
11120
|
throw new Error(`Expected redirect status (3xx), got ${this.status}`);
|
|
12962
11121
|
}
|
|
12963
|
-
if (
|
|
11122
|
+
if (url) {
|
|
12964
11123
|
const location = this.headers.location;
|
|
12965
|
-
if (location !==
|
|
12966
|
-
throw new Error(`Expected redirect to '${
|
|
11124
|
+
if (location !== url) {
|
|
11125
|
+
throw new Error(`Expected redirect to '${url}', got '${location}'`);
|
|
12967
11126
|
}
|
|
12968
11127
|
}
|
|
12969
11128
|
return this;
|
|
@@ -13106,24 +11265,24 @@ class TestClient {
|
|
|
13106
11265
|
async request(method, path, options = {}) {
|
|
13107
11266
|
const headers = { ...this.defaultHeaders, ...options.headers };
|
|
13108
11267
|
const requestOptions = { ...options, headers };
|
|
13109
|
-
const
|
|
11268
|
+
const request = new TestRequestBuilder(method, path, requestOptions).build();
|
|
13110
11269
|
try {
|
|
13111
|
-
const response2 = await this.handleRequest(
|
|
11270
|
+
const response2 = await this.handleRequest(request);
|
|
13112
11271
|
return await createTestResponse(response2);
|
|
13113
11272
|
} catch (error) {
|
|
13114
11273
|
throw new Error(`Test request failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
13115
11274
|
}
|
|
13116
11275
|
}
|
|
13117
|
-
async handleRequest(
|
|
13118
|
-
const
|
|
13119
|
-
const matchResult = this.findMatchingRoute(
|
|
11276
|
+
async handleRequest(request) {
|
|
11277
|
+
const url = new URL(request.url);
|
|
11278
|
+
const matchResult = this.findMatchingRoute(request.method, url.pathname);
|
|
13120
11279
|
if (!matchResult) {
|
|
13121
11280
|
return new Response("Not Found", { status: 404 });
|
|
13122
11281
|
}
|
|
13123
11282
|
const { route, params } = matchResult;
|
|
13124
|
-
|
|
11283
|
+
request.params = { ...request.params, ...params };
|
|
13125
11284
|
try {
|
|
13126
|
-
const response2 = await this.executeMiddlewareChain(
|
|
11285
|
+
const response2 = await this.executeMiddlewareChain(request, route);
|
|
13127
11286
|
return response2 || new Response("No response", { status: 500 });
|
|
13128
11287
|
} catch (error) {
|
|
13129
11288
|
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal Server Error" }), {
|
|
@@ -13163,28 +11322,28 @@ class TestClient {
|
|
|
13163
11322
|
}
|
|
13164
11323
|
return params;
|
|
13165
11324
|
}
|
|
13166
|
-
async executeMiddlewareChain(
|
|
11325
|
+
async executeMiddlewareChain(request, route) {
|
|
13167
11326
|
const middlewares = [...this.router.globalMiddleware, ...route.middleware];
|
|
13168
11327
|
let index = 0;
|
|
13169
11328
|
const next = async () => {
|
|
13170
11329
|
if (index < middlewares.length) {
|
|
13171
11330
|
const middleware2 = middlewares[index++];
|
|
13172
|
-
return await middleware2(
|
|
11331
|
+
return await middleware2(request, next);
|
|
13173
11332
|
} else {
|
|
13174
|
-
return await this.executeHandler(route.handler,
|
|
11333
|
+
return await this.executeHandler(route.handler, request);
|
|
13175
11334
|
}
|
|
13176
11335
|
};
|
|
13177
11336
|
const result = await next();
|
|
13178
11337
|
return result || new Response("No response", { status: 500 });
|
|
13179
11338
|
}
|
|
13180
|
-
async executeHandler(handler,
|
|
11339
|
+
async executeHandler(handler, request) {
|
|
13181
11340
|
if (typeof handler === "function") {
|
|
13182
|
-
return await handler(
|
|
11341
|
+
return await handler(request);
|
|
13183
11342
|
} else if (typeof handler === "string") {
|
|
13184
11343
|
throw new TypeError(`String handlers not supported in test environment: ${handler}`);
|
|
13185
11344
|
} else if (typeof handler === "object" && handler.handle) {
|
|
13186
11345
|
const instance = new handler;
|
|
13187
|
-
return await instance.handle(
|
|
11346
|
+
return await instance.handle(request);
|
|
13188
11347
|
}
|
|
13189
11348
|
throw new Error("Invalid handler type");
|
|
13190
11349
|
}
|
|
@@ -13225,6 +11384,7 @@ var testClient = {
|
|
|
13225
11384
|
withAuth: (token, type = "Bearer") => (router2) => createTestClient(router2).auth(token, type),
|
|
13226
11385
|
withHeaders: (headers) => (router2) => createTestClient(router2).withHeaders(headers)
|
|
13227
11386
|
};
|
|
11387
|
+
|
|
13228
11388
|
// src/testing/websocket-testing.ts
|
|
13229
11389
|
import { mock as mock6 } from "bun:test";
|
|
13230
11390
|
|
|
@@ -13579,13 +11739,13 @@ export {
|
|
|
13579
11739
|
spyOnMiddleware,
|
|
13580
11740
|
singleFileUpload,
|
|
13581
11741
|
signRequest,
|
|
13582
|
-
setCurrentRequest,
|
|
11742
|
+
setCurrentRequest2 as setCurrentRequest,
|
|
13583
11743
|
setContainer,
|
|
13584
11744
|
session,
|
|
13585
11745
|
securitySuite,
|
|
13586
11746
|
security,
|
|
13587
11747
|
scopedBindingHelpers,
|
|
13588
|
-
runWithRequest,
|
|
11748
|
+
runWithRequest2 as runWithRequest,
|
|
13589
11749
|
rule,
|
|
13590
11750
|
router,
|
|
13591
11751
|
routeModelHelpers,
|
|
@@ -13598,7 +11758,7 @@ export {
|
|
|
13598
11758
|
requestSigning,
|
|
13599
11759
|
requestId,
|
|
13600
11760
|
request,
|
|
13601
|
-
registerNamedRoute,
|
|
11761
|
+
registerNamedRoute2 as registerNamedRoute,
|
|
13602
11762
|
rateLimitRegistry,
|
|
13603
11763
|
rateLimit,
|
|
13604
11764
|
processHtmlTemplate,
|
|
@@ -13628,7 +11788,7 @@ export {
|
|
|
13628
11788
|
globalValidator,
|
|
13629
11789
|
globalMiddlewarePipeline,
|
|
13630
11790
|
getNamedRoutes,
|
|
13631
|
-
getNamedRoutePath,
|
|
11791
|
+
getNamedRoutePath2 as getNamedRoutePath,
|
|
13632
11792
|
getCurrentRequest,
|
|
13633
11793
|
getContainer,
|
|
13634
11794
|
getConfig,
|
|
@@ -13759,7 +11919,7 @@ export {
|
|
|
13759
11919
|
FileUpload,
|
|
13760
11920
|
FileSessionStore,
|
|
13761
11921
|
FileSecurity,
|
|
13762
|
-
|
|
11922
|
+
Errors,
|
|
13763
11923
|
EnvironmentManager,
|
|
13764
11924
|
EnhancementPresets,
|
|
13765
11925
|
DomainMatcher,
|
|
@@ -13779,6 +11939,6 @@ export {
|
|
|
13779
11939
|
BuiltInRules,
|
|
13780
11940
|
BindingBuilder,
|
|
13781
11941
|
AuthTester,
|
|
13782
|
-
|
|
11942
|
+
default2 as Auth,
|
|
13783
11943
|
ApiKeyManager
|
|
13784
11944
|
};
|