bxo 0.0.2 → 0.0.4
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/index.ts +26 -34
- package/package.json +1 -1
- package/plugins/auth.ts +68 -65
- package/plugins/cors.ts +44 -40
- package/plugins/index.ts +5 -7
- package/plugins/logger.ts +88 -83
- package/plugins/ratelimit.ts +61 -57
package/index.ts
CHANGED
@@ -30,16 +30,6 @@ export type Context<TConfig extends RouteConfig = {}> = {
|
|
30
30
|
// Handler function type
|
31
31
|
type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
|
32
32
|
|
33
|
-
// Plugin interface (also exported from plugins/index.ts)
|
34
|
-
interface Plugin {
|
35
|
-
name?: string;
|
36
|
-
onRequest?: (ctx: Context) => Promise<void> | void;
|
37
|
-
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
38
|
-
onError?: (ctx: Context, error: Error) => Promise<any> | any;
|
39
|
-
}
|
40
|
-
|
41
|
-
|
42
|
-
|
43
33
|
// Route definition
|
44
34
|
interface Route {
|
45
35
|
method: string;
|
@@ -63,14 +53,14 @@ interface LifecycleHooks {
|
|
63
53
|
|
64
54
|
export default class BXO {
|
65
55
|
private routes: Route[] = [];
|
66
|
-
private plugins:
|
56
|
+
private plugins: BXO[] = [];
|
67
57
|
private hooks: LifecycleHooks = {};
|
68
58
|
private server?: any;
|
69
59
|
private isRunning: boolean = false;
|
70
60
|
private hotReloadEnabled: boolean = false;
|
71
61
|
private watchedFiles: Set<string> = new Set();
|
72
62
|
|
73
|
-
constructor() {}
|
63
|
+
constructor() { }
|
74
64
|
|
75
65
|
// Lifecycle hook methods
|
76
66
|
onBeforeStart(handler: () => Promise<void> | void): this {
|
@@ -118,9 +108,9 @@ export default class BXO {
|
|
118
108
|
return this;
|
119
109
|
}
|
120
110
|
|
121
|
-
// Plugin system
|
122
|
-
use(
|
123
|
-
this.plugins.push(
|
111
|
+
// Plugin system - now accepts other BXO instances
|
112
|
+
use(bxoInstance: BXO): this {
|
113
|
+
this.plugins.push(bxoInstance);
|
124
114
|
return this;
|
125
115
|
}
|
126
116
|
|
@@ -326,10 +316,10 @@ export default class BXO {
|
|
326
316
|
await this.hooks.onRequest(ctx);
|
327
317
|
}
|
328
318
|
|
329
|
-
// Run
|
330
|
-
for (const
|
331
|
-
if (
|
332
|
-
await
|
319
|
+
// Run BXO instance onRequest hooks
|
320
|
+
for (const bxoInstance of this.plugins) {
|
321
|
+
if (bxoInstance.hooks.onRequest) {
|
322
|
+
await bxoInstance.hooks.onRequest(ctx);
|
333
323
|
}
|
334
324
|
}
|
335
325
|
|
@@ -341,10 +331,10 @@ export default class BXO {
|
|
341
331
|
response = await this.hooks.onResponse(ctx, response) || response;
|
342
332
|
}
|
343
333
|
|
344
|
-
// Run
|
345
|
-
for (const
|
346
|
-
if (
|
347
|
-
response = await
|
334
|
+
// Run BXO instance onResponse hooks
|
335
|
+
for (const bxoInstance of this.plugins) {
|
336
|
+
if (bxoInstance.hooks.onResponse) {
|
337
|
+
response = await bxoInstance.hooks.onResponse(ctx, response) || response;
|
348
338
|
}
|
349
339
|
}
|
350
340
|
|
@@ -378,9 +368,9 @@ export default class BXO {
|
|
378
368
|
errorResponse = await this.hooks.onError(ctx, error as Error);
|
379
369
|
}
|
380
370
|
|
381
|
-
for (const
|
382
|
-
if (
|
383
|
-
errorResponse = await
|
371
|
+
for (const bxoInstance of this.plugins) {
|
372
|
+
if (bxoInstance.hooks.onError) {
|
373
|
+
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
|
384
374
|
}
|
385
375
|
}
|
386
376
|
|
@@ -414,7 +404,7 @@ export default class BXO {
|
|
414
404
|
if (!this.hotReloadEnabled) return;
|
415
405
|
|
416
406
|
const fs = require('fs');
|
417
|
-
|
407
|
+
|
418
408
|
for (const watchPath of this.watchedFiles) {
|
419
409
|
try {
|
420
410
|
fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
|
@@ -516,19 +506,19 @@ export default class BXO {
|
|
516
506
|
}
|
517
507
|
|
518
508
|
console.log('🔄 Restarting BXO server...');
|
519
|
-
|
509
|
+
|
520
510
|
await this.stop();
|
521
|
-
|
511
|
+
|
522
512
|
// Small delay to ensure cleanup
|
523
513
|
await new Promise(resolve => setTimeout(resolve, 100));
|
524
|
-
|
514
|
+
|
525
515
|
await this.start(port, hostname);
|
526
516
|
|
527
517
|
// After restart hook
|
528
518
|
if (this.hooks.onAfterRestart) {
|
529
519
|
await this.hooks.onAfterRestart();
|
530
520
|
}
|
531
|
-
|
521
|
+
|
532
522
|
} catch (error) {
|
533
523
|
console.error('❌ Error restarting server:', error);
|
534
524
|
throw error;
|
@@ -554,10 +544,12 @@ export default class BXO {
|
|
554
544
|
}
|
555
545
|
}
|
556
546
|
|
557
|
-
|
558
|
-
|
547
|
+
const error = (error: Error, status: number = 500) => {
|
548
|
+
return new Response(JSON.stringify({ error: error.message }), { status });
|
549
|
+
}
|
559
550
|
|
560
|
-
|
551
|
+
// Export Zod for convenience
|
552
|
+
export { z, error };
|
561
553
|
|
562
554
|
// Export types for external use
|
563
555
|
export type { RouteConfig };
|
package/package.json
CHANGED
package/plugins/auth.ts
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import BXO from '../index';
|
2
|
+
|
1
3
|
interface AuthOptions {
|
2
4
|
type: 'jwt' | 'bearer' | 'apikey';
|
3
5
|
secret?: string;
|
@@ -6,7 +8,7 @@ interface AuthOptions {
|
|
6
8
|
exclude?: string[];
|
7
9
|
}
|
8
10
|
|
9
|
-
export function auth(options: AuthOptions) {
|
11
|
+
export function auth(options: AuthOptions): BXO {
|
10
12
|
const {
|
11
13
|
type,
|
12
14
|
secret,
|
@@ -15,84 +17,85 @@ export function auth(options: AuthOptions) {
|
|
15
17
|
exclude = []
|
16
18
|
} = options;
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
}
|
30
|
-
return pathname === path || pathname.startsWith(path);
|
31
|
-
})) {
|
32
|
-
return;
|
20
|
+
const authInstance = new BXO();
|
21
|
+
|
22
|
+
authInstance.onRequest(async (ctx: any) => {
|
23
|
+
const url = new URL(ctx.request.url);
|
24
|
+
const pathname = url.pathname;
|
25
|
+
|
26
|
+
// Skip auth for excluded paths
|
27
|
+
if (exclude.some(path => {
|
28
|
+
if (path.includes('*')) {
|
29
|
+
const regex = new RegExp(path.replace(/\*/g, '.*'));
|
30
|
+
return regex.test(pathname);
|
33
31
|
}
|
32
|
+
return pathname === path || pathname.startsWith(path);
|
33
|
+
})) {
|
34
|
+
return;
|
35
|
+
}
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
const authHeader = ctx.request.headers.get(header.toLowerCase());
|
38
|
+
|
39
|
+
if (!authHeader) {
|
40
|
+
throw new Response(JSON.stringify({ error: 'Authorization header required' }), {
|
41
|
+
status: 401,
|
42
|
+
headers: { 'Content-Type': 'application/json' }
|
43
|
+
});
|
44
|
+
}
|
45
|
+
|
46
|
+
let token: string;
|
47
|
+
|
48
|
+
if (type === 'jwt' || type === 'bearer') {
|
49
|
+
if (!authHeader.startsWith('Bearer ')) {
|
50
|
+
throw new Response(JSON.stringify({ error: 'Invalid authorization format. Use Bearer <token>' }), {
|
39
51
|
status: 401,
|
40
52
|
headers: { 'Content-Type': 'application/json' }
|
41
53
|
});
|
42
54
|
}
|
55
|
+
token = authHeader.slice(7);
|
56
|
+
} else if (type === 'apikey') {
|
57
|
+
token = authHeader;
|
58
|
+
} else {
|
59
|
+
token = authHeader;
|
60
|
+
}
|
43
61
|
|
44
|
-
|
62
|
+
try {
|
63
|
+
let user: any;
|
45
64
|
|
46
|
-
if (
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
65
|
+
if (verify) {
|
66
|
+
user = await verify(token, ctx);
|
67
|
+
} else if (type === 'jwt' && secret) {
|
68
|
+
// Simple JWT verification (in production, use a proper JWT library)
|
69
|
+
const [headerB64, payloadB64, signature] = token.split('.');
|
70
|
+
if (!headerB64 || !payloadB64 || !signature) {
|
71
|
+
throw new Error('Invalid JWT format');
|
52
72
|
}
|
53
|
-
token = authHeader.slice(7);
|
54
|
-
} else if (type === 'apikey') {
|
55
|
-
token = authHeader;
|
56
|
-
} else {
|
57
|
-
token = authHeader;
|
58
|
-
}
|
59
|
-
|
60
|
-
try {
|
61
|
-
let user: any;
|
62
73
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
if (!headerB64 || !payloadB64 || !signature) {
|
69
|
-
throw new Error('Invalid JWT format');
|
70
|
-
}
|
71
|
-
|
72
|
-
const payload = JSON.parse(atob(payloadB64));
|
73
|
-
|
74
|
-
// Check expiration
|
75
|
-
if (payload.exp && Date.now() >= payload.exp * 1000) {
|
76
|
-
throw new Error('Token expired');
|
77
|
-
}
|
78
|
-
|
79
|
-
user = payload;
|
80
|
-
} else {
|
81
|
-
user = { token };
|
74
|
+
const payload = JSON.parse(atob(payloadB64));
|
75
|
+
|
76
|
+
// Check expiration
|
77
|
+
if (payload.exp && Date.now() >= payload.exp * 1000) {
|
78
|
+
throw new Error('Token expired');
|
82
79
|
}
|
83
|
-
|
84
|
-
// Attach user to context
|
85
|
-
ctx.user = user;
|
86
80
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
status: 401,
|
91
|
-
headers: { 'Content-Type': 'application/json' }
|
92
|
-
});
|
81
|
+
user = payload;
|
82
|
+
} else {
|
83
|
+
user = { token };
|
93
84
|
}
|
85
|
+
|
86
|
+
// Attach user to context
|
87
|
+
ctx.user = user;
|
88
|
+
|
89
|
+
} catch (error) {
|
90
|
+
const message = error instanceof Error ? error.message : 'Invalid token';
|
91
|
+
throw new Response(JSON.stringify({ error: message }), {
|
92
|
+
status: 401,
|
93
|
+
headers: { 'Content-Type': 'application/json' }
|
94
|
+
});
|
94
95
|
}
|
95
|
-
};
|
96
|
+
});
|
97
|
+
|
98
|
+
return authInstance;
|
96
99
|
}
|
97
100
|
|
98
101
|
// Helper function for creating JWT tokens (simple implementation)
|
package/plugins/cors.ts
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import BXO from '../index';
|
2
|
+
|
1
3
|
interface CORSOptions {
|
2
4
|
origin?: string | string[] | boolean;
|
3
5
|
methods?: string[];
|
@@ -6,7 +8,7 @@ interface CORSOptions {
|
|
6
8
|
maxAge?: number;
|
7
9
|
}
|
8
10
|
|
9
|
-
export function cors(options: CORSOptions = {}) {
|
11
|
+
export function cors(options: CORSOptions = {}): BXO {
|
10
12
|
const {
|
11
13
|
origin = '*',
|
12
14
|
methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
@@ -15,46 +17,14 @@ export function cors(options: CORSOptions = {}) {
|
|
15
17
|
maxAge = 86400
|
16
18
|
} = options;
|
17
19
|
|
18
|
-
|
19
|
-
name: 'cors',
|
20
|
-
onRequest: async (ctx: any) => {
|
21
|
-
// Handle preflight OPTIONS request
|
22
|
-
if (ctx.request.method === 'OPTIONS') {
|
23
|
-
const headers: Record<string, string> = {};
|
24
|
-
|
25
|
-
// Handle origin
|
26
|
-
if (typeof origin === 'boolean') {
|
27
|
-
if (origin) {
|
28
|
-
headers['Access-Control-Allow-Origin'] = ctx.request.headers.get('origin') || '*';
|
29
|
-
}
|
30
|
-
} else if (typeof origin === 'string') {
|
31
|
-
headers['Access-Control-Allow-Origin'] = origin;
|
32
|
-
} else if (Array.isArray(origin)) {
|
33
|
-
const requestOrigin = ctx.request.headers.get('origin');
|
34
|
-
if (requestOrigin && origin.includes(requestOrigin)) {
|
35
|
-
headers['Access-Control-Allow-Origin'] = requestOrigin;
|
36
|
-
}
|
37
|
-
}
|
38
|
-
|
39
|
-
headers['Access-Control-Allow-Methods'] = methods.join(', ');
|
40
|
-
headers['Access-Control-Allow-Headers'] = allowedHeaders.join(', ');
|
41
|
-
|
42
|
-
if (credentials) {
|
43
|
-
headers['Access-Control-Allow-Credentials'] = 'true';
|
44
|
-
}
|
45
|
-
|
46
|
-
headers['Access-Control-Max-Age'] = maxAge.toString();
|
20
|
+
const corsInstance = new BXO();
|
47
21
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
throw new Response(null, { status: 204, headers });
|
52
|
-
}
|
53
|
-
},
|
54
|
-
onResponse: async (ctx: any, response: any) => {
|
22
|
+
corsInstance.onRequest(async (ctx: any) => {
|
23
|
+
// Handle preflight OPTIONS request
|
24
|
+
if (ctx.request.method === 'OPTIONS') {
|
55
25
|
const headers: Record<string, string> = {};
|
56
26
|
|
57
|
-
// Handle origin
|
27
|
+
// Handle origin
|
58
28
|
if (typeof origin === 'boolean') {
|
59
29
|
if (origin) {
|
60
30
|
headers['Access-Control-Allow-Origin'] = ctx.request.headers.get('origin') || '*';
|
@@ -68,12 +38,46 @@ export function cors(options: CORSOptions = {}) {
|
|
68
38
|
}
|
69
39
|
}
|
70
40
|
|
41
|
+
headers['Access-Control-Allow-Methods'] = methods.join(', ');
|
42
|
+
headers['Access-Control-Allow-Headers'] = allowedHeaders.join(', ');
|
43
|
+
|
71
44
|
if (credentials) {
|
72
45
|
headers['Access-Control-Allow-Credentials'] = 'true';
|
73
46
|
}
|
47
|
+
|
48
|
+
headers['Access-Control-Max-Age'] = maxAge.toString();
|
74
49
|
|
50
|
+
ctx.set.status = 204;
|
75
51
|
ctx.set.headers = { ...ctx.set.headers, ...headers };
|
76
|
-
|
52
|
+
|
53
|
+
throw new Response(null, { status: 204, headers });
|
77
54
|
}
|
78
|
-
};
|
55
|
+
});
|
56
|
+
|
57
|
+
corsInstance.onResponse(async (ctx: any, response: any) => {
|
58
|
+
const headers: Record<string, string> = {};
|
59
|
+
|
60
|
+
// Handle origin for actual requests
|
61
|
+
if (typeof origin === 'boolean') {
|
62
|
+
if (origin) {
|
63
|
+
headers['Access-Control-Allow-Origin'] = ctx.request.headers.get('origin') || '*';
|
64
|
+
}
|
65
|
+
} else if (typeof origin === 'string') {
|
66
|
+
headers['Access-Control-Allow-Origin'] = origin;
|
67
|
+
} else if (Array.isArray(origin)) {
|
68
|
+
const requestOrigin = ctx.request.headers.get('origin');
|
69
|
+
if (requestOrigin && origin.includes(requestOrigin)) {
|
70
|
+
headers['Access-Control-Allow-Origin'] = requestOrigin;
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
if (credentials) {
|
75
|
+
headers['Access-Control-Allow-Credentials'] = 'true';
|
76
|
+
}
|
77
|
+
|
78
|
+
ctx.set.headers = { ...ctx.set.headers, ...headers };
|
79
|
+
return response;
|
80
|
+
});
|
81
|
+
|
82
|
+
return corsInstance;
|
79
83
|
}
|
package/plugins/index.ts
CHANGED
@@ -4,10 +4,8 @@ export { logger } from './logger';
|
|
4
4
|
export { auth, createJWT } from './auth';
|
5
5
|
export { rateLimit } from './ratelimit';
|
6
6
|
|
7
|
-
//
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
onError?: (ctx: any, error: Error) => Promise<any> | any;
|
13
|
-
}
|
7
|
+
// Import BXO for plugin typing
|
8
|
+
import BXO from '../index';
|
9
|
+
|
10
|
+
// Plugin functions now return BXO instances
|
11
|
+
export type PluginFactory<T = any> = (options?: T) => BXO;
|
package/plugins/logger.ts
CHANGED
@@ -1,104 +1,109 @@
|
|
1
|
+
import BXO from '../index';
|
2
|
+
|
1
3
|
interface LoggerOptions {
|
2
4
|
format?: 'simple' | 'detailed' | 'json';
|
3
5
|
includeBody?: boolean;
|
4
6
|
includeHeaders?: boolean;
|
5
7
|
}
|
6
8
|
|
7
|
-
export function logger(options: LoggerOptions = {}) {
|
9
|
+
export function logger(options: LoggerOptions = {}): BXO {
|
8
10
|
const {
|
9
11
|
format = 'simple',
|
10
12
|
includeBody = false,
|
11
13
|
includeHeaders = false
|
12
14
|
} = options;
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
const loggerInstance = new BXO();
|
17
|
+
|
18
|
+
loggerInstance.onRequest(async (ctx: any) => {
|
19
|
+
ctx._startTime = Date.now();
|
20
|
+
|
21
|
+
if (format === 'json') {
|
22
|
+
const logData: any = {
|
23
|
+
timestamp: new Date().toISOString(),
|
24
|
+
method: ctx.request.method,
|
25
|
+
url: ctx.request.url,
|
26
|
+
type: 'request'
|
27
|
+
};
|
28
|
+
|
29
|
+
if (includeHeaders) {
|
30
|
+
logData.headers = Object.fromEntries(ctx.request.headers.entries());
|
31
|
+
}
|
18
32
|
|
19
|
-
if (
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
if (includeBody && ctx.body) {
|
32
|
-
logData.body = ctx.body;
|
33
|
-
}
|
34
|
-
|
35
|
-
console.log(JSON.stringify(logData));
|
36
|
-
} else if (format === 'detailed') {
|
37
|
-
console.log(`→ ${ctx.request.method} ${ctx.request.url}`);
|
38
|
-
if (includeHeaders) {
|
39
|
-
console.log(' Headers:', Object.fromEntries(ctx.request.headers.entries()));
|
40
|
-
}
|
41
|
-
if (includeBody && ctx.body) {
|
42
|
-
console.log(' Body:', ctx.body);
|
43
|
-
}
|
44
|
-
} else {
|
45
|
-
console.log(`→ ${ctx.request.method} ${ctx.request.url}`);
|
33
|
+
if (includeBody && ctx.body) {
|
34
|
+
logData.body = ctx.body;
|
35
|
+
}
|
36
|
+
|
37
|
+
console.log(JSON.stringify(logData));
|
38
|
+
} else if (format === 'detailed') {
|
39
|
+
console.log(`→ ${ctx.request.method} ${ctx.request.url}`);
|
40
|
+
if (includeHeaders) {
|
41
|
+
console.log(' Headers:', Object.fromEntries(ctx.request.headers.entries()));
|
42
|
+
}
|
43
|
+
if (includeBody && ctx.body) {
|
44
|
+
console.log(' Body:', ctx.body);
|
46
45
|
}
|
47
|
-
}
|
48
|
-
|
49
|
-
|
50
|
-
|
46
|
+
} else {
|
47
|
+
console.log(`→ ${ctx.request.method} ${ctx.request.url}`);
|
48
|
+
}
|
49
|
+
});
|
50
|
+
|
51
|
+
loggerInstance.onResponse(async (ctx: any, response: any) => {
|
52
|
+
const duration = Date.now() - (ctx._startTime || 0);
|
53
|
+
const status = ctx.set.status || 200;
|
54
|
+
|
55
|
+
if (format === 'json') {
|
56
|
+
const logData: any = {
|
57
|
+
timestamp: new Date().toISOString(),
|
58
|
+
method: ctx.request.method,
|
59
|
+
url: ctx.request.url,
|
60
|
+
status,
|
61
|
+
duration: `${duration}ms`,
|
62
|
+
type: 'response'
|
63
|
+
};
|
51
64
|
|
52
|
-
if (
|
53
|
-
|
54
|
-
timestamp: new Date().toISOString(),
|
55
|
-
method: ctx.request.method,
|
56
|
-
url: ctx.request.url,
|
57
|
-
status,
|
58
|
-
duration: `${duration}ms`,
|
59
|
-
type: 'response'
|
60
|
-
};
|
61
|
-
|
62
|
-
if (includeHeaders && ctx.set.headers) {
|
63
|
-
logData.responseHeaders = ctx.set.headers;
|
64
|
-
}
|
65
|
-
|
66
|
-
if (includeBody && response) {
|
67
|
-
logData.response = response;
|
68
|
-
}
|
69
|
-
|
70
|
-
console.log(JSON.stringify(logData));
|
71
|
-
} else if (format === 'detailed') {
|
72
|
-
console.log(`← ${ctx.request.method} ${ctx.request.url} ${status} ${duration}ms`);
|
73
|
-
if (includeHeaders && ctx.set.headers) {
|
74
|
-
console.log(' Response Headers:', ctx.set.headers);
|
75
|
-
}
|
76
|
-
if (includeBody && response) {
|
77
|
-
console.log(' Response:', response);
|
78
|
-
}
|
79
|
-
} else {
|
80
|
-
const statusColor = status >= 400 ? '\x1b[31m' : status >= 300 ? '\x1b[33m' : '\x1b[32m';
|
81
|
-
const resetColor = '\x1b[0m';
|
82
|
-
console.log(`← ${ctx.request.method} ${ctx.request.url} ${statusColor}${status}${resetColor} ${duration}ms`);
|
65
|
+
if (includeHeaders && ctx.set.headers) {
|
66
|
+
logData.responseHeaders = ctx.set.headers;
|
83
67
|
}
|
84
68
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
const duration = Date.now() - (ctx._startTime || 0);
|
69
|
+
if (includeBody && response) {
|
70
|
+
logData.response = response;
|
71
|
+
}
|
89
72
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
}));
|
99
|
-
} else {
|
100
|
-
console.log(`✗ ${ctx.request.method} ${ctx.request.url} \x1b[31mERROR\x1b[0m ${duration}ms: ${error.message}`);
|
73
|
+
console.log(JSON.stringify(logData));
|
74
|
+
} else if (format === 'detailed') {
|
75
|
+
console.log(`← ${ctx.request.method} ${ctx.request.url} ${status} ${duration}ms`);
|
76
|
+
if (includeHeaders && ctx.set.headers) {
|
77
|
+
console.log(' Response Headers:', ctx.set.headers);
|
78
|
+
}
|
79
|
+
if (includeBody && response) {
|
80
|
+
console.log(' Response:', response);
|
101
81
|
}
|
82
|
+
} else {
|
83
|
+
const statusColor = status >= 400 ? '\x1b[31m' : status >= 300 ? '\x1b[33m' : '\x1b[32m';
|
84
|
+
const resetColor = '\x1b[0m';
|
85
|
+
console.log(`← ${ctx.request.method} ${ctx.request.url} ${statusColor}${status}${resetColor} ${duration}ms`);
|
102
86
|
}
|
103
|
-
|
87
|
+
|
88
|
+
return response;
|
89
|
+
});
|
90
|
+
|
91
|
+
loggerInstance.onError(async (ctx: any, error: Error) => {
|
92
|
+
const duration = Date.now() - (ctx._startTime || 0);
|
93
|
+
|
94
|
+
if (format === 'json') {
|
95
|
+
console.log(JSON.stringify({
|
96
|
+
timestamp: new Date().toISOString(),
|
97
|
+
method: ctx.request.method,
|
98
|
+
url: ctx.request.url,
|
99
|
+
error: error.message,
|
100
|
+
duration: `${duration}ms`,
|
101
|
+
type: 'error'
|
102
|
+
}));
|
103
|
+
} else {
|
104
|
+
console.log(`✗ ${ctx.request.method} ${ctx.request.url} \x1b[31mERROR\x1b[0m ${duration}ms: ${error.message}`);
|
105
|
+
}
|
106
|
+
});
|
107
|
+
|
108
|
+
return loggerInstance;
|
104
109
|
}
|
package/plugins/ratelimit.ts
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import BXO from '../index';
|
2
|
+
|
1
3
|
interface RateLimitOptions {
|
2
4
|
max: number;
|
3
5
|
window: number; // in seconds
|
@@ -50,7 +52,7 @@ class RateLimitStore {
|
|
50
52
|
}
|
51
53
|
}
|
52
54
|
|
53
|
-
export function rateLimit(options: RateLimitOptions) {
|
55
|
+
export function rateLimit(options: RateLimitOptions): BXO {
|
54
56
|
const {
|
55
57
|
max,
|
56
58
|
window,
|
@@ -72,65 +74,67 @@ export function rateLimit(options: RateLimitOptions) {
|
|
72
74
|
// Cleanup expired entries every 5 minutes
|
73
75
|
setInterval(() => store.cleanup(), 5 * 60 * 1000);
|
74
76
|
|
75
|
-
|
76
|
-
name: 'rateLimit',
|
77
|
-
onRequest: async (ctx: any) => {
|
78
|
-
const url = new URL(ctx.request.url);
|
79
|
-
const pathname = url.pathname;
|
80
|
-
|
81
|
-
// Skip rate limiting for excluded paths
|
82
|
-
if (exclude.some(path => {
|
83
|
-
if (path.includes('*')) {
|
84
|
-
const regex = new RegExp(path.replace(/\*/g, '.*'));
|
85
|
-
return regex.test(pathname);
|
86
|
-
}
|
87
|
-
return pathname === path || pathname.startsWith(path);
|
88
|
-
})) {
|
89
|
-
return;
|
90
|
-
}
|
77
|
+
const rateLimitInstance = new BXO();
|
91
78
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
status: statusCode,
|
102
|
-
headers: {
|
103
|
-
'Content-Type': 'application/json',
|
104
|
-
'X-RateLimit-Limit': max.toString(),
|
105
|
-
'X-RateLimit-Remaining': '0',
|
106
|
-
'X-RateLimit-Reset': resetTime.toString(),
|
107
|
-
'Retry-After': (resetTime - Math.floor(Date.now() / 1000)).toString()
|
108
|
-
}
|
109
|
-
});
|
79
|
+
rateLimitInstance.onRequest(async (ctx: any) => {
|
80
|
+
const url = new URL(ctx.request.url);
|
81
|
+
const pathname = url.pathname;
|
82
|
+
|
83
|
+
// Skip rate limiting for excluded paths
|
84
|
+
if (exclude.some(path => {
|
85
|
+
if (path.includes('*')) {
|
86
|
+
const regex = new RegExp(path.replace(/\*/g, '.*'));
|
87
|
+
return regex.test(pathname);
|
110
88
|
}
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
89
|
+
return pathname === path || pathname.startsWith(path);
|
90
|
+
})) {
|
91
|
+
return;
|
92
|
+
}
|
93
|
+
|
94
|
+
const key = keyGenerator(ctx);
|
95
|
+
const entry = store.increment(key, window);
|
96
|
+
|
97
|
+
if (entry.count > max) {
|
98
|
+
const resetTime = Math.ceil(entry.resetTime / 1000);
|
99
|
+
throw new Response(JSON.stringify({
|
100
|
+
error: message,
|
101
|
+
retryAfter: resetTime - Math.floor(Date.now() / 1000)
|
102
|
+
}), {
|
103
|
+
status: statusCode,
|
104
|
+
headers: {
|
105
|
+
'Content-Type': 'application/json',
|
106
|
+
'X-RateLimit-Limit': max.toString(),
|
107
|
+
'X-RateLimit-Remaining': '0',
|
108
|
+
'X-RateLimit-Reset': resetTime.toString(),
|
109
|
+
'Retry-After': (resetTime - Math.floor(Date.now() / 1000)).toString()
|
130
110
|
}
|
111
|
+
});
|
112
|
+
}
|
113
|
+
|
114
|
+
// Add rate limit headers
|
115
|
+
ctx.set.headers = {
|
116
|
+
...ctx.set.headers,
|
117
|
+
'X-RateLimit-Limit': max.toString(),
|
118
|
+
'X-RateLimit-Remaining': Math.max(0, max - entry.count).toString(),
|
119
|
+
'X-RateLimit-Reset': Math.ceil(entry.resetTime / 1000).toString()
|
120
|
+
};
|
121
|
+
});
|
122
|
+
|
123
|
+
rateLimitInstance.onResponse(async (ctx: any, response: any) => {
|
124
|
+
const status = ctx.set.status || 200;
|
125
|
+
const key = keyGenerator(ctx);
|
126
|
+
|
127
|
+
// Optionally skip counting successful or failed requests
|
128
|
+
if ((skipSuccessful && status < 400) || (skipFailed && status >= 400)) {
|
129
|
+
// Decrement the counter since we don't want to count this request
|
130
|
+
const entry = store.get(key);
|
131
|
+
if (entry && entry.count > 0) {
|
132
|
+
store.set(key, entry.count - 1, entry.resetTime);
|
131
133
|
}
|
132
|
-
|
133
|
-
return response;
|
134
134
|
}
|
135
|
-
|
135
|
+
|
136
|
+
return response;
|
137
|
+
});
|
138
|
+
|
139
|
+
return rateLimitInstance;
|
136
140
|
}
|