shokupan 0.6.0 → 0.6.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.
@@ -5,5 +5,7 @@ export interface ProxyOptions {
5
5
  changeOrigin?: boolean;
6
6
  ws?: boolean;
7
7
  headers?: Record<string, string>;
8
+ allowedHosts?: string[];
9
+ allowPrivateIPs?: boolean;
8
10
  }
9
11
  export declare function Proxy(options: ProxyOptions): Middleware;
@@ -10,5 +10,6 @@ export interface RateLimitOptions {
10
10
  keyGenerator?: (ctx: ShokupanContext) => string;
11
11
  skip?: (ctx: ShokupanContext) => boolean;
12
12
  mode?: 'user' | 'absolute';
13
+ trustedProxies?: string[];
13
14
  }
14
15
  export declare function RateLimitMiddleware(options?: RateLimitOptions): Middleware;
package/dist/router.d.ts CHANGED
@@ -1,10 +1,69 @@
1
1
  import { ShokupanContext } from './context';
2
2
  import { Shokupan } from './shokupan';
3
3
  import { $appRoot, $childControllers, $childRouters, $isApplication, $isMounted, $isRouter, $mountPath, $parent, $routes } from './symbol';
4
- import { GuardAPISpec, JSXRenderer, Method, MethodAPISpec, Middleware, OpenAPIOptions, ProcessResult, RequestOptions, RouteMetadata, ShokupanController, ShokupanHandler, ShokupanHooks, ShokupanRoute, ShokupanRouteConfig, StaticServeOptions } from './types';
4
+ import { GuardAPISpec, JSXRenderer, Method, MethodAPISpec, Middleware, OpenAPIOptions, ProcessResult, RequestOptions, RouteMetadata, RouteParams, ShokupanController, ShokupanHandler, ShokupanHooks, ShokupanRoute, ShokupanRouteConfig, StaticServeOptions } from './types';
5
5
  type HeadersInit = Headers | Record<string, string> | [string, string][];
6
6
  export declare const RouterRegistry: Map<string, ShokupanRouter<any>>;
7
7
  export declare const ShokupanApplicationTree: {};
