bxo 0.0.5-dev.2 โ 0.0.5-dev.20
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 +43 -22
- package/index.ts +447 -182
- package/package.json +1 -1
- package/plugins/auth.ts +0 -119
- package/plugins/logger.ts +0 -109
package/example.ts
CHANGED
@@ -1,13 +1,39 @@
|
|
1
1
|
import BXO, { z } from './index';
|
2
2
|
import { cors, logger, auth, rateLimit, createJWT } from './plugins';
|
3
3
|
|
4
|
+
// Create a simple API plugin that defines its own routes
|
5
|
+
function createApiPlugin(): BXO {
|
6
|
+
const apiPlugin = new BXO();
|
7
|
+
|
8
|
+
apiPlugin
|
9
|
+
.get('/api/info', async (ctx) => {
|
10
|
+
return {
|
11
|
+
name: 'BXO API Plugin',
|
12
|
+
version: '1.0.0',
|
13
|
+
endpoints: ['/api/info', '/api/ping', '/api/time']
|
14
|
+
};
|
15
|
+
})
|
16
|
+
.get('/api/ping', async (ctx) => {
|
17
|
+
return { ping: 'pong', timestamp: Date.now() };
|
18
|
+
})
|
19
|
+
.get('/api/time', async (ctx) => {
|
20
|
+
return { time: new Date().toISOString() };
|
21
|
+
})
|
22
|
+
.post('/api/echo', async (ctx) => {
|
23
|
+
return { echo: ctx.body };
|
24
|
+
}, {
|
25
|
+
body: z.object({
|
26
|
+
message: z.string()
|
27
|
+
})
|
28
|
+
});
|
29
|
+
|
30
|
+
return apiPlugin;
|
31
|
+
}
|
32
|
+
|
4
33
|
// Create the app instance
|
5
34
|
const app = new BXO();
|
6
35
|
|
7
|
-
//
|
8
|
-
app.enableHotReload(['./']); // Watch current directory
|
9
|
-
|
10
|
-
// Add plugins
|
36
|
+
// Add plugins (including our new API plugin)
|
11
37
|
app
|
12
38
|
.use(logger({ format: 'simple' }))
|
13
39
|
.use(cors({
|
@@ -22,8 +48,9 @@ app
|
|
22
48
|
.use(auth({
|
23
49
|
type: 'jwt',
|
24
50
|
secret: 'your-secret-key',
|
25
|
-
exclude: ['/', '/login', '/health']
|
26
|
-
}))
|
51
|
+
exclude: ['/', '/login', '/health', '/api/*']
|
52
|
+
}))
|
53
|
+
.use(createApiPlugin()); // Add our plugin with actual routes
|
27
54
|
|
28
55
|
// Add simplified lifecycle hooks
|
29
56
|
app
|
@@ -39,12 +66,6 @@ app
|
|
39
66
|
.onAfterStop(() => {
|
40
67
|
console.log('โ
Server fully stopped!');
|
41
68
|
})
|
42
|
-
.onBeforeRestart(() => {
|
43
|
-
console.log('๐ง Preparing to restart server...');
|
44
|
-
})
|
45
|
-
.onAfterRestart(() => {
|
46
|
-
console.log('โ
Server restart completed!');
|
47
|
-
})
|
48
69
|
.onRequest((ctx) => {
|
49
70
|
console.log(`๐จ Processing ${ctx.request.method} ${ctx.request.url}`);
|
50
71
|
})
|
@@ -116,13 +137,6 @@ app
|
|
116
137
|
return { message: 'This is protected', user: ctx.user };
|
117
138
|
})
|
118
139
|
|
119
|
-
// Server control endpoints
|
120
|
-
.post('/restart', async (ctx) => {
|
121
|
-
// Restart the server
|
122
|
-
setTimeout(() => app.restart(3000), 100);
|
123
|
-
return { message: 'Server restart initiated' };
|
124
|
-
})
|
125
|
-
|
126
140
|
.get('/status', async (ctx) => {
|
127
141
|
return {
|
128
142
|
...app.getServerInfo(),
|
@@ -162,12 +176,12 @@ console.log(`
|
|
162
176
|
๐ฆ BXO Framework with Hot Reload
|
163
177
|
|
164
178
|
โจ Features Enabled:
|
165
|
-
- ๐ Hot reload (edit any .ts/.js file to restart)
|
166
179
|
- ๐ฃ Full lifecycle hooks (before/after pattern)
|
167
180
|
- ๐ JWT authentication
|
168
181
|
- ๐ Rate limiting
|
169
182
|
- ๐ CORS support
|
170
183
|
- ๐ Request logging
|
184
|
+
- ๐ API Plugin with routes
|
171
185
|
|
172
186
|
๐งช Try these endpoints:
|
173
187
|
- GET /simple
|
@@ -177,7 +191,14 @@ console.log(`
|
|
177
191
|
- POST /login (with JSON body: {"username": "admin", "password": "password"})
|
178
192
|
- GET /protected (requires Bearer token from /login)
|
179
193
|
- GET /status (server statistics)
|
180
|
-
|
194
|
+
|
195
|
+
๐ API Plugin endpoints:
|
196
|
+
- GET /api/info (plugin information)
|
197
|
+
- GET /api/ping (ping pong)
|
198
|
+
- GET /api/time (current time)
|
199
|
+
- POST /api/echo (echo message: {"message": "hello"})
|
181
200
|
|
182
201
|
๐ก Edit this file and save to see hot reload in action!
|
183
|
-
`);
|
202
|
+
`);
|
203
|
+
|
204
|
+
console.log(app.routes)
|
package/index.ts
CHANGED
@@ -3,6 +3,18 @@ import { z } from 'zod';
|
|
3
3
|
// Type utilities for extracting types from Zod schemas
|
4
4
|
type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
|
5
5
|
|
6
|
+
// OpenAPI detail information
|
7
|
+
interface RouteDetail {
|
8
|
+
summary?: string;
|
9
|
+
description?: string;
|
10
|
+
tags?: string[];
|
11
|
+
operationId?: string;
|
12
|
+
deprecated?: boolean;
|
13
|
+
produces?: string[];
|
14
|
+
consumes?: string[];
|
15
|
+
[key: string]: any; // Allow additional OpenAPI properties
|
16
|
+
}
|
17
|
+
|
6
18
|
// Configuration interface for route handlers
|
7
19
|
interface RouteConfig {
|
8
20
|
params?: z.ZodSchema<any>;
|
@@ -10,6 +22,7 @@ interface RouteConfig {
|
|
10
22
|
body?: z.ZodSchema<any>;
|
11
23
|
headers?: z.ZodSchema<any>;
|
12
24
|
response?: z.ZodSchema<any>;
|
25
|
+
detail?: RouteDetail;
|
13
26
|
}
|
14
27
|
|
15
28
|
// Context type that's fully typed based on the route configuration
|
@@ -18,18 +31,17 @@ export type Context<TConfig extends RouteConfig = {}> = {
|
|
18
31
|
query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
|
19
32
|
body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
|
20
33
|
headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
|
34
|
+
path: string;
|
21
35
|
request: Request;
|
22
36
|
set: {
|
23
37
|
status?: number;
|
24
38
|
headers?: Record<string, string>;
|
25
39
|
};
|
26
|
-
// Extended properties that can be added by plugins
|
27
|
-
user?: any;
|
28
40
|
[key: string]: any;
|
29
41
|
};
|
30
42
|
|
31
43
|
// Handler function type
|
32
|
-
type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
|
44
|
+
type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<any> | any;
|
33
45
|
|
34
46
|
// Route definition
|
35
47
|
interface Route {
|
@@ -39,73 +51,77 @@ interface Route {
|
|
39
51
|
config?: RouteConfig;
|
40
52
|
}
|
41
53
|
|
54
|
+
// WebSocket handler interface
|
55
|
+
interface WebSocketHandler {
|
56
|
+
onOpen?: (ws: any) => void;
|
57
|
+
onMessage?: (ws: any, message: string | Buffer) => void;
|
58
|
+
onClose?: (ws: any, code?: number, reason?: string) => void;
|
59
|
+
onError?: (ws: any, error: Error) => void;
|
60
|
+
}
|
61
|
+
|
62
|
+
// WebSocket route definition
|
63
|
+
interface WSRoute {
|
64
|
+
path: string;
|
65
|
+
handler: WebSocketHandler;
|
66
|
+
}
|
67
|
+
|
42
68
|
// Lifecycle hooks
|
43
69
|
interface LifecycleHooks {
|
44
|
-
onBeforeStart?: () => Promise<void> | void;
|
45
|
-
onAfterStart?: () => Promise<void> | void;
|
46
|
-
onBeforeStop?: () => Promise<void> | void;
|
47
|
-
onAfterStop?: () => Promise<void> | void;
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
52
|
-
onError?: (ctx: Context, error: Error) => Promise<any> | any;
|
70
|
+
onBeforeStart?: (instance: BXO) => Promise<void> | void;
|
71
|
+
onAfterStart?: (instance: BXO) => Promise<void> | void;
|
72
|
+
onBeforeStop?: (instance: BXO) => Promise<void> | void;
|
73
|
+
onAfterStop?: (instance: BXO) => Promise<void> | void;
|
74
|
+
onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
|
75
|
+
onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
|
76
|
+
onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
|
53
77
|
}
|
54
78
|
|
55
79
|
export default class BXO {
|
56
80
|
private _routes: Route[] = [];
|
81
|
+
private _wsRoutes: WSRoute[] = [];
|
57
82
|
private plugins: BXO[] = [];
|
58
83
|
private hooks: LifecycleHooks = {};
|
59
84
|
private server?: any;
|
60
85
|
private isRunning: boolean = false;
|
61
|
-
private
|
62
|
-
private
|
63
|
-
private watchedExclude: Set<string> = new Set();
|
86
|
+
private serverPort?: number;
|
87
|
+
private serverHostname?: string;
|
64
88
|
|
65
89
|
constructor() { }
|
66
90
|
|
67
91
|
// Lifecycle hook methods
|
68
|
-
onBeforeStart(handler: () => Promise<void> | void): this {
|
92
|
+
onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
|
69
93
|
this.hooks.onBeforeStart = handler;
|
70
94
|
return this;
|
71
95
|
}
|
72
96
|
|
73
|
-
onAfterStart(handler: () => Promise<void> | void): this {
|
97
|
+
onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
|
74
98
|
this.hooks.onAfterStart = handler;
|
75
99
|
return this;
|
76
100
|
}
|
77
101
|
|
78
|
-
onBeforeStop(handler: () => Promise<void> | void): this {
|
102
|
+
onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
|
79
103
|
this.hooks.onBeforeStop = handler;
|
80
104
|
return this;
|
81
105
|
}
|
82
106
|
|
83
|
-
onAfterStop(handler: () => Promise<void> | void): this {
|
107
|
+
onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
|
84
108
|
this.hooks.onAfterStop = handler;
|
85
109
|
return this;
|
86
110
|
}
|
87
111
|
|
88
|
-
onBeforeRestart(handler: () => Promise<void> | void): this {
|
89
|
-
this.hooks.onBeforeRestart = handler;
|
90
|
-
return this;
|
91
|
-
}
|
92
112
|
|
93
|
-
onAfterRestart(handler: () => Promise<void> | void): this {
|
94
|
-
this.hooks.onAfterRestart = handler;
|
95
|
-
return this;
|
96
|
-
}
|
97
113
|
|
98
|
-
onRequest(handler: (ctx: Context) => Promise<void> | void): this {
|
114
|
+
onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
|
99
115
|
this.hooks.onRequest = handler;
|
100
116
|
return this;
|
101
117
|
}
|
102
118
|
|
103
|
-
onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
|
119
|
+
onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
|
104
120
|
this.hooks.onResponse = handler;
|
105
121
|
return this;
|
106
122
|
}
|
107
123
|
|
108
|
-
onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
|
124
|
+
onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
|
109
125
|
this.hooks.onError = handler;
|
110
126
|
return this;
|
111
127
|
}
|
@@ -207,24 +223,135 @@ export default class BXO {
|
|
207
223
|
return this;
|
208
224
|
}
|
209
225
|
|
226
|
+
// WebSocket route handler
|
227
|
+
ws(path: string, handler: WebSocketHandler): this {
|
228
|
+
this._wsRoutes.push({ path, handler });
|
229
|
+
return this;
|
230
|
+
}
|
231
|
+
|
232
|
+
// Helper methods to get all routes including plugin routes
|
233
|
+
private getAllRoutes(): Route[] {
|
234
|
+
const allRoutes = [...this._routes];
|
235
|
+
for (const plugin of this.plugins) {
|
236
|
+
allRoutes.push(...plugin._routes);
|
237
|
+
}
|
238
|
+
return allRoutes;
|
239
|
+
}
|
240
|
+
|
241
|
+
private getAllWSRoutes(): WSRoute[] {
|
242
|
+
const allWSRoutes = [...this._wsRoutes];
|
243
|
+
for (const plugin of this.plugins) {
|
244
|
+
allWSRoutes.push(...plugin._wsRoutes);
|
245
|
+
}
|
246
|
+
return allWSRoutes;
|
247
|
+
}
|
248
|
+
|
210
249
|
// Route matching utility
|
211
250
|
private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
|
212
|
-
|
251
|
+
const allRoutes = this.getAllRoutes();
|
252
|
+
|
253
|
+
for (const route of allRoutes) {
|
213
254
|
if (route.method !== method) continue;
|
214
255
|
|
215
256
|
const routeSegments = route.path.split('/').filter(Boolean);
|
216
257
|
const pathSegments = pathname.split('/').filter(Boolean);
|
217
258
|
|
218
|
-
|
259
|
+
const params: Record<string, string> = {};
|
260
|
+
let isMatch = true;
|
261
|
+
|
262
|
+
// Handle wildcard at the end (catch-all)
|
263
|
+
const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
|
264
|
+
|
265
|
+
if (hasWildcardAtEnd) {
|
266
|
+
// For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
|
267
|
+
if (pathSegments.length < routeSegments.length - 1) continue;
|
268
|
+
} else {
|
269
|
+
// For exact matching (with possible single-segment wildcards), lengths must match
|
270
|
+
if (routeSegments.length !== pathSegments.length) continue;
|
271
|
+
}
|
272
|
+
|
273
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
274
|
+
const routeSegment = routeSegments[i];
|
275
|
+
const pathSegment = pathSegments[i];
|
276
|
+
|
277
|
+
if (!routeSegment) {
|
278
|
+
isMatch = false;
|
279
|
+
break;
|
280
|
+
}
|
281
|
+
|
282
|
+
// Handle catch-all wildcard at the end
|
283
|
+
if (routeSegment === '*' && i === routeSegments.length - 1) {
|
284
|
+
// Wildcard at end matches remaining path segments
|
285
|
+
const remainingPath = pathSegments.slice(i).join('/');
|
286
|
+
params['*'] = remainingPath;
|
287
|
+
break;
|
288
|
+
}
|
289
|
+
|
290
|
+
if (!pathSegment) {
|
291
|
+
isMatch = false;
|
292
|
+
break;
|
293
|
+
}
|
294
|
+
|
295
|
+
if (routeSegment.startsWith(':')) {
|
296
|
+
const paramName = routeSegment.slice(1);
|
297
|
+
params[paramName] = decodeURIComponent(pathSegment);
|
298
|
+
} else if (routeSegment === '*') {
|
299
|
+
// Single segment wildcard
|
300
|
+
params['*'] = decodeURIComponent(pathSegment);
|
301
|
+
} else if (routeSegment !== pathSegment) {
|
302
|
+
isMatch = false;
|
303
|
+
break;
|
304
|
+
}
|
305
|
+
}
|
306
|
+
|
307
|
+
if (isMatch) {
|
308
|
+
return { route, params };
|
309
|
+
}
|
310
|
+
}
|
311
|
+
|
312
|
+
return null;
|
313
|
+
}
|
314
|
+
|
315
|
+
// WebSocket route matching utility
|
316
|
+
private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
|
317
|
+
const allWSRoutes = this.getAllWSRoutes();
|
318
|
+
|
319
|
+
for (const route of allWSRoutes) {
|
320
|
+
const routeSegments = route.path.split('/').filter(Boolean);
|
321
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
219
322
|
|
220
323
|
const params: Record<string, string> = {};
|
221
324
|
let isMatch = true;
|
222
325
|
|
326
|
+
// Handle wildcard at the end (catch-all)
|
327
|
+
const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
|
328
|
+
|
329
|
+
if (hasWildcardAtEnd) {
|
330
|
+
// For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
|
331
|
+
if (pathSegments.length < routeSegments.length - 1) continue;
|
332
|
+
} else {
|
333
|
+
// For exact matching (with possible single-segment wildcards), lengths must match
|
334
|
+
if (routeSegments.length !== pathSegments.length) continue;
|
335
|
+
}
|
336
|
+
|
223
337
|
for (let i = 0; i < routeSegments.length; i++) {
|
224
338
|
const routeSegment = routeSegments[i];
|
225
339
|
const pathSegment = pathSegments[i];
|
226
340
|
|
227
|
-
if (!routeSegment
|
341
|
+
if (!routeSegment) {
|
342
|
+
isMatch = false;
|
343
|
+
break;
|
344
|
+
}
|
345
|
+
|
346
|
+
// Handle catch-all wildcard at the end
|
347
|
+
if (routeSegment === '*' && i === routeSegments.length - 1) {
|
348
|
+
// Wildcard at end matches remaining path segments
|
349
|
+
const remainingPath = pathSegments.slice(i).join('/');
|
350
|
+
params['*'] = remainingPath;
|
351
|
+
break;
|
352
|
+
}
|
353
|
+
|
354
|
+
if (!pathSegment) {
|
228
355
|
isMatch = false;
|
229
356
|
break;
|
230
357
|
}
|
@@ -232,6 +359,9 @@ export default class BXO {
|
|
232
359
|
if (routeSegment.startsWith(':')) {
|
233
360
|
const paramName = routeSegment.slice(1);
|
234
361
|
params[paramName] = decodeURIComponent(pathSegment);
|
362
|
+
} else if (routeSegment === '*') {
|
363
|
+
// Single segment wildcard
|
364
|
+
params['*'] = decodeURIComponent(pathSegment);
|
235
365
|
} else if (routeSegment !== pathSegment) {
|
236
366
|
isMatch = false;
|
237
367
|
break;
|
@@ -271,11 +401,30 @@ export default class BXO {
|
|
271
401
|
}
|
272
402
|
|
273
403
|
// Main request handler
|
274
|
-
private async handleRequest(request: Request): Promise<Response> {
|
404
|
+
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
275
405
|
const url = new URL(request.url);
|
276
406
|
const method = request.method;
|
277
407
|
const pathname = url.pathname;
|
278
408
|
|
409
|
+
// Check for WebSocket upgrade
|
410
|
+
if (request.headers.get('upgrade') === 'websocket') {
|
411
|
+
const wsMatchResult = this.matchWSRoute(pathname);
|
412
|
+
if (wsMatchResult && server) {
|
413
|
+
const success = server.upgrade(request, {
|
414
|
+
data: {
|
415
|
+
handler: wsMatchResult.route.handler,
|
416
|
+
params: wsMatchResult.params,
|
417
|
+
pathname
|
418
|
+
}
|
419
|
+
});
|
420
|
+
|
421
|
+
if (success) {
|
422
|
+
return; // undefined response means upgrade was successful
|
423
|
+
}
|
424
|
+
}
|
425
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
426
|
+
}
|
427
|
+
|
279
428
|
const matchResult = this.matchRoute(method, pathname);
|
280
429
|
if (!matchResult) {
|
281
430
|
return new Response('Not Found', { status: 404 });
|
@@ -298,30 +447,76 @@ export default class BXO {
|
|
298
447
|
const formData = await request.formData();
|
299
448
|
body = Object.fromEntries(formData.entries());
|
300
449
|
} else {
|
301
|
-
|
450
|
+
// Try to parse as JSON if it looks like JSON, otherwise treat as text
|
451
|
+
const textBody = await request.text();
|
452
|
+
try {
|
453
|
+
// Check if the text looks like JSON
|
454
|
+
if (textBody.trim().startsWith('{') || textBody.trim().startsWith('[')) {
|
455
|
+
body = JSON.parse(textBody);
|
456
|
+
} else {
|
457
|
+
body = textBody;
|
458
|
+
}
|
459
|
+
} catch {
|
460
|
+
body = textBody;
|
461
|
+
}
|
302
462
|
}
|
303
463
|
}
|
304
464
|
|
305
|
-
// Create context
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
465
|
+
// Create context with validation
|
466
|
+
let ctx: Context;
|
467
|
+
try {
|
468
|
+
// Validate each part separately to get better error messages
|
469
|
+
const validatedParams = route.config?.params ? this.validateData(route.config.params, params) : params;
|
470
|
+
const validatedQuery = route.config?.query ? this.validateData(route.config.query, query) : query;
|
471
|
+
const validatedBody = route.config?.body ? this.validateData(route.config.body, body) : body;
|
472
|
+
const validatedHeaders = route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
|
473
|
+
|
474
|
+
ctx = {
|
475
|
+
params: validatedParams,
|
476
|
+
query: validatedQuery,
|
477
|
+
body: validatedBody,
|
478
|
+
headers: validatedHeaders,
|
479
|
+
path: pathname,
|
480
|
+
request,
|
481
|
+
set: {}
|
482
|
+
};
|
483
|
+
} catch (validationError) {
|
484
|
+
// Validation failed - return error response
|
485
|
+
|
486
|
+
// Extract detailed validation errors from Zod
|
487
|
+
let validationDetails = undefined;
|
488
|
+
if (validationError instanceof Error) {
|
489
|
+
if ('errors' in validationError && Array.isArray(validationError.errors)) {
|
490
|
+
validationDetails = validationError.errors;
|
491
|
+
} else if ('issues' in validationError && Array.isArray(validationError.issues)) {
|
492
|
+
validationDetails = validationError.issues;
|
493
|
+
}
|
494
|
+
}
|
495
|
+
|
496
|
+
// Create a clean error message
|
497
|
+
const errorMessage = validationDetails && validationDetails.length > 0
|
498
|
+
? `Validation failed for ${validationDetails.length} field(s)`
|
499
|
+
: 'Validation failed';
|
500
|
+
|
501
|
+
return new Response(JSON.stringify({
|
502
|
+
error: errorMessage,
|
503
|
+
details: validationDetails
|
504
|
+
}), {
|
505
|
+
status: 400,
|
506
|
+
headers: { 'Content-Type': 'application/json' }
|
507
|
+
});
|
508
|
+
}
|
314
509
|
|
315
510
|
try {
|
316
511
|
// Run global onRequest hook
|
317
512
|
if (this.hooks.onRequest) {
|
318
|
-
await this.hooks.onRequest(ctx);
|
513
|
+
await this.hooks.onRequest(ctx, this);
|
319
514
|
}
|
320
515
|
|
321
516
|
// Run BXO instance onRequest hooks
|
322
517
|
for (const bxoInstance of this.plugins) {
|
323
518
|
if (bxoInstance.hooks.onRequest) {
|
324
|
-
await bxoInstance.hooks.onRequest(ctx);
|
519
|
+
await bxoInstance.hooks.onRequest(ctx, this);
|
325
520
|
}
|
326
521
|
}
|
327
522
|
|
@@ -330,24 +525,43 @@ export default class BXO {
|
|
330
525
|
|
331
526
|
// Run global onResponse hook
|
332
527
|
if (this.hooks.onResponse) {
|
333
|
-
response = await this.hooks.onResponse(ctx, response) || response;
|
528
|
+
response = await this.hooks.onResponse(ctx, response, this) || response;
|
334
529
|
}
|
335
530
|
|
336
531
|
// Run BXO instance onResponse hooks
|
337
532
|
for (const bxoInstance of this.plugins) {
|
338
533
|
if (bxoInstance.hooks.onResponse) {
|
339
|
-
response = await bxoInstance.hooks.onResponse(ctx, response) || response;
|
534
|
+
response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
|
340
535
|
}
|
341
536
|
}
|
342
537
|
|
343
538
|
// Validate response against schema if provided
|
344
539
|
if (route.config?.response && !(response instanceof Response)) {
|
345
540
|
try {
|
541
|
+
console.log('response', response);
|
346
542
|
response = this.validateData(route.config.response, response);
|
347
543
|
} catch (validationError) {
|
348
544
|
// Response validation failed
|
349
|
-
|
350
|
-
|
545
|
+
|
546
|
+
// Extract detailed validation errors from Zod
|
547
|
+
let validationDetails = undefined;
|
548
|
+
if (validationError instanceof Error) {
|
549
|
+
if ('errors' in validationError && Array.isArray(validationError.errors)) {
|
550
|
+
validationDetails = validationError.errors;
|
551
|
+
} else if ('issues' in validationError && Array.isArray(validationError.issues)) {
|
552
|
+
validationDetails = validationError.issues;
|
553
|
+
}
|
554
|
+
}
|
555
|
+
|
556
|
+
// Create a clean error message
|
557
|
+
const errorMessage = validationDetails && validationDetails.length > 0
|
558
|
+
? `Response validation failed for ${validationDetails.length} field(s)`
|
559
|
+
: 'Response validation failed';
|
560
|
+
|
561
|
+
return new Response(JSON.stringify({
|
562
|
+
error: errorMessage,
|
563
|
+
details: validationDetails
|
564
|
+
}), {
|
351
565
|
status: 500,
|
352
566
|
headers: { 'Content-Type': 'application/json' }
|
353
567
|
});
|
@@ -359,6 +573,35 @@ export default class BXO {
|
|
359
573
|
return response;
|
360
574
|
}
|
361
575
|
|
576
|
+
// Handle File response (like Elysia)
|
577
|
+
if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
|
578
|
+
const file = response as File;
|
579
|
+
const responseInit: ResponseInit = {
|
580
|
+
status: ctx.set.status || 200,
|
581
|
+
headers: {
|
582
|
+
'Content-Type': file.type || 'application/octet-stream',
|
583
|
+
'Content-Length': file.size.toString(),
|
584
|
+
...ctx.set.headers
|
585
|
+
}
|
586
|
+
};
|
587
|
+
return new Response(file, responseInit);
|
588
|
+
}
|
589
|
+
|
590
|
+
// Handle Bun.file() response
|
591
|
+
if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
|
592
|
+
const bunFile = response as any;
|
593
|
+
const responseInit: ResponseInit = {
|
594
|
+
status: ctx.set.status || 200,
|
595
|
+
headers: {
|
596
|
+
'Content-Type': bunFile.type || 'application/octet-stream',
|
597
|
+
'Content-Length': bunFile.size?.toString() || '',
|
598
|
+
...ctx.set.headers,
|
599
|
+
...(bunFile.headers || {}) // Support custom headers from file helper
|
600
|
+
}
|
601
|
+
};
|
602
|
+
return new Response(bunFile, responseInit);
|
603
|
+
}
|
604
|
+
|
362
605
|
const responseInit: ResponseInit = {
|
363
606
|
status: ctx.set.status || 200,
|
364
607
|
headers: ctx.set.headers || {}
|
@@ -381,12 +624,12 @@ export default class BXO {
|
|
381
624
|
let errorResponse: any;
|
382
625
|
|
383
626
|
if (this.hooks.onError) {
|
384
|
-
errorResponse = await this.hooks.onError(ctx, error as Error);
|
627
|
+
errorResponse = await this.hooks.onError(ctx, error as Error, this);
|
385
628
|
}
|
386
629
|
|
387
630
|
for (const bxoInstance of this.plugins) {
|
388
631
|
if (bxoInstance.hooks.onError) {
|
389
|
-
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
|
632
|
+
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
|
390
633
|
}
|
391
634
|
}
|
392
635
|
|
@@ -409,77 +652,7 @@ export default class BXO {
|
|
409
652
|
}
|
410
653
|
}
|
411
654
|
|
412
|
-
// Hot reload functionality
|
413
|
-
enableHotReload(watchPaths: string[] = ['./'], excludePatterns: string[] = []): this {
|
414
|
-
this.hotReloadEnabled = true;
|
415
|
-
watchPaths.forEach(path => this.watchedFiles.add(path));
|
416
|
-
excludePatterns.forEach(pattern => this.watchedExclude.add(pattern));
|
417
|
-
return this;
|
418
|
-
}
|
419
|
-
|
420
|
-
private shouldExcludeFile(filename: string): boolean {
|
421
|
-
for (const pattern of this.watchedExclude) {
|
422
|
-
// Handle exact match
|
423
|
-
if (pattern === filename) {
|
424
|
-
return true;
|
425
|
-
}
|
426
|
-
|
427
|
-
// Handle directory patterns (e.g., "node_modules/", "dist/")
|
428
|
-
if (pattern.endsWith('/')) {
|
429
|
-
if (filename.startsWith(pattern) || filename.includes(`/${pattern}`)) {
|
430
|
-
return true;
|
431
|
-
}
|
432
|
-
}
|
433
|
-
|
434
|
-
// Handle wildcard patterns (e.g., "*.log", "temp*")
|
435
|
-
if (pattern.includes('*')) {
|
436
|
-
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
437
|
-
if (regex.test(filename)) {
|
438
|
-
return true;
|
439
|
-
}
|
440
|
-
}
|
441
|
-
|
442
|
-
// Handle file extension patterns (e.g., ".log", ".tmp")
|
443
|
-
if (pattern.startsWith('.') && filename.endsWith(pattern)) {
|
444
|
-
return true;
|
445
|
-
}
|
446
|
-
|
447
|
-
// Handle substring matches for directories
|
448
|
-
if (filename.includes(pattern)) {
|
449
|
-
return true;
|
450
|
-
}
|
451
|
-
}
|
452
|
-
|
453
|
-
return false;
|
454
|
-
}
|
455
|
-
|
456
|
-
private async setupFileWatcher(port: number, hostname: string): Promise<void> {
|
457
|
-
if (!this.hotReloadEnabled) return;
|
458
655
|
|
459
|
-
const fs = require('fs');
|
460
|
-
|
461
|
-
for (const watchPath of this.watchedFiles) {
|
462
|
-
try {
|
463
|
-
fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
|
464
|
-
if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
|
465
|
-
// Check if file should be excluded
|
466
|
-
if (this.shouldExcludeFile(filename)) {
|
467
|
-
return;
|
468
|
-
}
|
469
|
-
|
470
|
-
console.log(`๐ File changed: ${filename}, restarting server...`);
|
471
|
-
await this.restart(port, hostname);
|
472
|
-
}
|
473
|
-
});
|
474
|
-
console.log(`๐ Watching ${watchPath} for changes...`);
|
475
|
-
if (this.watchedExclude.size > 0) {
|
476
|
-
console.log(`๐ซ Excluding patterns: ${Array.from(this.watchedExclude).join(', ')}`);
|
477
|
-
}
|
478
|
-
} catch (error) {
|
479
|
-
console.warn(`โ ๏ธ Could not watch ${watchPath}:`, error);
|
480
|
-
}
|
481
|
-
}
|
482
|
-
}
|
483
656
|
|
484
657
|
// Server management methods
|
485
658
|
async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
@@ -491,27 +664,49 @@ export default class BXO {
|
|
491
664
|
try {
|
492
665
|
// Before start hook
|
493
666
|
if (this.hooks.onBeforeStart) {
|
494
|
-
await this.hooks.onBeforeStart();
|
667
|
+
await this.hooks.onBeforeStart(this);
|
495
668
|
}
|
496
669
|
|
497
670
|
this.server = Bun.serve({
|
498
671
|
port,
|
499
672
|
hostname,
|
500
|
-
fetch: (request) => this.handleRequest(request),
|
673
|
+
fetch: (request, server) => this.handleRequest(request, server),
|
674
|
+
websocket: {
|
675
|
+
message: (ws: any, message: any) => {
|
676
|
+
const handler = ws.data?.handler;
|
677
|
+
if (handler?.onMessage) {
|
678
|
+
handler.onMessage(ws, message);
|
679
|
+
}
|
680
|
+
},
|
681
|
+
open: (ws: any) => {
|
682
|
+
const handler = ws.data?.handler;
|
683
|
+
if (handler?.onOpen) {
|
684
|
+
handler.onOpen(ws);
|
685
|
+
}
|
686
|
+
},
|
687
|
+
close: (ws: any, code?: number, reason?: string) => {
|
688
|
+
const handler = ws.data?.handler;
|
689
|
+
if (handler?.onClose) {
|
690
|
+
handler.onClose(ws, code, reason);
|
691
|
+
}
|
692
|
+
}
|
693
|
+
}
|
501
694
|
});
|
502
695
|
|
503
|
-
|
696
|
+
// Verify server was created successfully
|
697
|
+
if (!this.server) {
|
698
|
+
throw new Error('Failed to create server instance');
|
699
|
+
}
|
504
700
|
|
505
|
-
|
701
|
+
this.isRunning = true;
|
702
|
+
this.serverPort = port;
|
703
|
+
this.serverHostname = hostname;
|
506
704
|
|
507
705
|
// After start hook
|
508
706
|
if (this.hooks.onAfterStart) {
|
509
|
-
await this.hooks.onAfterStart();
|
707
|
+
await this.hooks.onAfterStart(this);
|
510
708
|
}
|
511
709
|
|
512
|
-
// Setup hot reload
|
513
|
-
await this.setupFileWatcher(port, hostname);
|
514
|
-
|
515
710
|
// Handle graceful shutdown
|
516
711
|
const shutdownHandler = async () => {
|
517
712
|
await this.stop();
|
@@ -536,55 +731,49 @@ export default class BXO {
|
|
536
731
|
try {
|
537
732
|
// Before stop hook
|
538
733
|
if (this.hooks.onBeforeStop) {
|
539
|
-
await this.hooks.onBeforeStop();
|
734
|
+
await this.hooks.onBeforeStop(this);
|
540
735
|
}
|
541
736
|
|
542
737
|
if (this.server) {
|
543
|
-
|
544
|
-
|
738
|
+
try {
|
739
|
+
// Try to stop the server gracefully
|
740
|
+
if (typeof this.server.stop === 'function') {
|
741
|
+
this.server.stop();
|
742
|
+
} else {
|
743
|
+
console.warn('โ ๏ธ Server stop method not available');
|
744
|
+
}
|
745
|
+
} catch (stopError) {
|
746
|
+
console.error('โ Error calling server.stop():', stopError);
|
747
|
+
}
|
748
|
+
|
749
|
+
// Clear the server reference
|
750
|
+
this.server = undefined;
|
545
751
|
}
|
546
752
|
|
753
|
+
// Reset state regardless of server.stop() success
|
547
754
|
this.isRunning = false;
|
548
|
-
|
549
|
-
|
755
|
+
this.serverPort = undefined;
|
756
|
+
this.serverHostname = undefined;
|
550
757
|
|
551
758
|
// After stop hook
|
552
759
|
if (this.hooks.onAfterStop) {
|
553
|
-
await this.hooks.onAfterStop();
|
760
|
+
await this.hooks.onAfterStop(this);
|
554
761
|
}
|
555
762
|
|
763
|
+
console.log('โ
Server stopped successfully');
|
764
|
+
|
556
765
|
} catch (error) {
|
557
766
|
console.error('โ Error stopping server:', error);
|
767
|
+
// Even if there's an error, reset the state
|
768
|
+
this.isRunning = false;
|
769
|
+
this.server = undefined;
|
770
|
+
this.serverPort = undefined;
|
771
|
+
this.serverHostname = undefined;
|
558
772
|
throw error;
|
559
773
|
}
|
560
774
|
}
|
561
775
|
|
562
|
-
async restart(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
563
|
-
try {
|
564
|
-
// Before restart hook
|
565
|
-
if (this.hooks.onBeforeRestart) {
|
566
|
-
await this.hooks.onBeforeRestart();
|
567
|
-
}
|
568
|
-
|
569
|
-
console.log('๐ Restarting BXO server...');
|
570
776
|
|
571
|
-
await this.stop();
|
572
|
-
|
573
|
-
// Small delay to ensure cleanup
|
574
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
575
|
-
|
576
|
-
await this.start(port, hostname);
|
577
|
-
|
578
|
-
// After restart hook
|
579
|
-
if (this.hooks.onAfterRestart) {
|
580
|
-
await this.hooks.onAfterRestart();
|
581
|
-
}
|
582
|
-
|
583
|
-
} catch (error) {
|
584
|
-
console.error('โ Error restarting server:', error);
|
585
|
-
throw error;
|
586
|
-
}
|
587
|
-
}
|
588
777
|
|
589
778
|
// Backward compatibility
|
590
779
|
async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
@@ -593,51 +782,127 @@ export default class BXO {
|
|
593
782
|
|
594
783
|
// Server status
|
595
784
|
isServerRunning(): boolean {
|
596
|
-
return this.isRunning;
|
785
|
+
return this.isRunning && this.server !== undefined;
|
597
786
|
}
|
598
787
|
|
599
|
-
getServerInfo(): { running: boolean
|
788
|
+
getServerInfo(): { running: boolean } {
|
600
789
|
return {
|
601
|
-
running: this.isRunning
|
602
|
-
hotReload: this.hotReloadEnabled,
|
603
|
-
watchedFiles: Array.from(this.watchedFiles),
|
604
|
-
excludePatterns: Array.from(this.watchedExclude)
|
790
|
+
running: this.isRunning
|
605
791
|
};
|
606
792
|
}
|
607
793
|
|
608
794
|
// Get server information (alias for getServerInfo)
|
609
795
|
get info() {
|
796
|
+
// Calculate total routes including plugins
|
797
|
+
const totalRoutes = this._routes.length + this.plugins.reduce((total, plugin) => total + plugin._routes.length, 0);
|
798
|
+
const totalWsRoutes = this._wsRoutes.length + this.plugins.reduce((total, plugin) => total + plugin._wsRoutes.length, 0);
|
799
|
+
|
610
800
|
return {
|
611
|
-
|
612
|
-
|
801
|
+
// Server status
|
802
|
+
running: this.isRunning,
|
803
|
+
server: this.server ? 'Bun' : null,
|
804
|
+
|
805
|
+
// Connection details
|
806
|
+
hostname: this.serverHostname,
|
807
|
+
port: this.serverPort,
|
808
|
+
url: this.isRunning && this.serverHostname && this.serverPort
|
809
|
+
? `http://${this.serverHostname}:${this.serverPort}`
|
810
|
+
: null,
|
811
|
+
|
812
|
+
// Application statistics
|
813
|
+
totalRoutes,
|
814
|
+
totalWsRoutes,
|
613
815
|
totalPlugins: this.plugins.length,
|
614
|
-
|
816
|
+
|
817
|
+
// System information
|
818
|
+
runtime: 'Bun',
|
819
|
+
version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
|
820
|
+
pid: process.pid,
|
821
|
+
uptime: this.isRunning ? process.uptime() : 0
|
615
822
|
};
|
616
823
|
}
|
617
824
|
|
618
825
|
// Get all routes information
|
619
826
|
get routes() {
|
620
|
-
|
827
|
+
// Get routes from main instance
|
828
|
+
const mainRoutes = this._routes.map((route: Route) => ({
|
621
829
|
method: route.method,
|
622
830
|
path: route.path,
|
623
831
|
hasConfig: !!route.config,
|
624
|
-
config: route.config
|
625
|
-
|
626
|
-
hasQuery: !!route.config.query,
|
627
|
-
hasBody: !!route.config.body,
|
628
|
-
hasHeaders: !!route.config.headers,
|
629
|
-
hasResponse: !!route.config.response
|
630
|
-
} : null
|
832
|
+
config: route.config || null,
|
833
|
+
source: 'main' as const
|
631
834
|
}));
|
835
|
+
|
836
|
+
// Get routes from all plugins
|
837
|
+
const pluginRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
|
838
|
+
plugin._routes.map((route: Route) => ({
|
839
|
+
method: route.method,
|
840
|
+
path: route.path,
|
841
|
+
hasConfig: !!route.config,
|
842
|
+
config: route.config || null,
|
843
|
+
source: 'plugin' as const,
|
844
|
+
pluginIndex
|
845
|
+
}))
|
846
|
+
);
|
847
|
+
|
848
|
+
return [...mainRoutes, ...pluginRoutes];
|
632
849
|
}
|
850
|
+
|
851
|
+
// Get all WebSocket routes information
|
852
|
+
get wsRoutes() {
|
853
|
+
// Get WebSocket routes from main instance
|
854
|
+
const mainWsRoutes = this._wsRoutes.map((route: WSRoute) => ({
|
855
|
+
path: route.path,
|
856
|
+
hasHandlers: {
|
857
|
+
onOpen: !!route.handler.onOpen,
|
858
|
+
onMessage: !!route.handler.onMessage,
|
859
|
+
onClose: !!route.handler.onClose,
|
860
|
+
onError: !!route.handler.onError
|
861
|
+
},
|
862
|
+
source: 'main' as const
|
863
|
+
}));
|
864
|
+
|
865
|
+
// Get WebSocket routes from all plugins
|
866
|
+
const pluginWsRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
|
867
|
+
plugin._wsRoutes.map((route: WSRoute) => ({
|
868
|
+
path: route.path,
|
869
|
+
hasHandlers: {
|
870
|
+
onOpen: !!route.handler.onOpen,
|
871
|
+
onMessage: !!route.handler.onMessage,
|
872
|
+
onClose: !!route.handler.onClose,
|
873
|
+
onError: !!route.handler.onError
|
874
|
+
},
|
875
|
+
source: 'plugin' as const,
|
876
|
+
pluginIndex
|
877
|
+
}))
|
878
|
+
);
|
879
|
+
|
880
|
+
return [...mainWsRoutes, ...pluginWsRoutes];
|
881
|
+
}
|
882
|
+
}
|
883
|
+
|
884
|
+
const error = (error: Error | string, status: number = 500) => {
|
885
|
+
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
|
633
886
|
}
|
634
887
|
|
635
|
-
|
636
|
-
|
888
|
+
// File helper function (like Elysia)
|
889
|
+
const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
|
890
|
+
const bunFile = Bun.file(path);
|
891
|
+
|
892
|
+
if (options?.type) {
|
893
|
+
// Create a wrapper to override the MIME type
|
894
|
+
return {
|
895
|
+
...bunFile,
|
896
|
+
type: options.type,
|
897
|
+
headers: options.headers
|
898
|
+
};
|
899
|
+
}
|
900
|
+
|
901
|
+
return bunFile;
|
637
902
|
}
|
638
903
|
|
639
904
|
// Export Zod for convenience
|
640
|
-
export { z, error };
|
905
|
+
export { z, error, file };
|
641
906
|
|
642
907
|
// Export types for external use
|
643
|
-
export type { RouteConfig, Handler };
|
908
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
|
package/package.json
CHANGED
package/plugins/auth.ts
DELETED
@@ -1,119 +0,0 @@
|
|
1
|
-
import BXO from '../index';
|
2
|
-
|
3
|
-
interface AuthOptions {
|
4
|
-
type: 'jwt' | 'bearer' | 'apikey';
|
5
|
-
secret?: string;
|
6
|
-
header?: string;
|
7
|
-
verify?: (token: string, ctx: any) => Promise<any> | any;
|
8
|
-
exclude?: string[];
|
9
|
-
}
|
10
|
-
|
11
|
-
export function auth(options: AuthOptions): BXO {
|
12
|
-
const {
|
13
|
-
type,
|
14
|
-
secret,
|
15
|
-
header = 'authorization',
|
16
|
-
verify,
|
17
|
-
exclude = []
|
18
|
-
} = options;
|
19
|
-
|
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);
|
31
|
-
}
|
32
|
-
return pathname === path || pathname.startsWith(path);
|
33
|
-
})) {
|
34
|
-
return;
|
35
|
-
}
|
36
|
-
|
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>' }), {
|
51
|
-
status: 401,
|
52
|
-
headers: { 'Content-Type': 'application/json' }
|
53
|
-
});
|
54
|
-
}
|
55
|
-
token = authHeader.slice(7);
|
56
|
-
} else if (type === 'apikey') {
|
57
|
-
token = authHeader;
|
58
|
-
} else {
|
59
|
-
token = authHeader;
|
60
|
-
}
|
61
|
-
|
62
|
-
try {
|
63
|
-
let user: any;
|
64
|
-
|
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');
|
72
|
-
}
|
73
|
-
|
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');
|
79
|
-
}
|
80
|
-
|
81
|
-
user = payload;
|
82
|
-
} else {
|
83
|
-
user = { token };
|
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
|
-
});
|
95
|
-
}
|
96
|
-
});
|
97
|
-
|
98
|
-
return authInstance;
|
99
|
-
}
|
100
|
-
|
101
|
-
// Helper function for creating JWT tokens (simple implementation)
|
102
|
-
export function createJWT(payload: any, secret: string, expiresIn: number = 3600): string {
|
103
|
-
const header = { alg: 'HS256', typ: 'JWT' };
|
104
|
-
const now = Math.floor(Date.now() / 1000);
|
105
|
-
|
106
|
-
const jwtPayload = {
|
107
|
-
...payload,
|
108
|
-
iat: now,
|
109
|
-
exp: now + expiresIn
|
110
|
-
};
|
111
|
-
|
112
|
-
const headerB64 = btoa(JSON.stringify(header));
|
113
|
-
const payloadB64 = btoa(JSON.stringify(jwtPayload));
|
114
|
-
|
115
|
-
// Simple signature (in production, use proper HMAC-SHA256)
|
116
|
-
const signature = btoa(`${headerB64}.${payloadB64}.${secret}`);
|
117
|
-
|
118
|
-
return `${headerB64}.${payloadB64}.${signature}`;
|
119
|
-
}
|
package/plugins/logger.ts
DELETED
@@ -1,109 +0,0 @@
|
|
1
|
-
import BXO from '../index';
|
2
|
-
|
3
|
-
interface LoggerOptions {
|
4
|
-
format?: 'simple' | 'detailed' | 'json';
|
5
|
-
includeBody?: boolean;
|
6
|
-
includeHeaders?: boolean;
|
7
|
-
}
|
8
|
-
|
9
|
-
export function logger(options: LoggerOptions = {}): BXO {
|
10
|
-
const {
|
11
|
-
format = 'simple',
|
12
|
-
includeBody = false,
|
13
|
-
includeHeaders = false
|
14
|
-
} = options;
|
15
|
-
|
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
|
-
}
|
32
|
-
|
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);
|
45
|
-
}
|
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
|
-
};
|
64
|
-
|
65
|
-
if (includeHeaders && ctx.set.headers) {
|
66
|
-
logData.responseHeaders = ctx.set.headers;
|
67
|
-
}
|
68
|
-
|
69
|
-
if (includeBody && response) {
|
70
|
-
logData.response = response;
|
71
|
-
}
|
72
|
-
|
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);
|
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`);
|
86
|
-
}
|
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;
|
109
|
-
}
|