bxo 0.0.5-dev.44 → 0.0.5-dev.46
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/example.ts +20 -0
- package/index.ts +80 -2
- package/package.json +1 -1
- package/plugins/cors.ts +30 -26
- package/plugins/example.ts +0 -173
package/example.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import BXO from ".";
|
|
2
|
+
import { cors } from "./plugins";
|
|
3
|
+
|
|
4
|
+
const app = new BXO();
|
|
5
|
+
|
|
6
|
+
app.use(cors());
|
|
7
|
+
|
|
8
|
+
app.get('/', (ctx) => {
|
|
9
|
+
return { message: 'Hello, world!' };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
app.get("/api/actions/nodula.auth.login", (ctx) => {
|
|
13
|
+
return { message: 'Hello, world!' };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
app.post("/api/actions/nodula.auth.login", (ctx) => {
|
|
17
|
+
return { message: 'Hello, world!' };
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
app.listen(3000);
|
package/index.ts
CHANGED
|
@@ -136,7 +136,7 @@ interface BXOOptions {
|
|
|
136
136
|
// Plugin interface for middleware-style plugins
|
|
137
137
|
interface Plugin {
|
|
138
138
|
name?: string;
|
|
139
|
-
onRequest?: (ctx: Context) => Promise<void> | void;
|
|
139
|
+
onRequest?: (ctx: Context) => Promise<Response | void> | Response | void;
|
|
140
140
|
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
|
141
141
|
onError?: (ctx: Context, error: Error) => Promise<any> | any;
|
|
142
142
|
}
|
|
@@ -153,7 +153,7 @@ export default class BXO {
|
|
|
153
153
|
onAfterRestart?: (instance: BXO) => Promise<void> | void;
|
|
154
154
|
onBeforeStop?: (instance: BXO) => Promise<void> | void;
|
|
155
155
|
onAfterStop?: (instance: BXO) => Promise<void> | void;
|
|
156
|
-
onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
|
|
156
|
+
onRequest?: (ctx: Context, instance: BXO) => Promise<Response | void> | Response | void;
|
|
157
157
|
onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
|
|
158
158
|
onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
|
|
159
159
|
} = {};
|
|
@@ -721,6 +721,84 @@ export default class BXO {
|
|
|
721
721
|
return new Response('WebSocket upgrade failed', { status: 400 });
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
+
// Handle OPTIONS requests for CORS preflight before route matching
|
|
725
|
+
if (method === 'OPTIONS') {
|
|
726
|
+
// Create a minimal context for OPTIONS requests
|
|
727
|
+
const ctx: Context = {
|
|
728
|
+
params: {},
|
|
729
|
+
query: {},
|
|
730
|
+
body: {},
|
|
731
|
+
headers: this.parseHeaders(request.headers),
|
|
732
|
+
cookies: {},
|
|
733
|
+
path: pathname,
|
|
734
|
+
request,
|
|
735
|
+
set: {},
|
|
736
|
+
status: ((code: number, data?: any) => {
|
|
737
|
+
ctx.set.status = code;
|
|
738
|
+
return data;
|
|
739
|
+
}) as any,
|
|
740
|
+
redirect: ((location: string, status: number = 302) => {
|
|
741
|
+
ctx.set.redirect = { location, status };
|
|
742
|
+
const responseHeaders: Record<string, string> = {
|
|
743
|
+
Location: location,
|
|
744
|
+
...(ctx.set.headers || {})
|
|
745
|
+
};
|
|
746
|
+
return new Response(null, {
|
|
747
|
+
status,
|
|
748
|
+
headers: responseHeaders
|
|
749
|
+
});
|
|
750
|
+
}) as any,
|
|
751
|
+
clearRedirect: (() => {
|
|
752
|
+
delete ctx.set.redirect;
|
|
753
|
+
if (ctx.set.headers) {
|
|
754
|
+
for (const key of Object.keys(ctx.set.headers)) {
|
|
755
|
+
if (key.toLowerCase() === 'location') {
|
|
756
|
+
delete ctx.set.headers[key];
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400) {
|
|
761
|
+
delete ctx.set.status;
|
|
762
|
+
}
|
|
763
|
+
}) as any
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// Run middleware onRequest hooks for OPTIONS requests
|
|
767
|
+
for (const plugin of this.middleware) {
|
|
768
|
+
if (plugin.onRequest) {
|
|
769
|
+
const result = await plugin.onRequest(ctx);
|
|
770
|
+
// If middleware returns a response, return it immediately
|
|
771
|
+
if (result instanceof Response) {
|
|
772
|
+
return result;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Run global onRequest hook
|
|
778
|
+
if (this.hooks.onRequest) {
|
|
779
|
+
const result = await this.hooks.onRequest(ctx, this);
|
|
780
|
+
if (result instanceof Response) {
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Run BXO instance onRequest hooks
|
|
786
|
+
for (const bxoInstance of this.plugins) {
|
|
787
|
+
if (bxoInstance.hooks.onRequest) {
|
|
788
|
+
const result = await bxoInstance.hooks.onRequest(ctx, this);
|
|
789
|
+
if (result instanceof Response) {
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// If no middleware handled the OPTIONS request, return a default response
|
|
796
|
+
return new Response(null, {
|
|
797
|
+
status: 204,
|
|
798
|
+
headers: ctx.set.headers || {}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
724
802
|
const matchResult = this.matchRoute(method, pathname);
|
|
725
803
|
if (!matchResult) {
|
|
726
804
|
return new Response('Not Found', { status: 404 });
|
package/package.json
CHANGED
package/plugins/cors.ts
CHANGED
|
@@ -64,7 +64,7 @@ export function cors(options: CORSOptions = {}): Plugin {
|
|
|
64
64
|
if (ctx.request.method === 'OPTIONS') {
|
|
65
65
|
const requestOrigin = getRequestOrigin(ctx.request);
|
|
66
66
|
const allowedOrigin = validateOrigin(requestOrigin, origin);
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
// Set CORS headers for preflight
|
|
69
69
|
ctx.set.headers = {
|
|
70
70
|
...ctx.set.headers,
|
|
@@ -73,39 +73,43 @@ export function cors(options: CORSOptions = {}): Plugin {
|
|
|
73
73
|
'Access-Control-Allow-Headers': allowedHeaders.join(', '),
|
|
74
74
|
'Access-Control-Max-Age': maxAge.toString()
|
|
75
75
|
};
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
if (credentials) {
|
|
78
78
|
ctx.set.headers['Access-Control-Allow-Credentials'] = 'true';
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
console.log('ctx.set.headers', ctx.set.headers);
|
|
82
|
+
|
|
83
|
+
// Return a proper Response for OPTIONS requests
|
|
84
|
+
return new Response(null, {
|
|
85
|
+
status: 204,
|
|
86
|
+
headers: ctx.set.headers
|
|
87
|
+
});
|
|
80
88
|
}
|
|
81
89
|
},
|
|
82
90
|
onResponse: async (ctx, response) => {
|
|
83
91
|
// Handle CORS headers for actual requests
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
newResponse.headers.set('Access-Control-Allow-Credentials', 'true');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return newResponse;
|
|
92
|
+
const requestOrigin = getRequestOrigin(ctx.request);
|
|
93
|
+
const allowedOrigin = validateOrigin(requestOrigin, origin);
|
|
94
|
+
|
|
95
|
+
// Clone the response to modify headers
|
|
96
|
+
const newResponse = new Response(response.body, {
|
|
97
|
+
status: response.status,
|
|
98
|
+
statusText: response.statusText,
|
|
99
|
+
headers: new Headers(response.headers)
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Set CORS headers
|
|
103
|
+
newResponse.headers.set('Access-Control-Allow-Origin', allowedOrigin || '*');
|
|
104
|
+
newResponse.headers.set('Access-Control-Allow-Methods', methods.join(', '));
|
|
105
|
+
newResponse.headers.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
|
|
106
|
+
newResponse.headers.set('Access-Control-Max-Age', maxAge.toString());
|
|
107
|
+
|
|
108
|
+
if (credentials) {
|
|
109
|
+
newResponse.headers.set('Access-Control-Allow-Credentials', 'true');
|
|
106
110
|
}
|
|
107
|
-
|
|
108
|
-
return
|
|
111
|
+
|
|
112
|
+
return newResponse;
|
|
109
113
|
}
|
|
110
114
|
};
|
|
111
115
|
}
|
package/plugins/example.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
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
|
-
}
|