bxo 0.0.1 → 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/README.md +570 -6
- package/example.ts +70 -24
- package/index.ts +200 -48
- 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/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
|
}
|