8
+ /**
9
+ * Shokupan Router
10
+ *
11
+ * A router for organizing and grouping routes with shared middleware and configuration.
12
+ *
13
+ * @template State - The shape of `ctx.state` for all routes in this router.
14
+ * Provides type safety for state management within the router's middleware and handlers.
15
+ *
16
+ * @example Basic Router
17
+ * ```typescript
18
+ * const router = new ShokupanRouter();
19
+ * router.get('/users', (ctx) => ctx.json({ users: [] }));
20
+ *
21
+ * app.mount('/api', router);
22
+ * // Routes: GET /api/users
23
+ * ```
24
+ *
25
+ * @example Typed State Router
26
+ * ```typescript
27
+ * interface AuthState {
28
+ * userId: string;
29
+ * isAuthenticated: boolean;
30
+ * }
31
+ *
32
+ * class AuthRouter extends ShokupanRouter<AuthState> {
33
+ * constructor() {
34
+ * super();
35
+ *
36
+ * // Router middleware has typed state
37
+ * this.use((ctx, next) => {
38
+ * ctx.state.userId = 'user-123';
39
+ * ctx.state.isAuthenticated = true;
40
+ * return next();
41
+ * });
42
+ *
43
+ * this.get('/me', (ctx) => {
44
+ * // State is fully typed
45
+ * return ctx.json({ userId: ctx.state.userId });
46
+ * });
47
+ * }
48
+ * }
49
+ *
50
+ * app.mount('/auth', new AuthRouter());
51
+ * ```
52
+ *
53
+ * @example Router with Middleware
54
+ * ```typescript
55
+ * const apiRouter = new ShokupanRouter();
56
+ *
57
+ * // Router-level middleware
58
+ * apiRouter.use(async (ctx, next) => {
59
+ * console.log(`API request: ${ctx.method} ${ctx.path}`);
60
+ * return next();
61
+ * });
62
+ *
63
+ * apiRouter.get('/status', (ctx) => ctx.json({ status: 'ok' }));
64
+ * app.mount('/api', apiRouter);
65
+ * ```
66
+ */
8
67
  export declare class ShokupanRouter<T extends Record<string, any> = Record<string, any>> {
9
68
  readonly config?: ShokupanRouteConfig;
10
69
  private [$isApplication];
@@ -34,7 +93,7 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
34
93
  enableMiddlewareTracking?: boolean;
35
94
  middlewareTrackingMaxCapacity?: number;
36
95
  middlewareTrackingTTL?: number;
37
- httpLogger?: (ctx: ShokupanContext<Record<string, any>>) => void;
96
+ httpLogger?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void;
38
97
  logger?: {
39
98
  verbose?: boolean;
40
99
  info?: (msg: string, props: Record<string, any>) => void;
@@ -49,27 +108,27 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
49
108
  renderer?: JSXRenderer;
50
109
  serverFactory?: import('./types').ServerFactory;
51
110
  hooks?: {
52
- onError?: (ctx: ShokupanContext<Record<string, any>>, error: unknown) => void | Promise<void>;
53
- onRequestStart?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
54
- onRequestEnd?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
55
- onResponseStart?: (ctx: ShokupanContext<Record<string, any>>, response: Response) => void | Promise<void>;
56
- onResponseEnd?: (ctx: ShokupanContext<Record<string, any>>, response: Response) => void | Promise<void>;
57
- beforeValidate?: (ctx: ShokupanContext<Record<string, any>>, data: any) => void | Promise<void>;
58
- afterValidate?: (ctx: ShokupanContext<Record<string, any>>, data: any) => void | Promise<void>;
59
- onReadTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
60
- onWriteTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
61
- onRequestTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
111
+ onError?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, error: unknown) => void | Promise<void>;
112
+ onRequestStart?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
113
+ onRequestEnd?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
114
+ onResponseStart?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, response: Response) => void | Promise<void>;
115
+ onResponseEnd?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, response: Response) => void | Promise<void>;
116
+ beforeValidate?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, data: any) => void | Promise<void>;
117
+ afterValidate?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, data: any) => void | Promise<void>;
118
+ onReadTimeout?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
119
+ onWriteTimeout?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
120
+ onRequestTimeout?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
62
121
  } | {
63
- onError?: (ctx: ShokupanContext<Record<string, any>>, error: unknown) => void | Promise<void>;
64
- onRequestStart?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
65
- onRequestEnd?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
66
- onResponseStart?: (ctx: ShokupanContext<Record<string, any>>, response: Response) => void | Promise<void>;
67
- onResponseEnd?: (ctx: ShokupanContext<Record<string, any>>, response: Response) => void | Promise<void>;
68
- beforeValidate?: (ctx: ShokupanContext<Record<string, any>>, data: any) => void | Promise<void>;
69
- afterValidate?: (ctx: ShokupanContext<Record<string, any>>, data: any) => void | Promise<void>;
70
- onReadTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
71
- onWriteTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
72
- onRequestTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
122
+ onError?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, error: unknown) => void | Promise<void>;
123
+ onRequestStart?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
124
+ onRequestEnd?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
125
+ onResponseStart?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, response: Response) => void | Promise<void>;
126
+ onResponseEnd?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, response: Response) => void | Promise<void>;
127
+ beforeValidate?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, data: any) => void | Promise<void>;
128
+ afterValidate?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>, data: any) => void | Promise<void>;
129
+ onReadTimeout?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
130
+ onWriteTimeout?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
131
+ onRequestTimeout?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
73
132
  }[];
74
133
  validateStatusCodes?: boolean;
75
134
  };
@@ -196,7 +255,7 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
196
255
  * @param path - URL path
