bxo 0.0.2 → 0.0.3
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 +28 -30
- 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,13 +30,7 @@ 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
|
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
|
-
}
|
33
|
+
// Note: Plugin interface moved to plugins/index.ts as PluginFactory type
|
40
34
|
|
41
35
|
|
42
36
|
|
@@ -63,14 +57,14 @@ interface LifecycleHooks {
|
|
63
57
|
|
64
58
|
export default class BXO {
|
65
59
|
private routes: Route[] = [];
|
66
|
-
private plugins:
|
60
|
+
private plugins: BXO[] = [];
|
67
61
|
private hooks: LifecycleHooks = {};
|
68
62
|
private server?: any;
|
69
63
|
private isRunning: boolean = false;
|
70
64
|
private hotReloadEnabled: boolean = false;
|
71
65
|
private watchedFiles: Set<string> = new Set();
|
72
66
|
|
73
|
-
constructor() {}
|
67
|
+
constructor() { }
|
74
68
|
|
75
69
|
// Lifecycle hook methods
|
76
70
|
onBeforeStart(handler: () => Promise<void> | void): this {
|
@@ -118,9 +112,9 @@ export default class BXO {
|
|
118
112
|
return this;
|
119
113
|
}
|
120
114
|
|
121
|
-
// Plugin system
|
122
|
-
use(
|
123
|
-
this.plugins.push(
|
115
|
+
// Plugin system - now accepts other BXO instances
|
116
|
+
use(bxoInstance: BXO): this {
|
117
|
+
this.plugins.push(bxoInstance);
|
124
118
|
return this;
|
125
119
|
}
|
126
120
|
|
@@ -326,10 +320,10 @@ export default class BXO {
|
|
326
320
|
await this.hooks.onRequest(ctx);
|
327
321
|
}
|
328
322
|
|
329
|
-
// Run
|
330
|
-
for (const
|
331
|
-
if (
|
332
|
-
await
|
323
|
+
// Run BXO instance onRequest hooks
|
324
|
+
for (const bxoInstance of this.plugins) {
|
325
|
+
if (bxoInstance.hooks.onRequest) {
|
326
|
+
await bxoInstance.hooks.onRequest(ctx);
|
333
327
|
}
|
334
328
|
}
|
335
329
|
|
@@ -341,10 +335,10 @@ export default class BXO {
|
|
341
335
|
response = await this.hooks.onResponse(ctx, response) || response;
|
342
336
|
}
|
343
337
|
|
344
|
-
// Run
|
345
|
-
for (const
|
346
|
-
if (
|
347
|
-
response = await
|
338
|
+
// Run BXO instance onResponse hooks
|
339
|
+
for (const bxoInstance of this.plugins) {
|
340
|
+
if (bxoInstance.hooks.onResponse) {
|
341
|
+
response = await bxoInstance.hooks.onResponse(ctx, response) || response;
|
348
342
|
}
|
349
343
|
}
|
350
344
|
|
@@ -378,9 +372,9 @@ export default class BXO {
|
|
378
372
|
errorResponse = await this.hooks.onError(ctx, error as Error);
|
379
373
|
}
|
380
374
|
|
381
|
-
for (const
|
382
|
-
if (
|
383
|
-
errorResponse = await
|
375
|
+
for (const bxoInstance of this.plugins) {
|
376
|
+
if (bxoInstance.hooks.onError) {
|
377
|
+
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
|
384
378
|
}
|
385
379
|
}
|
386
380
|
|
@@ -414,7 +408,7 @@ export default class BXO {
|
|
414
408
|
if (!this.hotReloadEnabled) return;
|
415
409
|
|
416
410
|
const fs = require('fs');
|
417
|
-
|
411
|
+
|
418
412
|
for (const watchPath of this.watchedFiles) {
|
419
413
|
try {
|
420
414
|
fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
|
@@ -516,19 +510,19 @@ export default class BXO {
|
|
516
510
|
}
|
517
511
|
|
518
512
|
console.log('🔄 Restarting BXO server...');
|
519
|
-
|
513
|
+
|
520
514
|
await this.stop();
|
521
|
-
|
515
|
+
|
522
516
|
// Small delay to ensure cleanup
|
523
517
|
await new Promise(resolve => setTimeout(resolve, 100));
|
524
|
-
|
518
|
+
|
525
519
|
await this.start(port, hostname);
|
526
520
|
|
527
521
|
// After restart hook
|
528
522
|
if (this.hooks.onAfterRestart) {
|
529
523
|
await this.hooks.onAfterRestart();
|
530
524
|
}
|
531
|
-
|
525
|
+
|
532
526
|
} catch (error) {
|
533
527
|
console.error('❌ Error restarting server:', error);
|
534
528
|
throw error;
|
@@ -554,10 +548,14 @@ export default class BXO {
|
|
554
548
|
}
|
555
549
|
}
|
556
550
|
|
551
|
+
const error = (error: Error, status: number = 500) => {
|
552
|
+
return new Response(JSON.stringify({ error: error.message }), { status });
|
553
|
+
}
|
554
|
+
|
557
555
|
// Export Zod for convenience
|
558
|
-
export { z };
|
556
|
+
export { z, error };
|
559
557
|
|
560
|
-
export type {
|
558
|
+
export type { PluginFactory } from './plugins';
|
561
559
|
|
562
560
|
// Export types for external use
|
563
561
|
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
|
}
|