bxo 0.0.5-dev.42 → 0.0.5-dev.44
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 +48 -7
- package/plugins/example.ts +173 -0
- 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,12 +59,53 @@ export function cors(options: CORSOptions = {}): any {
|
|
|
59
59
|
|
|
60
60
|
return {
|
|
61
61
|
name: 'cors',
|
|
62
|
-
onRequest: async (ctx
|
|
62
|
+
onRequest: async (ctx) => {
|
|
63
|
+
// Handle preflight OPTIONS requests
|
|
64
|
+
if (ctx.request.method === 'OPTIONS') {
|
|
65
|
+
const requestOrigin = getRequestOrigin(ctx.request);
|
|
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
|
+
};
|
|
76
|
+
|
|
77
|
+
if (credentials) {
|
|
78
|
+
ctx.set.headers['Access-Control-Allow-Credentials'] = 'true';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
63
81
|
},
|
|
64
|
-
onResponse: async (ctx
|
|
65
|
-
|
|
66
|
-
response
|
|
82
|
+
onResponse: async (ctx, response) => {
|
|
83
|
+
// Handle CORS headers for actual requests
|
|
84
|
+
if (response instanceof Response) {
|
|
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, {
|
|
90
|
+
status: response.status,
|
|
91
|
+
statusText: response.statusText,
|
|
92
|
+
headers: new Headers(response.headers)
|
|
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;
|
|
106
|
+
}
|
|
107
|
+
|
|
67
108
|
return response;
|
|
68
109
|
}
|
|
69
110
|
};
|
|
70
|
-
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Plugin } from '../index';
|
|
2
|
+
|
|
3
|
+
// Example 1: Simple logging plugin
|
|
4
|
+
export function logger(): Plugin {
|
|
5
|
+
return {
|
|
6
|
+
name: 'logger',
|
|
7
|
+
onRequest: async (ctx) => {
|
|
8
|
+
console.log(`[${new Date().toISOString()}] ${ctx.request.method} ${ctx.path}`);
|
|
9
|
+
},
|
|
10
|
+
onResponse: async (ctx, response) => {
|
|
11
|
+
if (response instanceof Response) {
|
|
12
|
+
console.log(`[${new Date().toISOString()}] ${ctx.request.method} ${ctx.path} - ${response.status}`);
|
|
13
|
+
}
|
|
14
|
+
return response;
|
|
15
|
+
},
|
|
16
|
+
onError: async (ctx, error) => {
|
|
17
|
+
console.error(`[${new Date().toISOString()}] Error in ${ctx.request.method} ${ctx.path}:`, error);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Example 2: Authentication plugin
|
|
23
|
+
export function auth(secret: string): Plugin {
|
|
24
|
+
return {
|
|
25
|
+
name: 'auth',
|
|
26
|
+
onRequest: async (ctx) => {
|
|
27
|
+
const token = ctx.headers.authorization?.replace('Bearer ', '');
|
|
28
|
+
|
|
29
|
+
if (!token) {
|
|
30
|
+
throw new Response(JSON.stringify({ error: 'No token provided' }), {
|
|
31
|
+
status: 401,
|
|
32
|
+
headers: { 'Content-Type': 'application/json' }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Simple token validation (replace with your actual logic)
|
|
37
|
+
if (token !== secret) {
|
|
38
|
+
throw new Response(JSON.stringify({ error: 'Invalid token' }), {
|
|
39
|
+
status: 401,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add user info to context
|
|
45
|
+
(ctx as any).user = { id: 'user-123', token };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Example 3: Request timing plugin
|
|
51
|
+
export function timing(): Plugin {
|
|
52
|
+
return {
|
|
53
|
+
name: 'timing',
|
|
54
|
+
onRequest: async (ctx) => {
|
|
55
|
+
(ctx as any).startTime = Date.now();
|
|
56
|
+
},
|
|
57
|
+
onResponse: async (ctx, response) => {
|
|
58
|
+
const startTime = (ctx as any).startTime;
|
|
59
|
+
if (startTime) {
|
|
60
|
+
const duration = Date.now() - startTime;
|
|
61
|
+
console.log(`Request to ${ctx.path} took ${duration}ms`);
|
|
62
|
+
|
|
63
|
+
// Add timing header to response
|
|
64
|
+
if (response instanceof Response) {
|
|
65
|
+
const newResponse = new Response(response.body, {
|
|
66
|
+
status: response.status,
|
|
67
|
+
statusText: response.statusText,
|
|
68
|
+
headers: new Headers(response.headers)
|
|
69
|
+
});
|
|
70
|
+
newResponse.headers.set('X-Response-Time', `${duration}ms`);
|
|
71
|
+
return newResponse;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Example 4: Request ID plugin
|
|
80
|
+
export function requestId(): Plugin {
|
|
81
|
+
return {
|
|
82
|
+
name: 'requestId',
|
|
83
|
+
onRequest: async (ctx) => {
|
|
84
|
+
const requestId = crypto.randomUUID();
|
|
85
|
+
(ctx as any).requestId = requestId;
|
|
86
|
+
|
|
87
|
+
// Add request ID to response headers
|
|
88
|
+
ctx.set.headers = {
|
|
89
|
+
...ctx.set.headers,
|
|
90
|
+
'X-Request-ID': requestId
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Example 5: Conditional plugin (only applies to certain paths)
|
|
97
|
+
export function conditionalAuth(secret: string, paths: string[]): Plugin {
|
|
98
|
+
return {
|
|
99
|
+
name: 'conditionalAuth',
|
|
100
|
+
onRequest: async (ctx) => {
|
|
101
|
+
// Only apply auth to specified paths
|
|
102
|
+
if (!paths.some(path => ctx.path.startsWith(path))) {
|
|
103
|
+
return; // Skip authentication for this path
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const token = ctx.headers.authorization?.replace('Bearer ', '');
|
|
107
|
+
|
|
108
|
+
if (!token || token !== secret) {
|
|
109
|
+
throw new Response(JSON.stringify({ error: 'Authentication required' }), {
|
|
110
|
+
status: 401,
|
|
111
|
+
headers: { 'Content-Type': 'application/json' }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Example 6: Response transformation plugin
|
|
119
|
+
export function responseTransformer(): Plugin {
|
|
120
|
+
return {
|
|
121
|
+
name: 'responseTransformer',
|
|
122
|
+
onResponse: async (ctx, response) => {
|
|
123
|
+
// Only transform JSON responses
|
|
124
|
+
if (response instanceof Response) {
|
|
125
|
+
const contentType = response.headers.get('content-type');
|
|
126
|
+
if (contentType?.includes('application/json')) {
|
|
127
|
+
try {
|
|
128
|
+
const data = await response.json();
|
|
129
|
+
|
|
130
|
+
// Transform the response data
|
|
131
|
+
const transformed = {
|
|
132
|
+
success: true,
|
|
133
|
+
data,
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
path: ctx.path
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return new Response(JSON.stringify(transformed), {
|
|
139
|
+
status: response.status,
|
|
140
|
+
headers: response.headers
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// If response is not JSON, return as-is
|
|
144
|
+
return response;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return response;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Example 7: Error handling plugin
|
|
155
|
+
export function errorHandler(): Plugin {
|
|
156
|
+
return {
|
|
157
|
+
name: 'errorHandler',
|
|
158
|
+
onError: async (ctx, error) => {
|
|
159
|
+
console.error(`Error in ${ctx.request.method} ${ctx.path}:`, error);
|
|
160
|
+
|
|
161
|
+
// Return a standardized error response
|
|
162
|
+
return new Response(JSON.stringify({
|
|
163
|
+
error: 'Internal Server Error',
|
|
164
|
+
message: error.message,
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
path: ctx.path
|
|
167
|
+
}), {
|
|
168
|
+
status: 500,
|
|
169
|
+
headers: { 'Content-Type': 'application/json' }
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
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,
|