197
256
  * @param handlers - Route handler functions
198
257
  */
199
- get(path: string, ...handlers: ShokupanHandler<T>[]): any;
258
+ get<Path extends string>(path: Path, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
200
259
  /**
201
260
  * Adds a GET route to the router.
202
261
  *
@@ -204,14 +263,14 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
204
263
  * @param spec - OpenAPI specification for the route
205
264
  * @param handlers - Route handler functions
206
265
  */
207
- get(path: string, spec: MethodAPISpec, ...handlers: ShokupanHandler<T>[]): any;
266
+ get<Path extends string>(path: Path, spec: MethodAPISpec, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
208
267
  /**
209
268
  * Adds a POST route to the router.
210
269
  *
211
270
  * @param path - URL path
212
271
  * @param handlers - Route handler functions
213
272
  */
214
- post(path: string, ...handlers: ShokupanHandler<T>[]): any;
273
+ post<Path extends string>(path: Path, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
215
274
  /**
216
275
  * Adds a POST route to the router.
217
276
  *
@@ -219,14 +278,14 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
219
278
  * @param spec - OpenAPI specification for the route
220
279
  * @param handlers - Route handler functions
221
280
  */
222
- post(path: string, spec: MethodAPISpec, ...handlers: ShokupanHandler<T>[]): any;
281
+ post<Path extends string>(path: Path, spec: MethodAPISpec, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
223
282
  /**
224
283
  * Adds a PUT route to the router.
225
284
  *
226
285
  * @param path - URL path
227
286
  * @param handlers - Route handler functions
228
287
  */
229
- put(path: string, ...handlers: ShokupanHandler<T>[]): any;
288
+ put<Path extends string>(path: Path, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
230
289
  /**
231
290
  * Adds a PUT route to the router.
232
291
  *
@@ -234,14 +293,14 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
234
293
  * @param spec - OpenAPI specification for the route
235
294
  * @param handlers - Route handler functions
236
295
  */
237
- put(path: string, spec: MethodAPISpec, ...handlers: ShokupanHandler<T>[]): any;
296
+ put<Path extends string>(path: Path, spec: MethodAPISpec, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
238
297
  /**
239
298
  * Adds a DELETE route to the router.
240
299
  *
241
300
  * @param path - URL path
242
301
  * @param handlers - Route handler functions
243
302
  */
244
- delete(path: string, ...handlers: ShokupanHandler<T>[]): any;
303
+ delete<Path extends string>(path: Path, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
245
304
  /**
246
305
  * Adds a DELETE route to the router.
247
306
  *
@@ -249,14 +308,14 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
249
308
  * @param spec - OpenAPI specification for the route
250
309
  * @param handlers - Route handler functions
251
310
  */
252
- delete(path: string, spec: MethodAPISpec, ...handlers: ShokupanHandler<T>[]): any;
311
+ delete<Path extends string>(path: Path, spec: MethodAPISpec, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
253
312
  /**
254
313
  * Adds a PATCH route to the router.
255
314
  *
256
315
  * @param path - URL path
257
316
  * @param handlers - Route handler functions
258
317
  */
259
- patch(path: string, ...handlers: ShokupanHandler<T>[]): any;
318
+ patch<Path extends string>(path: Path, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
260
319
  /**
261
320
  * Adds a PATCH route to the router.
262
321
  *
@@ -264,14 +323,14 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
264
323
  * @param spec - OpenAPI specification for the route
265
324
  * @param handlers - Route handler functions
266
325
  */
267
- patch(path: string, spec: MethodAPISpec, ...handlers: ShokupanHandler<T>[]): any;
326
+ patch<Path extends string>(path: Path, spec: MethodAPISpec, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
268
327
  /**
269
328
  * Adds a OPTIONS route to the router.
270
329
  *
271
330
  * @param path - URL path
272
331
  * @param handlers - Route handler functions
273
332
  */
274
- options(path: string, ...handlers: ShokupanHandler<T>[]): any;
333
+ options<Path extends string>(path: Path, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
275
334
  /**
276
335
  * Adds a OPTIONS route to the router.
277
336
  *
@@ -279,14 +338,14 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
279
338
  * @param spec - OpenAPI specification for the route
280
339
  * @param handlers - Route handler functions
281
340
  */
282
- options(path: string, spec: MethodAPISpec, ...handlers: ShokupanHandler<T>[]): any;
341
+ options<Path extends string>(path: Path, spec: MethodAPISpec, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
283
342
  /**
284
343
  * Adds a HEAD route to the router.
285
344
  *
286
345
  * @param path - URL path
287
346
  * @param handlers - Route handler functions
288
347
  */
289
- head(path: string, ...handlers: ShokupanHandler<T>[]): any;
348
+ head<Path extends string>(path: Path, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
290
349
  /**
291
350
  * Adds a HEAD route to the router.
292
351
  *
@@ -294,7 +353,7 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
294
353
  * @param spec - OpenAPI specification for the route
295
354
  * @param handlers - Route handler functions
296
355
  */
297
- head(path: string, spec: MethodAPISpec, ...handlers: ShokupanHandler<T>[]): any;
356
+ head<Path extends string>(path: Path, spec: MethodAPISpec, ...handlers: ShokupanHandler<T, RouteParams<Path>>[]): any;
298
357
  /**
299
358
  * Adds a guard to the router that applies to all routes added **after** this point.
300
359
  * Guards must return true or call `ctx.next()` to allow the request to continue.
@@ -3,6 +3,70 @@ import { ShokupanRouter } from './router';
3
3
  import { $dispatch } from './symbol';
4
4
  import { Middleware, ProcessResult, RequestOptions, ShokupanConfig } from './types';
5
5
  import { Server } from 'bun';
6
+ /**
7
+ * Shokupan Application
8
+ *
9
+ * The main application class for creating a Shokupan web server.
10
+ *
11
+ * @template State - The shape of `ctx.state` for all routes in the application.
12
+ * Use this to provide type safety for state management across middleware and handlers.
13
+ *
14
+ * @example Basic Usage
15
+ * ```typescript
16
+ * const app = new Shokupan();
17
+ * app.get('/hello', (ctx) => ctx.json({ message: 'Hello' }));
18
+ * await app.listen(3000);
19
+ * ```
20
+ *
21
+ * @example Typed State
22
+ * ```typescript
23
+ * interface AppState {
24
+ * userId: string;
25
+ * tenant: string;
26
+ * requestId: string;
27
+ * }
28
+ *
29
+ * const app = new Shokupan<AppState>();
30
+ *
31
+ * // Middleware has typed state access
32
+ * app.use((ctx, next) => {
33
+ * ctx.state.userId = 'user-123'; // ✓ Type-safe
34
+ * ctx.state.requestId = crypto.randomUUID();
35
+ * return next();
36
+ * });
37
+ *
38
+ * // Handlers have typed state access
39
+ * app.get('/profile', (ctx) => {
40
+ * const { userId, tenant } = ctx.state; // ✓ TypeScript knows these exist
41
+ * return ctx.json({ userId, tenant });
42
+ * });
43
+ * ```
44
+ *
45
+ * @example Empty State (No State Management)
46
+ * ```typescript
47
+ * import { EmptyState } from 'shokupan';
48
+ *
49
+ * const app = new Shokupan<EmptyState>();
50
+ * // ctx.state will be an empty object with no properties
51
+ * ```
52
+ *
53
+ * @example Combining Path Params and State
54
+ * ```typescript
55
+ * interface RequestState {
56
+ * userId: string;
57
+ * permissions: string[];
58
+ * }
59
+ *
60
+ * const app = new Shokupan<RequestState>();
61
+ *
62
+ * app.get('/users/:userId/posts/:postId', (ctx) => {
63
+ * // Both params and state are fully typed!
64
+ * const { userId, postId } = ctx.params; // ✓ Path params typed
65
+ * const { permissions } = ctx.state; // ✓ State typed
66
+ * return ctx.json({ userId, postId, permissions });
67
+ * });
68
+ * ```
69
+ */
6
70
  export declare class Shokupan<T = any> extends ShokupanRouter<T> {
7
71
  readonly applicationConfig: ShokupanConfig;
8
72
  openApiSpec?: any;
package/dist/types.d.ts CHANGED
@@ -6,6 +6,33 @@ import { $isRouter } from './symbol';
6
6
  export type DeepPartial<T> = T extends Function ? T : T extends object ? {
7
7
  [P in keyof T]?: DeepPartial<T[P]>;
8
8
  } : T;
9
+ /**
10
+ * Helper type for applications that don't use ctx.state.
11
+ * Prevents accidental property access on state.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const app = new Shokupan<EmptyState>();
16
+ * ```
17
+ */
18
+ export type EmptyState = Record<string, never>;
19
+ /**
20
+ * Default state type that allows any properties.
21
+ * This is the default if no state type is specified.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const app = new Shokupan<DefaultState>();
26
+ * // Equivalent to: new Shokupan();
27
+ * ```
28
+ */
29
+ export type DefaultState = Record<string, any>;
30
+ type ParsePathParams<Path extends string> = Path extends `${infer _Start}:${infer Param}/${infer Rest}` ? {
31
+ [K in Param | keyof ParsePathParams<`/${Rest}`>]: string;
32
+ } : Path extends `${infer _Start}:${infer Param}` ? {
33
+ [K in Param]: string;
34
+ } : {};
35
+ export type RouteParams<Path extends string> = string extends Path ? Record<string, string> : ParsePathParams<Path> extends Record<string, never> ? Record<string, string> : ParsePathParams<Path>;
9
36
  export interface RouteMetadata {
10
37
  file: string;
11
38
  line: number;
@@ -49,7 +76,7 @@ export interface CookieOptions {
49
76
  sameSite?: boolean | 'lax' | 'strict' | 'none' | 'Lax' | 'Strict' | 'None';
50
77
  priority?: 'low' | 'medium' | 'high' | 'Low' | 'Medium' | 'High';
51
78
  }
52
- export type ShokupanHandler<T extends Record<string, any> = Record<string, any>> = (ctx: ShokupanContext<T>, next?: NextFn) => Promise<any> | any;
79
+ export type ShokupanHandler<State extends Record<string, any> = Record<string, any>, Params extends Record<string, string> = Record<string, string>> = (ctx: ShokupanContext<State, Params>, next?: NextFn) => Promise<any> | any;
53
80
  export declare const HTTPMethods: string[];
54
81
  export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD" | "ALL";
55
82
  export declare enum RouteParamType {
@@ -350,6 +377,10 @@ export interface StaticServeOptions<T extends Record<string, any>> {
350
377
  root?: string;
351
378
  /**
352
379
  * Whether to list directory contents if no index file is found.
380
+ *
381
+ * Security Note: Directory listing is disabled by default to prevent information disclosure.
382
+ * Enable this only if you specifically need it and understand the security implications.
383
+ *
353
384
  * @default false
354
385
  */
355
386
  listDirectory?: boolean;
@@ -384,3 +415,4 @@ export interface StaticServeOptions<T extends Record<string, any>> {
384
415
  */
385
416
  openapi?: MethodAPISpec;
386
417
  }
418
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shokupan",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Shokupan is a low-lift modern web framework for Bun.",
5
5
  "author": "Andrew G. Knackstedt",
6
6
  "publishConfig": {
@@ -152,4 +152,4 @@
152
152
  "vite-plugin-dts": "^4.5.4",
153
153
  "zod": "^4.2.1"
154
154
  }
155
- }
155
+ }