bxo 0.0.5-dev.41 → 0.0.5-dev.43
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 +1 -1
- package/package.json +1 -1
- package/plugins/README.md +160 -0
- package/plugins/cors.ts +37 -53
- package/plugins/index.ts +4 -4
- package/plugins/ratelimit.ts +2 -2
package/index.ts
CHANGED
|
@@ -1352,7 +1352,7 @@ const redirect = (location: string, status: number = 302) => {
|
|
|
1352
1352
|
export { z, error, file, redirect };
|
|
1353
1353
|
|
|
1354
1354
|
// Export types for external use
|
|
1355
|
-
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie, BXOOptions };
|
|
1355
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie, BXOOptions, Plugin };
|
|
1356
1356
|
|
|
1357
1357
|
// Helper function to create a cookie
|
|
1358
1358
|
export const createCookie = (
|
package/package.json
CHANGED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# BXO Plugins
|
|
2
|
+
|
|
3
|
+
BXO supports a plugin system that allows you to extend functionality through middleware-style plugins. Plugins can intercept requests, modify responses, and handle errors.
|
|
4
|
+
|
|
5
|
+
## Plugin Type
|
|
6
|
+
|
|
7
|
+
The `Plugin` type defines the interface for all plugins:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
interface Plugin {
|
|
11
|
+
name?: string;
|
|
12
|
+
onRequest?: (ctx: Context) => Promise<void> | void;
|
|
13
|
+
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
|
14
|
+
onError?: (ctx: Context, error: Error) => Promise<any> | any;
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Built-in Plugins
|
|
19
|
+
|
|
20
|
+
### CORS Plugin
|
|
21
|
+
|
|
22
|
+
The CORS plugin adds Cross-Origin Resource Sharing headers to your responses:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import BXO from 'bxo';
|
|
26
|
+
import { cors } from 'bxo/plugins';
|
|
27
|
+
|
|
28
|
+
const app = new BXO();
|
|
29
|
+
|
|
30
|
+
// Use with default options
|
|
31
|
+
app.use(cors());
|
|
32
|
+
|
|
33
|
+
// Or with custom options
|
|
34
|
+
app.use(cors({
|
|
35
|
+
origin: ['http://localhost:3000', 'https://myapp.com'],
|
|
36
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
37
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
38
|
+
credentials: true,
|
|
39
|
+
maxAge: 86400
|
|
40
|
+
}));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Rate Limit Plugin
|
|
44
|
+
|
|
45
|
+
The rate limit plugin helps protect your API from abuse:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import BXO from 'bxo';
|
|
49
|
+
import { rateLimit } from 'bxo/plugins';
|
|
50
|
+
|
|
51
|
+
const app = new BXO();
|
|
52
|
+
|
|
53
|
+
app.use(rateLimit({
|
|
54
|
+
max: 100, // Maximum requests per window
|
|
55
|
+
window: 60, // Time window in seconds
|
|
56
|
+
message: 'Too many requests',
|
|
57
|
+
statusCode: 429,
|
|
58
|
+
exclude: ['/health', '/metrics'] // Exclude certain paths
|
|
59
|
+
}));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Creating Custom Plugins
|
|
63
|
+
|
|
64
|
+
You can create your own plugins by implementing the `Plugin` interface:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import type { Plugin } from 'bxo';
|
|
68
|
+
|
|
69
|
+
// Simple logging plugin
|
|
70
|
+
export function logger(): Plugin {
|
|
71
|
+
return {
|
|
72
|
+
name: 'logger',
|
|
73
|
+
onRequest: async (ctx) => {
|
|
74
|
+
console.log(`${new Date().toISOString()} - ${ctx.request.method} ${ctx.path}`);
|
|
75
|
+
},
|
|
76
|
+
onResponse: async (ctx, response) => {
|
|
77
|
+
if (response instanceof Response) {
|
|
78
|
+
console.log(`${new Date().toISOString()} - ${ctx.request.method} ${ctx.path} - ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
return response;
|
|
81
|
+
},
|
|
82
|
+
onError: async (ctx, error) => {
|
|
83
|
+
console.error(`${new Date().toISOString()} - Error in ${ctx.request.method} ${ctx.path}:`, error);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Authentication plugin
|
|
89
|
+
export function auth(secret: string): Plugin {
|
|
90
|
+
return {
|
|
91
|
+
name: 'auth',
|
|
92
|
+
onRequest: async (ctx) => {
|
|
93
|
+
const token = ctx.headers.authorization?.replace('Bearer ', '');
|
|
94
|
+
|
|
95
|
+
if (!token) {
|
|
96
|
+
throw new Response(JSON.stringify({ error: 'No token provided' }), {
|
|
97
|
+
status: 401,
|
|
98
|
+
headers: { 'Content-Type': 'application/json' }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Verify token logic here...
|
|
103
|
+
// If invalid, throw a Response with 401 status
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Using Plugins
|
|
110
|
+
|
|
111
|
+
Plugins are applied using the `use()` method:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import BXO from 'bxo';
|
|
115
|
+
import { cors, rateLimit } from 'bxo/plugins';
|
|
116
|
+
import { logger, auth } from './my-plugins';
|
|
117
|
+
|
|
118
|
+
const app = new BXO();
|
|
119
|
+
|
|
120
|
+
// Apply plugins in order (they will execute in this order)
|
|
121
|
+
app.use(logger());
|
|
122
|
+
app.use(cors());
|
|
123
|
+
app.use(rateLimit({ max: 100, window: 60 }));
|
|
124
|
+
app.use(auth('my-secret'));
|
|
125
|
+
|
|
126
|
+
// Your routes
|
|
127
|
+
app.get('/', (ctx) => {
|
|
128
|
+
return { message: 'Hello World!' };
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Plugin Execution Order
|
|
133
|
+
|
|
134
|
+
Plugins execute in the order they are added:
|
|
135
|
+
|
|
136
|
+
1. **onRequest**: All plugins' `onRequest` hooks execute before the route handler
|
|
137
|
+
2. **Route Handler**: The actual route handler executes
|
|
138
|
+
3. **onResponse**: All plugins' `onResponse` hooks execute after the route handler
|
|
139
|
+
4. **onError**: If an error occurs, all plugins' `onError` hooks execute
|
|
140
|
+
|
|
141
|
+
## Plugin Context
|
|
142
|
+
|
|
143
|
+
Each plugin receives a `Context` object that contains:
|
|
144
|
+
|
|
145
|
+
- `params`: Route parameters
|
|
146
|
+
- `query`: Query string parameters
|
|
147
|
+
- `body`: Request body
|
|
148
|
+
- `headers`: Request headers
|
|
149
|
+
- `cookies`: Request cookies
|
|
150
|
+
- `path`: Request path
|
|
151
|
+
- `request`: The original Request object
|
|
152
|
+
- `set`: Object to set response properties (status, headers, cookies, redirect)
|
|
153
|
+
|
|
154
|
+
## Best Practices
|
|
155
|
+
|
|
156
|
+
1. **Keep plugins focused**: Each plugin should handle one specific concern
|
|
157
|
+
2. **Handle errors gracefully**: Use try-catch blocks in your plugin hooks
|
|
158
|
+
3. **Return responses properly**: Always return the response from `onResponse` hooks
|
|
159
|
+
4. **Use meaningful names**: Give your plugins descriptive names for debugging
|
|
160
|
+
5. **Document your plugins**: Include JSDoc comments explaining what your plugin does
|
package/plugins/cors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Plugin } from '../index';
|
|
2
2
|
|
|
3
3
|
interface CORSOptions {
|
|
4
4
|
origin?: string | string[] | boolean;
|
|
@@ -48,7 +48,7 @@ function validateOrigin(requestOrigin: string | null, allowedOrigins: string | s
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function cors(options: CORSOptions = {}):
|
|
51
|
+
export function cors(options: CORSOptions = {}): Plugin {
|
|
52
52
|
const {
|
|
53
53
|
origin = '*',
|
|
54
54
|
methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
@@ -59,69 +59,53 @@ export function cors(options: CORSOptions = {}): any {
|
|
|
59
59
|
|
|
60
60
|
return {
|
|
61
61
|
name: 'cors',
|
|
62
|
-
onRequest: async (ctx
|
|
63
|
-
// Handle preflight OPTIONS
|
|
62
|
+
onRequest: async (ctx) => {
|
|
63
|
+
// Handle preflight OPTIONS requests
|
|
64
64
|
if (ctx.request.method === 'OPTIONS') {
|
|
65
|
-
const headers: Record<string, string> = {};
|
|
66
|
-
|
|
67
|
-
// Get and validate origin
|
|
68
65
|
const requestOrigin = getRequestOrigin(ctx.request);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
const allowedOrigin = validateOrigin(requestOrigin, origin);
|
|
67
|
+
|
|
68
|
+
// Set CORS headers for preflight
|
|
69
|
+
ctx.set.headers = {
|
|
70
|
+
...ctx.set.headers,
|
|
71
|
+
'Access-Control-Allow-Origin': allowedOrigin || '*',
|
|
72
|
+
'Access-Control-Allow-Methods': methods.join(', '),
|
|
73
|
+
'Access-Control-Allow-Headers': allowedHeaders.join(', '),
|
|
74
|
+
'Access-Control-Max-Age': maxAge.toString()
|
|
75
|
+
};
|
|
79
76
|
|
|
80
77
|
if (credentials) {
|
|
81
|
-
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
78
|
+
ctx.set.headers['Access-Control-Allow-Credentials'] = 'true';
|
|
82
79
|
}
|
|
83
|
-
|
|
84
|
-
headers['Access-Control-Max-Age'] = maxAge.toString();
|
|
85
|
-
|
|
86
|
-
ctx.set.status = 204;
|
|
87
|
-
ctx.set.headers = { ...ctx.set.headers, ...headers };
|
|
88
|
-
|
|
89
|
-
throw new Response(null, { status: 204, headers });
|
|
90
80
|
}
|
|
91
81
|
},
|
|
92
|
-
onResponse: async (ctx
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Get and validate origin for actual requests
|
|
96
|
-
const requestOrigin = getRequestOrigin(ctx.request);
|
|
97
|
-
const validatedOrigin = validateOrigin(requestOrigin, origin);
|
|
98
|
-
|
|
99
|
-
if (validatedOrigin) {
|
|
100
|
-
headers['Access-Control-Allow-Origin'] = validatedOrigin;
|
|
101
|
-
} else if (typeof origin === 'string' && origin === '*') {
|
|
102
|
-
headers['Access-Control-Allow-Origin'] = '*';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (credentials) {
|
|
106
|
-
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// If response is a Response object, add headers to it
|
|
82
|
+
onResponse: async (ctx, response) => {
|
|
83
|
+
// Handle CORS headers for actual requests
|
|
110
84
|
if (response instanceof Response) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
85
|
+
const requestOrigin = getRequestOrigin(ctx.request);
|
|
86
|
+
const allowedOrigin = validateOrigin(requestOrigin, origin);
|
|
87
|
+
|
|
88
|
+
// Clone the response to modify headers
|
|
89
|
+
const newResponse = new Response(response.body, {
|
|
116
90
|
status: response.status,
|
|
117
91
|
statusText: response.statusText,
|
|
118
|
-
headers:
|
|
92
|
+
headers: new Headers(response.headers)
|
|
119
93
|
});
|
|
94
|
+
|
|
95
|
+
// Set CORS headers
|
|
96
|
+
newResponse.headers.set('Access-Control-Allow-Origin', allowedOrigin || '*');
|
|
97
|
+
newResponse.headers.set('Access-Control-Allow-Methods', methods.join(', '));
|
|
98
|
+
newResponse.headers.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
|
|
99
|
+
newResponse.headers.set('Access-Control-Max-Age', maxAge.toString());
|
|
100
|
+
|
|
101
|
+
if (credentials) {
|
|
102
|
+
newResponse.headers.set('Access-Control-Allow-Credentials', 'true');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return newResponse;
|
|
120
106
|
}
|
|
121
|
-
|
|
122
|
-
// Otherwise, set headers in context for the framework to handle
|
|
123
|
-
ctx.set.headers = { ...ctx.set.headers, ...headers };
|
|
107
|
+
|
|
124
108
|
return response;
|
|
125
109
|
}
|
|
126
110
|
};
|
|
127
|
-
}
|
|
111
|
+
}
|
package/plugins/index.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
export { cors } from './cors';
|
|
3
3
|
export { rateLimit } from './ratelimit';
|
|
4
4
|
|
|
5
|
-
// Import
|
|
6
|
-
import
|
|
5
|
+
// Import types for plugin typing
|
|
6
|
+
import type { Plugin } from '../index';
|
|
7
7
|
|
|
8
|
-
// Plugin functions
|
|
9
|
-
export type PluginFactory<T = any> = (options?: T) =>
|
|
8
|
+
// Plugin functions return Plugin instances
|
|
9
|
+
export type PluginFactory<T = any> = (options?: T) => Plugin;
|
package/plugins/ratelimit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Plugin } from '../index';
|
|
2
2
|
|
|
3
3
|
interface RateLimitOptions {
|
|
4
4
|
max: number;
|
|
@@ -52,7 +52,7 @@ class RateLimitStore {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function rateLimit(options: RateLimitOptions):
|
|
55
|
+
export function rateLimit(options: RateLimitOptions): Plugin {
|
|
56
56
|
const {
|
|
57
57
|
max,
|
|
58
58
|
window,
|