bxo 0.0.1

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.
@@ -0,0 +1,111 @@
1
+ ---
2
+ description: Use Bun instead of Node.js, npm, pnpm, or vite.
3
+ globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
4
+ alwaysApply: false
5
+ ---
6
+
7
+ Default to using Bun instead of Node.js.
8
+
9
+ - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
10
+ - Use `bun test` instead of `jest` or `vitest`
11
+ - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
12
+ - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
13
+ - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
14
+ - Bun automatically loads .env, so don't use dotenv.
15
+
16
+ ## APIs
17
+
18
+ - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
19
+ - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
20
+ - `Bun.redis` for Redis. Don't use `ioredis`.
21
+ - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
22
+ - `WebSocket` is built-in. Don't use `ws`.
23
+ - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
24
+ - Bun.$`ls` instead of execa.
25
+
26
+ ## Testing
27
+
28
+ Use `bun test` to run tests.
29
+
30
+ ```ts#index.test.ts
31
+ import { test, expect } from "bun:test";
32
+
33
+ test("hello world", () => {
34
+ expect(1).toBe(1);
35
+ });
36
+ ```
37
+
38
+ ## Frontend
39
+
40
+ Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
41
+
42
+ Server:
43
+
44
+ ```ts#index.ts
45
+ import index from "./index.html"
46
+
47
+ Bun.serve({
48
+ routes: {
49
+ "/": index,
50
+ "/api/users/:id": {
51
+ GET: (req) => {
52
+ return new Response(JSON.stringify({ id: req.params.id }));
53
+ },
54
+ },
55
+ },
56
+ // optional websocket support
57
+ websocket: {
58
+ open: (ws) => {
59
+ ws.send("Hello, world!");
60
+ },
61
+ message: (ws, message) => {
62
+ ws.send(message);
63
+ },
64
+ close: (ws) => {
65
+ // handle close
66
+ }
67
+ },
68
+ development: {
69
+ hmr: true,
70
+ console: true,
71
+ }
72
+ })
73
+ ```
74
+
75
+ HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
76
+
77
+ ```html#index.html
78
+ <html>
79
+ <body>
80
+ <h1>Hello, world!</h1>
81
+ <script type="module" src="./frontend.tsx"></script>
82
+ </body>
83
+ </html>
84
+ ```
85
+
86
+ With the following `frontend.tsx`:
87
+
88
+ ```tsx#frontend.tsx
89
+ import React from "react";
90
+
91
+ // import .css files directly and it works
92
+ import './index.css';
93
+
94
+ import { createRoot } from "react-dom/client";
95
+
96
+ const root = createRoot(document.body);
97
+
98
+ export default function Frontend() {
99
+ return <h1>Hello, world!</h1>;
100
+ }
101
+
102
+ root.render(<Frontend />);
103
+ ```
104
+
105
+ Then, run index.ts
106
+
107
+ ```sh
108
+ bun --hot ./index.ts
109
+ ```
110
+
111
+ For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # XOXO
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.2.18. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
package/example.ts ADDED
@@ -0,0 +1,137 @@
1
+ import BXO, { z } from './index';
2
+ import { cors, logger, auth, rateLimit, createJWT } from './plugins';
3
+
4
+ // Create the app instance
5
+ const app = new BXO();
6
+
7
+ // Add plugins
8
+ app
9
+ .use(logger({ format: 'simple' }))
10
+ .use(cors({
11
+ origin: ['http://localhost:3000', 'https://example.com'],
12
+ credentials: true
13
+ }))
14
+ .use(rateLimit({
15
+ max: 100,
16
+ window: 60, // 1 minute
17
+ exclude: ['/health']
18
+ }))
19
+ .use(auth({
20
+ type: 'jwt',
21
+ secret: 'your-secret-key',
22
+ exclude: ['/', '/login', '/health']
23
+ }));
24
+
25
+ // Add lifecycle hooks
26
+ app
27
+ .onStart(() => {
28
+ console.log('🚀 Server starting up...');
29
+ })
30
+ .onStop(() => {
31
+ console.log('🛑 Server shutting down...');
32
+ })
33
+ .onRequest((ctx) => {
34
+ console.log(`📨 Processing ${ctx.request.method} ${ctx.request.url}`);
35
+ })
36
+ .onResponse((ctx, response) => {
37
+ console.log(`📤 Response sent for ${ctx.request.method} ${ctx.request.url}`);
38
+ return response;
39
+ })
40
+ .onError((ctx, error) => {
41
+ console.error(`💥 Error in ${ctx.request.method} ${ctx.request.url}:`, error.message);
42
+ return { error: 'Something went wrong', timestamp: new Date().toISOString() };
43
+ });
44
+
45
+ // Routes exactly like your example
46
+ app
47
+ // Two arguments: path, handler
48
+ .get('/simple', async (ctx) => {
49
+ return { message: 'Hello World' };
50
+ })
51
+
52
+ // Three arguments: path, handler, config
53
+ .get('/users/:id', async (ctx) => {
54
+ // ctx.params.id is fully typed as string (UUID)
55
+ // ctx.query.include is typed as string | undefined
56
+ return { user: { id: ctx.params.id, include: ctx.query.include } };
57
+ }, {
58
+ params: z.object({ id: z.string().uuid() }),
59
+ query: z.object({ include: z.string().optional() })
60
+ })
61
+
62
+ .post('/users', async (ctx) => {
63
+ // ctx.body is fully typed with name: string, email: string
64
+ return { created: ctx.body };
65
+ }, {
66
+ body: z.object({
67
+ name: z.string(),
68
+ email: z.string().email()
69
+ })
70
+ })
71
+
72
+ // Additional examples
73
+ .get('/health', async (ctx) => {
74
+ return { status: 'ok', timestamp: new Date().toISOString() };
75
+ })
76
+
77
+ .post('/login', async (ctx) => {
78
+ const { username, password } = ctx.body;
79
+
80
+ // Simple auth check (in production, verify against database)
81
+ if (username === 'admin' && password === 'password') {
82
+ const token = createJWT({ username, role: 'admin' }, 'your-secret-key', 3600);
83
+ return { token, user: { username, role: 'admin' } };
84
+ }
85
+
86
+ ctx.set.status = 401;
87
+ return { error: 'Invalid credentials' };
88
+ }, {
89
+ body: z.object({
90
+ username: z.string(),
91
+ password: z.string()
92
+ })
93
+ })
94
+
95
+ .get('/protected', async (ctx) => {
96
+ // ctx.user is available here because of auth plugin
97
+ return { message: 'This is protected', user: ctx.user };
98
+ })
99
+
100
+ .put('/users/:id', async (ctx) => {
101
+ return {
102
+ updated: ctx.body,
103
+ id: ctx.params.id,
104
+ version: ctx.headers['if-match']
105
+ };
106
+ }, {
107
+ params: z.object({ id: z.string().uuid() }),
108
+ body: z.object({
109
+ name: z.string().optional(),
110
+ email: z.string().email().optional()
111
+ }),
112
+ headers: z.object({
113
+ 'if-match': z.string()
114
+ })
115
+ })
116
+
117
+ .delete('/users/:id', async (ctx) => {
118
+ ctx.set.status = 204;
119
+ return null;
120
+ }, {
121
+ params: z.object({ id: z.string().uuid() })
122
+ });
123
+
124
+ // Start the server
125
+ app.listen(3000, 'localhost');
126
+
127
+ console.log(`
128
+ 🦊 BXO Framework Example
129
+
130
+ Try these endpoints:
131
+ - GET /simple
132
+ - GET /users/123e4567-e89b-12d3-a456-426614174000?include=profile
133
+ - POST /users (with JSON body: {"name": "John", "email": "john@example.com"})
134
+ - GET /health
135
+ - POST /login (with JSON body: {"username": "admin", "password": "password"})
136
+ - GET /protected (requires Bearer token from /login)
137
+ `);
package/index.ts ADDED
@@ -0,0 +1,409 @@
1
+ import { z } from 'zod';
2
+
3
+ // Type utilities for extracting types from Zod schemas
4
+ type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
5
+
6
+ // Configuration interface for route handlers
7
+ interface RouteConfig {
8
+ params?: z.ZodSchema<any>;
9
+ query?: z.ZodSchema<any>;
10
+ body?: z.ZodSchema<any>;
11
+ headers?: z.ZodSchema<any>;
12
+ }
13
+
14
+ // Context type that's fully typed based on the route configuration
15
+ export type Context<TConfig extends RouteConfig = {}> = {
16
+ params: TConfig['params'] extends z.ZodSchema<any> ? InferZodType<TConfig['params']> : Record<string, string>;
17
+ query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
18
+ body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
19
+ headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
20
+ request: Request;
21
+ set: {
22
+ status?: number;
23
+ headers?: Record<string, string>;
24
+ };
25
+ // Extended properties that can be added by plugins
26
+ user?: any;
27
+ [key: string]: any;
28
+ };
29
+
30
+ // Handler function type
31
+ type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
32
+
33
+ // Plugin interface (also exported from plugins/index.ts)
34
+ interface Plugin {
35
+ name?: string;
36
+ onRequest?: (ctx: Context) => Promise<void> | void;
37
+ onResponse?: (ctx: Context, response: any) => Promise<any> | any;
38
+ onError?: (ctx: Context, error: Error) => Promise<any> | any;
39
+ }
40
+
41
+
42
+
43
+ // Route definition
44
+ interface Route {
45
+ method: string;
46
+ path: string;
47
+ handler: Handler<any>;
48
+ config?: RouteConfig;
49
+ }
50
+
51
+ // Lifecycle hooks
52
+ interface LifecycleHooks {
53
+ onStart?: () => Promise<void> | void;
54
+ onStop?: () => Promise<void> | void;
55
+ onRequest?: (ctx: Context) => Promise<void> | void;
56
+ onResponse?: (ctx: Context, response: any) => Promise<any> | any;
57
+ onError?: (ctx: Context, error: Error) => Promise<any> | any;
58
+ }
59
+
60
+ export default class BXO {
61
+ private routes: Route[] = [];
62
+ private plugins: Plugin[] = [];
63
+ private hooks: LifecycleHooks = {};
64
+
65
+ constructor() {}
66
+
67
+ // Lifecycle hook methods
68
+ onStart(handler: () => Promise<void> | void): this {
69
+ this.hooks.onStart = handler;
70
+ return this;
71
+ }
72
+
73
+ onStop(handler: () => Promise<void> | void): this {
74
+ this.hooks.onStop = handler;
75
+ return this;
76
+ }
77
+
78
+ onRequest(handler: (ctx: Context) => Promise<void> | void): this {
79
+ this.hooks.onRequest = handler;
80
+ return this;
81
+ }
82
+
83
+ onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
84
+ this.hooks.onResponse = handler;
85
+ return this;
86
+ }
87
+
88
+ onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
89
+ this.hooks.onError = handler;
90
+ return this;
91
+ }
92
+
93
+ // Plugin system
94
+ use(plugin: Plugin): this {
95
+ this.plugins.push(plugin);
96
+ return this;
97
+ }
98
+
99
+ // HTTP method handlers with overloads for type safety
100
+ get<TConfig extends RouteConfig = {}>(
101
+ path: string,
102
+ handler: Handler<TConfig>
103
+ ): this;
104
+ get<TConfig extends RouteConfig = {}>(
105
+ path: string,
106
+ handler: Handler<TConfig>,
107
+ config: TConfig
108
+ ): this;
109
+ get<TConfig extends RouteConfig = {}>(
110
+ path: string,
111
+ handler: Handler<TConfig>,
112
+ config?: TConfig
113
+ ): this {
114
+ this.routes.push({ method: 'GET', path, handler, config });
115
+ return this;
116
+ }
117
+
118
+ post<TConfig extends RouteConfig = {}>(
119
+ path: string,
120
+ handler: Handler<TConfig>
121
+ ): this;
122
+ post<TConfig extends RouteConfig = {}>(
123
+ path: string,
124
+ handler: Handler<TConfig>,
125
+ config: TConfig
126
+ ): this;
127
+ post<TConfig extends RouteConfig = {}>(
128
+ path: string,
129
+ handler: Handler<TConfig>,
130
+ config?: TConfig
131
+ ): this {
132
+ this.routes.push({ method: 'POST', path, handler, config });
133
+ return this;
134
+ }
135
+
136
+ put<TConfig extends RouteConfig = {}>(
137
+ path: string,
138
+ handler: Handler<TConfig>
139
+ ): this;
140
+ put<TConfig extends RouteConfig = {}>(
141
+ path: string,
142
+ handler: Handler<TConfig>,
143
+ config: TConfig
144
+ ): this;
145
+ put<TConfig extends RouteConfig = {}>(
146
+ path: string,
147
+ handler: Handler<TConfig>,
148
+ config?: TConfig
149
+ ): this {
150
+ this.routes.push({ method: 'PUT', path, handler, config });
151
+ return this;
152
+ }
153
+
154
+ delete<TConfig extends RouteConfig = {}>(
155
+ path: string,
156
+ handler: Handler<TConfig>
157
+ ): this;
158
+ delete<TConfig extends RouteConfig = {}>(
159
+ path: string,
160
+ handler: Handler<TConfig>,
161
+ config: TConfig
162
+ ): this;
163
+ delete<TConfig extends RouteConfig = {}>(
164
+ path: string,
165
+ handler: Handler<TConfig>,
166
+ config?: TConfig
167
+ ): this {
168
+ this.routes.push({ method: 'DELETE', path, handler, config });
169
+ return this;
170
+ }
171
+
172
+ patch<TConfig extends RouteConfig = {}>(
173
+ path: string,
174
+ handler: Handler<TConfig>
175
+ ): this;
176
+ patch<TConfig extends RouteConfig = {}>(
177
+ path: string,
178
+ handler: Handler<TConfig>,
179
+ config: TConfig
180
+ ): this;
181
+ patch<TConfig extends RouteConfig = {}>(
182
+ path: string,
183
+ handler: Handler<TConfig>,
184
+ config?: TConfig
185
+ ): this {
186
+ this.routes.push({ method: 'PATCH', path, handler, config });
187
+ return this;
188
+ }
189
+
190
+ // Route matching utility
191
+ private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
192
+ for (const route of this.routes) {
193
+ if (route.method !== method) continue;
194
+
195
+ const routeSegments = route.path.split('/').filter(Boolean);
196
+ const pathSegments = pathname.split('/').filter(Boolean);
197
+
198
+ if (routeSegments.length !== pathSegments.length) continue;
199
+
200
+ const params: Record<string, string> = {};
201
+ let isMatch = true;
202
+
203
+ for (let i = 0; i < routeSegments.length; i++) {
204
+ const routeSegment = routeSegments[i];
205
+ const pathSegment = pathSegments[i];
206
+
207
+ if (!routeSegment || !pathSegment) {
208
+ isMatch = false;
209
+ break;
210
+ }
211
+
212
+ if (routeSegment.startsWith(':')) {
213
+ const paramName = routeSegment.slice(1);
214
+ params[paramName] = decodeURIComponent(pathSegment);
215
+ } else if (routeSegment !== pathSegment) {
216
+ isMatch = false;
217
+ break;
218
+ }
219
+ }
220
+
221
+ if (isMatch) {
222
+ return { route, params };
223
+ }
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ // Parse query string
230
+ private parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
231
+ const query: Record<string, string | undefined> = {};
232
+ for (const [key, value] of searchParams.entries()) {
233
+ query[key] = value;
234
+ }
235
+ return query;
236
+ }
237
+
238
+ // Parse headers
239
+ private parseHeaders(headers: Headers): Record<string, string> {
240
+ const headerObj: Record<string, string> = {};
241
+ for (const [key, value] of headers.entries()) {
242
+ headerObj[key] = value;
243
+ }
244
+ return headerObj;
245
+ }
246
+
247
+ // Validate data against Zod schema
248
+ private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
249
+ if (!schema) return data;
250
+ return schema.parse(data);
251
+ }
252
+
253
+ // Main request handler
254
+ private async handleRequest(request: Request): Promise<Response> {
255
+ const url = new URL(request.url);
256
+ const method = request.method;
257
+ const pathname = url.pathname;
258
+
259
+ const matchResult = this.matchRoute(method, pathname);
260
+ if (!matchResult) {
261
+ return new Response('Not Found', { status: 404 });
262
+ }
263
+
264
+ const { route, params } = matchResult;
265
+ const query = this.parseQuery(url.searchParams);
266
+ const headers = this.parseHeaders(request.headers);
267
+
268
+ let body: any;
269
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
270
+ const contentType = request.headers.get('content-type');
271
+ if (contentType?.includes('application/json')) {
272
+ try {
273
+ body = await request.json();
274
+ } catch {
275
+ body = {};
276
+ }
277
+ } else if (contentType?.includes('application/x-www-form-urlencoded')) {
278
+ const formData = await request.formData();
279
+ body = Object.fromEntries(formData.entries());
280
+ } else {
281
+ body = await request.text();
282
+ }
283
+ }
284
+
285
+ // Create context
286
+ const ctx: Context = {
287
+ params: route.config?.params ? this.validateData(route.config.params, params) : params,
288
+ query: route.config?.query ? this.validateData(route.config.query, query) : query,
289
+ body: route.config?.body ? this.validateData(route.config.body, body) : body,
290
+ headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
291
+ request,
292
+ set: {}
293
+ };
294
+
295
+ try {
296
+ // Run global onRequest hook
297
+ if (this.hooks.onRequest) {
298
+ await this.hooks.onRequest(ctx);
299
+ }
300
+
301
+ // Run plugin onRequest hooks
302
+ for (const plugin of this.plugins) {
303
+ if (plugin.onRequest) {
304
+ await plugin.onRequest(ctx);
305
+ }
306
+ }
307
+
308
+ // Execute route handler
309
+ let response = await route.handler(ctx);
310
+
311
+ // Run global onResponse hook
312
+ if (this.hooks.onResponse) {
313
+ response = await this.hooks.onResponse(ctx, response) || response;
314
+ }
315
+
316
+ // Run plugin onResponse hooks
317
+ for (const plugin of this.plugins) {
318
+ if (plugin.onResponse) {
319
+ response = await plugin.onResponse(ctx, response) || response;
320
+ }
321
+ }
322
+
323
+ // Convert response to Response object
324
+ if (response instanceof Response) {
325
+ return response;
326
+ }
327
+
328
+ const responseInit: ResponseInit = {
329
+ status: ctx.set.status || 200,
330
+ headers: ctx.set.headers || {}
331
+ };
332
+
333
+ if (typeof response === 'string') {
334
+ return new Response(response, responseInit);
335
+ }
336
+
337
+ return new Response(JSON.stringify(response), {
338
+ ...responseInit,
339
+ headers: {
340
+ 'Content-Type': 'application/json',
341
+ ...responseInit.headers
342
+ }
343
+ });
344
+
345
+ } catch (error) {
346
+ // Run error hooks
347
+ let errorResponse: any;
348
+
349
+ if (this.hooks.onError) {
350
+ errorResponse = await this.hooks.onError(ctx, error as Error);
351
+ }
352
+
353
+ for (const plugin of this.plugins) {
354
+ if (plugin.onError) {
355
+ errorResponse = await plugin.onError(ctx, error as Error) || errorResponse;
356
+ }
357
+ }
358
+
359
+ if (errorResponse) {
360
+ if (errorResponse instanceof Response) {
361
+ return errorResponse;
362
+ }
363
+ return new Response(JSON.stringify(errorResponse), {
364
+ status: 500,
365
+ headers: { 'Content-Type': 'application/json' }
366
+ });
367
+ }
368
+
369
+ // Default error response
370
+ const errorMessage = error instanceof Error ? error.message : 'Internal Server Error';
371
+ return new Response(JSON.stringify({ error: errorMessage }), {
372
+ status: 500,
373
+ headers: { 'Content-Type': 'application/json' }
374
+ });
375
+ }
376
+ }
377
+
378
+ // Start the server
379
+ async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
380
+ if (this.hooks.onStart) {
381
+ await this.hooks.onStart();
382
+ }
383
+
384
+ const server = Bun.serve({
385
+ port,
386
+ hostname,
387
+ fetch: (request) => this.handleRequest(request),
388
+ });
389
+
390
+ console.log(`🦊 BXO server running at http://${hostname}:${port}`);
391
+
392
+ // Handle graceful shutdown
393
+ process.on('SIGINT', async () => {
394
+ if (this.hooks.onStop) {
395
+ await this.hooks.onStop();
396
+ }
397
+ server.stop();
398
+ process.exit(0);
399
+ });
400
+ }
401
+ }
402
+
403
+ // Export Zod for convenience
404
+ export { z };
405
+
406
+ export type { Plugin } from './plugins';
407
+
408
+ // Export types for external use
409
+ export type { RouteConfig };
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "bxo",
3
+ "module": "index.ts",
4
+ "version": "0.0.1",
5
+ "description": "A simple and lightweight web framework for Bun",
6
+ "type": "module",
7
+ "devDependencies": {
8
+ "@types/bun": "latest"
9
+ },
10
+ "exports": {
11
+ ".": "./index.ts",
12
+ "./plugins": "./plugins/index.ts"
13
+ },
14
+ "peerDependencies": {
15
+ "typescript": "^5"
16
+ },
17
+ "dependencies": {
18
+ "zod": "^4.0.5"
19
+ }
20
+ }
@@ -0,0 +1,116 @@
1
+ interface AuthOptions {
2
+ type: 'jwt' | 'bearer' | 'apikey';
3
+ secret?: string;
4
+ header?: string;
5
+ verify?: (token: string, ctx: any) => Promise<any> | any;
6
+ exclude?: string[];
7
+ }
8
+
9
+ export function auth(options: AuthOptions) {
10
+ const {
11
+ type,
12
+ secret,
13
+ header = 'authorization',
14
+ verify,
15
+ exclude = []
16
+ } = options;
17
+
18
+ return {
19
+ name: 'auth',
20
+ onRequest: async (ctx: any) => {
21
+ const url = new URL(ctx.request.url);
22
+ const pathname = url.pathname;
23
+
24
+ // Skip auth for excluded paths
25
+ if (exclude.some(path => {
26
+ if (path.includes('*')) {
27
+ const regex = new RegExp(path.replace(/\*/g, '.*'));
28
+ return regex.test(pathname);
29
+ }
30
+ return pathname === path || pathname.startsWith(path);
31
+ })) {
32
+ return;
33
+ }
34
+
35
+ const authHeader = ctx.request.headers.get(header.toLowerCase());
36
+
37
+ if (!authHeader) {
38
+ throw new Response(JSON.stringify({ error: 'Authorization header required' }), {
39
+ status: 401,
40
+ headers: { 'Content-Type': 'application/json' }
41
+ });
42
+ }
43
+
44
+ let token: string;
45
+
46
+ if (type === 'jwt' || type === 'bearer') {
47
+ if (!authHeader.startsWith('Bearer ')) {
48
+ throw new Response(JSON.stringify({ error: 'Invalid authorization format. Use Bearer <token>' }), {
49
+ status: 401,
50
+ headers: { 'Content-Type': 'application/json' }
51
+ });
52
+ }
53
+ token = authHeader.slice(7);
54
+ } else if (type === 'apikey') {
55
+ token = authHeader;
56
+ } else {
57
+ token = authHeader;
58
+ }
59
+
60
+ try {
61
+ let user: any;
62
+
63
+ if (verify) {
64
+ user = await verify(token, ctx);
65
+ } else if (type === 'jwt' && secret) {
66
+ // Simple JWT verification (in production, use a proper JWT library)
67
+ const [headerB64, payloadB64, signature] = token.split('.');
68
+ if (!headerB64 || !payloadB64 || !signature) {
69
+ throw new Error('Invalid JWT format');
70
+ }
71
+
72
+ const payload = JSON.parse(atob(payloadB64));
73
+
74
+ // Check expiration
75
+ if (payload.exp && Date.now() >= payload.exp * 1000) {
76
+ throw new Error('Token expired');
77
+ }
78
+
79
+ user = payload;
80
+ } else {
81
+ user = { token };
82
+ }
83
+
84
+ // Attach user to context
85
+ ctx.user = user;
86
+
87
+ } catch (error) {
88
+ const message = error instanceof Error ? error.message : 'Invalid token';
89
+ throw new Response(JSON.stringify({ error: message }), {
90
+ status: 401,
91
+ headers: { 'Content-Type': 'application/json' }
92
+ });
93
+ }
94
+ }
95
+ };
96
+ }
97
+
98
+ // Helper function for creating JWT tokens (simple implementation)
99
+ export function createJWT(payload: any, secret: string, expiresIn: number = 3600): string {
100
+ const header = { alg: 'HS256', typ: 'JWT' };
101
+ const now = Math.floor(Date.now() / 1000);
102
+
103
+ const jwtPayload = {
104
+ ...payload,
105
+ iat: now,
106
+ exp: now + expiresIn
107
+ };
108
+
109
+ const headerB64 = btoa(JSON.stringify(header));
110
+ const payloadB64 = btoa(JSON.stringify(jwtPayload));
111
+
112
+ // Simple signature (in production, use proper HMAC-SHA256)
113
+ const signature = btoa(`${headerB64}.${payloadB64}.${secret}`);
114
+
115
+ return `${headerB64}.${payloadB64}.${signature}`;
116
+ }
@@ -0,0 +1,79 @@
1
+ interface CORSOptions {
2
+ origin?: string | string[] | boolean;
3
+ methods?: string[];
4
+ allowedHeaders?: string[];
5
+ credentials?: boolean;
6
+ maxAge?: number;
7
+ }
8
+
9
+ export function cors(options: CORSOptions = {}) {
10
+ const {
11
+ origin = '*',
12
+ methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
13
+ allowedHeaders = ['Content-Type', 'Authorization'],
14
+ credentials = false,
15
+ maxAge = 86400
16
+ } = options;
17
+
18
+ return {
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();
47
+
48
+ ctx.set.status = 204;
49
+ ctx.set.headers = { ...ctx.set.headers, ...headers };
50
+
51
+ throw new Response(null, { status: 204, headers });
52
+ }
53
+ },
54
+ onResponse: async (ctx: any, response: any) => {
55
+ const headers: Record<string, string> = {};
56
+
57
+ // Handle origin for actual requests
58
+ if (typeof origin === 'boolean') {
59
+ if (origin) {
60
+ headers['Access-Control-Allow-Origin'] = ctx.request.headers.get('origin') || '*';
61
+ }
62
+ } else if (typeof origin === 'string') {
63
+ headers['Access-Control-Allow-Origin'] = origin;
64
+ } else if (Array.isArray(origin)) {
65
+ const requestOrigin = ctx.request.headers.get('origin');
66
+ if (requestOrigin && origin.includes(requestOrigin)) {
67
+ headers['Access-Control-Allow-Origin'] = requestOrigin;
68
+ }
69
+ }
70
+
71
+ if (credentials) {
72
+ headers['Access-Control-Allow-Credentials'] = 'true';
73
+ }
74
+
75
+ ctx.set.headers = { ...ctx.set.headers, ...headers };
76
+ return response;
77
+ }
78
+ };
79
+ }
@@ -0,0 +1,13 @@
1
+ // Export all plugins
2
+ export { cors } from './cors';
3
+ export { logger } from './logger';
4
+ export { auth, createJWT } from './auth';
5
+ export { rateLimit } from './ratelimit';
6
+
7
+ // Plugin types for convenience
8
+ export interface Plugin {
9
+ name?: string;
10
+ onRequest?: (ctx: any) => Promise<void> | void;
11
+ onResponse?: (ctx: any, response: any) => Promise<any> | any;
12
+ onError?: (ctx: any, error: Error) => Promise<any> | any;
13
+ }
@@ -0,0 +1,104 @@
1
+ interface LoggerOptions {
2
+ format?: 'simple' | 'detailed' | 'json';
3
+ includeBody?: boolean;
4
+ includeHeaders?: boolean;
5
+ }
6
+
7
+ export function logger(options: LoggerOptions = {}) {
8
+ const {
9
+ format = 'simple',
10
+ includeBody = false,
11
+ includeHeaders = false
12
+ } = options;
13
+
14
+ return {
15
+ name: 'logger',
16
+ onRequest: async (ctx: any) => {
17
+ ctx._startTime = Date.now();
18
+
19
+ if (format === 'json') {
20
+ const logData: any = {
21
+ timestamp: new Date().toISOString(),
22
+ method: ctx.request.method,
23
+ url: ctx.request.url,
24
+ type: 'request'
25
+ };
26
+
27
+ if (includeHeaders) {
28
+ logData.headers = Object.fromEntries(ctx.request.headers.entries());
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}`);
46
+ }
47
+ },
48
+ onResponse: async (ctx: any, response: any) => {
49
+ const duration = Date.now() - (ctx._startTime || 0);
50
+ const status = ctx.set.status || 200;
51
+
52
+ if (format === 'json') {
53
+ const logData: any = {
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`);
83
+ }
84
+
85
+ return response;
86
+ },
87
+ onError: async (ctx: any, error: Error) => {
88
+ const duration = Date.now() - (ctx._startTime || 0);
89
+
90
+ if (format === 'json') {
91
+ console.log(JSON.stringify({
92
+ timestamp: new Date().toISOString(),
93
+ method: ctx.request.method,
94
+ url: ctx.request.url,
95
+ error: error.message,
96
+ duration: `${duration}ms`,
97
+ type: 'error'
98
+ }));
99
+ } else {
100
+ console.log(`✗ ${ctx.request.method} ${ctx.request.url} \x1b[31mERROR\x1b[0m ${duration}ms: ${error.message}`);
101
+ }
102
+ }
103
+ };
104
+ }
@@ -0,0 +1,136 @@
1
+ interface RateLimitOptions {
2
+ max: number;
3
+ window: number; // in seconds
4
+ keyGenerator?: (ctx: any) => string;
5
+ skipSuccessful?: boolean;
6
+ skipFailed?: boolean;
7
+ exclude?: string[];
8
+ message?: string;
9
+ statusCode?: number;
10
+ }
11
+
12
+ class RateLimitStore {
13
+ private store = new Map<string, { count: number; resetTime: number }>();
14
+
15
+ get(key: string): { count: number; resetTime: number } | undefined {
16
+ const entry = this.store.get(key);
17
+ if (entry && Date.now() > entry.resetTime) {
18
+ this.store.delete(key);
19
+ return undefined;
20
+ }
21
+ return entry;
22
+ }
23
+
24
+ set(key: string, count: number, resetTime: number): void {
25
+ this.store.set(key, { count, resetTime });
26
+ }
27
+
28
+ increment(key: string, window: number): { count: number; resetTime: number } {
29
+ const now = Date.now();
30
+ const entry = this.get(key);
31
+
32
+ if (!entry) {
33
+ const resetTime = now + (window * 1000);
34
+ this.set(key, 1, resetTime);
35
+ return { count: 1, resetTime };
36
+ }
37
+
38
+ entry.count++;
39
+ this.set(key, entry.count, entry.resetTime);
40
+ return entry;
41
+ }
42
+
43
+ cleanup(): void {
44
+ const now = Date.now();
45
+ for (const [key, entry] of this.store.entries()) {
46
+ if (now > entry.resetTime) {
47
+ this.store.delete(key);
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ export function rateLimit(options: RateLimitOptions) {
54
+ const {
55
+ max,
56
+ window,
57
+ keyGenerator = (ctx) => {
58
+ // Default: use IP address
59
+ return ctx.request.headers.get('x-forwarded-for') ||
60
+ ctx.request.headers.get('x-real-ip') ||
61
+ 'unknown';
62
+ },
63
+ skipSuccessful = false,
64
+ skipFailed = false,
65
+ exclude = [],
66
+ message = 'Too many requests',
67
+ statusCode = 429
68
+ } = options;
69
+
70
+ const store = new RateLimitStore();
71
+
72
+ // Cleanup expired entries every 5 minutes
73
+ setInterval(() => store.cleanup(), 5 * 60 * 1000);
74
+
75
+ return {
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
+ }
91
+
92
+ const key = keyGenerator(ctx);
93
+ const entry = store.increment(key, window);
94
+
95
+ if (entry.count > max) {
96
+ const resetTime = Math.ceil(entry.resetTime / 1000);
97
+ throw new Response(JSON.stringify({
98
+ error: message,
99
+ retryAfter: resetTime - Math.floor(Date.now() / 1000)
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
+ });
110
+ }
111
+
112
+ // Add rate limit headers
113
+ ctx.set.headers = {
114
+ ...ctx.set.headers,
115
+ 'X-RateLimit-Limit': max.toString(),
116
+ 'X-RateLimit-Remaining': Math.max(0, max - entry.count).toString(),
117
+ 'X-RateLimit-Reset': Math.ceil(entry.resetTime / 1000).toString()
118
+ };
119
+ },
120
+ onResponse: async (ctx: any, response: any) => {
121
+ const status = ctx.set.status || 200;
122
+ const key = keyGenerator(ctx);
123
+
124
+ // Optionally skip counting successful or failed requests
125
+ if ((skipSuccessful && status < 400) || (skipFailed && status >= 400)) {
126
+ // Decrement the counter since we don't want to count this request
127
+ const entry = store.get(key);
128
+ if (entry && entry.count > 0) {
129
+ store.set(key, entry.count - 1, entry.resetTime);
130
+ }
131
+ }
132
+
133
+ return response;
134
+ }
135
+ };
136
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }