@fresho/router 0.0.1 → 0.1.0

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,441 @@
1
+ # Middleware
2
+
3
+ @fresho/router provides a composable middleware system for request/response processing. Middleware can handle cross-cutting concerns like authentication, logging, CORS, and rate limiting.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Middleware Basics](#middleware-basics)
8
+ - [Authoring Middleware](#authoring-middleware)
9
+ - [Simple Middleware](#simple-middleware)
10
+ - [Typed Context Access](#typed-context-access)
11
+ - [Adding Context Properties](#adding-context-properties)
12
+ - [Built-in Middleware](#built-in-middleware)
13
+ - [Authentication](#authentication)
14
+ - [CORS](#cors)
15
+ - [Error Handling](#errorhandler)
16
+ - [Logging](#logger)
17
+ - [Rate Limiting](#ratelimit)
18
+ - [Request ID](#requestid)
19
+ - [Timeout](#timeout)
20
+ - [Content Type Validation](#contenttype)
21
+ - [Middleware Utilities](#middleware-utilities)
22
+ - [compose](#compose)
23
+
24
+ ---
25
+
26
+ ## Middleware Basics
27
+
28
+ Middleware functions receive a context and a `next` function. They can:
29
+ - Modify the request before it reaches the handler
30
+ - Short-circuit the request (return early without calling `next`)
31
+ - Modify the response after the handler completes
32
+ - Add properties to the context for downstream middleware and handlers
33
+
34
+ ```typescript
35
+ import type { Middleware } from '@fresho/router';
36
+
37
+ const timing: Middleware = async (ctx, next) => {
38
+ const start = Date.now();
39
+ const response = await next();
40
+ const ms = Date.now() - start;
41
+ response.headers.set('X-Response-Time', `${ms}ms`);
42
+ return response;
43
+ };
44
+ ```
45
+
46
+ Add middleware to a router after the routes:
47
+
48
+ ```typescript
49
+ import { router } from '@fresho/router';
50
+ import { cors, errorHandler } from '@fresho/router/middleware';
51
+
52
+ const api = router({
53
+ hello: router({
54
+ get: async () => ({ message: 'Hello' }),
55
+ }),
56
+ }, cors(), errorHandler(), timing);
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Authoring Middleware
62
+
63
+ ### Simple Middleware
64
+
65
+ A middleware is a function that takes `(context, next)` and returns a `Response`:
66
+
67
+ ```typescript
68
+ import type { Middleware, MiddlewareContext, MiddlewareNext } from '@fresho/router';
69
+
70
+ const logger: Middleware = async (ctx: MiddlewareContext, next: MiddlewareNext) => {
71
+ console.log(`${ctx.request.method} ${ctx.request.url}`);
72
+ return next();
73
+ };
74
+ ```
75
+
76
+ ### Typed Context Access
77
+
78
+ Use `Middleware<Ctx>` to type the context (environment and middleware-added properties):
79
+
80
+ ```typescript
81
+ interface AppContext {
82
+ env: {
83
+ JWT_SECRET: string;
84
+ DB: D1Database;
85
+ };
86
+ user?: { id: string }; // Optional - may not exist yet
87
+ }
88
+
89
+ const auth: Middleware<AppContext> = async (ctx, next) => {
90
+ // ctx.env is typed (env.JWT_SECRET, env.DB)
91
+ // Other context properties are Partial (user may be undefined)
92
+ const secret = ctx.env.JWT_SECRET;
93
+ ctx.user = await validateToken(ctx.request, secret);
94
+ return next();
95
+ };
96
+ ```
97
+
98
+ Note: In middleware, `env` is required but other context properties are `Partial` since middleware may be building them up. In handlers, all context properties are required (middleware has already run).
99
+
100
+ ### Adding Context Properties
101
+
102
+ Middleware can add properties to the context. These are available to downstream middleware and route handlers:
103
+
104
+ ```typescript
105
+ // Middleware adds user to context
106
+ const auth: Middleware = async (ctx, next) => {
107
+ ctx.user = await authenticate(ctx.request);
108
+ return next();
109
+ };
110
+
111
+ // Route handler receives the context with user
112
+ profile: router({
113
+ get: route.ctx<{ user: User }>()({
114
+ handler: async (c) => {
115
+ // c.user is typed as User
116
+ return { name: c.user.name };
117
+ },
118
+ }),
119
+ })
120
+ ```
121
+
122
+ The `route.ctx<T>()` builder tells TypeScript what context properties the handler expects.
123
+
124
+ ---
125
+
126
+ ## Built-in Middleware
127
+
128
+ Import from `@fresho/router/middleware`:
129
+
130
+ ```typescript
131
+ import {
132
+ // Authentication
133
+ basicAuth,
134
+ bearerAuth,
135
+ jwtAuth,
136
+ // CORS
137
+ cors,
138
+ // Utilities
139
+ errorHandler,
140
+ logger,
141
+ rateLimit,
142
+ requestId,
143
+ timeout,
144
+ contentType,
145
+ } from '@fresho/router/middleware';
146
+ ```
147
+
148
+ ---
149
+
150
+ ### Authentication
151
+
152
+ @fresho/router includes several authentication middleware options:
153
+
154
+ - **`basicAuth`** - HTTP Basic authentication
155
+ - **`bearerAuth`** - Simple bearer token authentication
156
+ - **`jwtAuth`** - JWT authentication with signature verification
157
+ - **`jwtSign`** - JWT token signing utility
158
+
159
+ For complete documentation, examples, and patterns (cookie-based auth, token refresh, etc.), see the **[Authentication Guide](authentication.md)**.
160
+
161
+ Quick example:
162
+
163
+ ```typescript
164
+ import { jwtAuth } from '@fresho/router/middleware';
165
+
166
+ const api = router({
167
+ profile: router({
168
+ get: route.ctx<{ user: { id: string } }>()({
169
+ handler: async (c) => ({ id: c.user.id }),
170
+ }),
171
+ }),
172
+ },
173
+ jwtAuth({
174
+ secret: process.env.JWT_SECRET,
175
+ claims: (payload) => ({ user: { id: payload.sub } }),
176
+ })
177
+ );
178
+ ```
179
+
180
+ ---
181
+
182
+ ### cors
183
+
184
+ Cross-Origin Resource Sharing headers.
185
+
186
+ ```typescript
187
+ cors({
188
+ // Allowed origins (default: '*')
189
+ // Can be: string, string[], RegExp, or (origin: string) => boolean
190
+ origin: 'https://example.com',
191
+ // or: origin: ['https://example.com', 'https://app.example.com'],
192
+ // or: origin: /\.example\.com$/,
193
+ // or: origin: (origin) => origin.endsWith('.example.com'),
194
+
195
+ // Allowed methods (default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD)
196
+ methods: ['GET', 'POST'],
197
+
198
+ // Allowed request headers (default: Content-Type, Authorization)
199
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
200
+
201
+ // Headers to expose to the browser
202
+ exposedHeaders: ['X-Total-Count'],
203
+
204
+ // Allow credentials (cookies, authorization headers)
205
+ credentials: true,
206
+
207
+ // Max age for preflight cache in seconds (default: 86400)
208
+ maxAge: 3600,
209
+
210
+ // Pass preflight to next handler instead of returning 204
211
+ preflightContinue: false,
212
+ })
213
+ ```
214
+
215
+ ---
216
+
217
+ ### errorHandler
218
+
219
+ Catches errors and returns JSON error responses.
220
+
221
+ ```typescript
222
+ errorHandler({
223
+ // Custom error logger
224
+ log: (error, ctx) => {
225
+ console.error(`[${ctx.requestId}]`, error);
226
+ },
227
+
228
+ // Expose error details in response (default: false)
229
+ // WARNING: Set to false in production
230
+ expose: process.env.NODE_ENV === 'development',
231
+
232
+ // Custom error response formatter
233
+ formatter: (error, ctx) => {
234
+ return new Response(JSON.stringify({
235
+ error: error.message,
236
+ requestId: ctx.requestId,
237
+ }), {
238
+ status: 500,
239
+ headers: { 'Content-Type': 'application/json' },
240
+ });
241
+ },
242
+ })
243
+ ```
244
+
245
+ Supports errors with `status` or `statusCode` properties for custom HTTP status codes.
246
+
247
+ **Using HttpError:**
248
+
249
+ Use the `HttpError` class to throw errors with specific status codes:
250
+
251
+ ```typescript
252
+ import { route, router } from '@fresho/router';
253
+ import { errorHandler, HttpError } from '@fresho/router/middleware';
254
+
255
+ const api = router({
256
+ users: router({
257
+ $id: router({
258
+ get: async (c) => {
259
+ const user = await db.findUser(c.path.id);
260
+ if (!user) throw new HttpError('User not found', 404);
261
+ return user;
262
+ },
263
+ }),
264
+ }),
265
+ }, errorHandler());
266
+ ```
267
+
268
+ This keeps handler return types clean (just `User`, not `User | Response`) while still allowing custom error responses.
269
+
270
+ ---
271
+
272
+ ### logger
273
+
274
+ Request/response logging.
275
+
276
+ ```typescript
277
+ logger({
278
+ // Custom log function (default: console.log)
279
+ log: (message) => myLogger.info(message),
280
+
281
+ // Include request headers in log
282
+ includeHeaders: true,
283
+
284
+ // Include request body in log
285
+ includeBody: true,
286
+
287
+ // Custom log formatter
288
+ formatter: (info) => {
289
+ return `${info.method} ${info.url} ${info.status} ${info.duration}ms`;
290
+ },
291
+ })
292
+ ```
293
+
294
+ To skip logging for certain paths, use separate routers:
295
+
296
+ ```typescript
297
+ // Health check without logging
298
+ const healthRouter = router({
299
+ get: async () => ({ status: 'ok' }),
300
+ });
301
+
302
+ // API routes with logging
303
+ const apiRouter = router({
304
+ users: router({ ... }),
305
+ }, logger());
306
+
307
+ // Combine
308
+ const app = router({
309
+ health: healthRouter,
310
+ api: apiRouter,
311
+ });
312
+ ```
313
+
314
+ ---
315
+
316
+ ### rateLimit
317
+
318
+ Rate limiting with configurable store.
319
+
320
+ ```typescript
321
+ rateLimit({
322
+ // Time window in milliseconds (default: 60000 = 1 minute)
323
+ windowMs: 60 * 1000,
324
+
325
+ // Maximum requests per window (default: 100)
326
+ max: 100,
327
+
328
+ // Custom key generator (default: IP address from headers)
329
+ keyGenerator: (ctx) => {
330
+ return ctx.user?.id || getClientIp(ctx.request);
331
+ },
332
+
333
+ // Custom store (default: in-memory)
334
+ store: new RedisRateLimitStore(redis),
335
+
336
+ // Don't count successful requests (status < 400)
337
+ skipSuccessfulRequests: true,
338
+
339
+ // Don't count failed requests (status >= 400)
340
+ skipFailedRequests: true,
341
+
342
+ // Custom handler for rate-limited requests
343
+ handler: (ctx) => {
344
+ return new Response(JSON.stringify({ error: 'Slow down!' }), {
345
+ status: 429,
346
+ headers: { 'Content-Type': 'application/json' },
347
+ });
348
+ },
349
+ })
350
+ ```
351
+
352
+ Adds rate limit headers to responses:
353
+ - `X-RateLimit-Limit`: Maximum requests allowed
354
+ - `X-RateLimit-Remaining`: Remaining requests in window
355
+ - `X-RateLimit-Reset`: ISO timestamp when the window resets
356
+
357
+ ---
358
+
359
+ ### requestId
360
+
361
+ Adds a unique request ID to each request.
362
+
363
+ ```typescript
364
+ requestId({
365
+ // Header name (default: 'X-Request-ID')
366
+ headerName: 'X-Request-ID',
367
+
368
+ // Custom ID generator (default: crypto.randomUUID())
369
+ generator: () => `req-${Date.now()}-${Math.random().toString(36).slice(2)}`,
370
+ })
371
+ ```
372
+
373
+ Sets `ctx.requestId` and adds the ID to response headers.
374
+
375
+ ---
376
+
377
+ ### timeout
378
+
379
+ Times out long-running requests.
380
+
381
+ ```typescript
382
+ timeout({
383
+ // Timeout in milliseconds (required)
384
+ timeout: 5000,
385
+
386
+ // Custom timeout message
387
+ message: 'Request took too long',
388
+ })
389
+ ```
390
+
391
+ Returns 408 Request Timeout on timeout.
392
+
393
+ ---
394
+
395
+ ### contentType
396
+
397
+ Validates request Content-Type header.
398
+
399
+ ```typescript
400
+ contentType({
401
+ // Required: expected content types
402
+ types: ['application/json', 'application/x-www-form-urlencoded'],
403
+
404
+ // Skip validation for these methods (default: GET, HEAD, OPTIONS)
405
+ skipMethods: ['GET', 'HEAD', 'OPTIONS'],
406
+
407
+ // Custom error message
408
+ message: 'Only JSON is accepted',
409
+ })
410
+ ```
411
+
412
+ Returns 415 Unsupported Media Type on mismatch.
413
+
414
+ ---
415
+
416
+ ## Middleware Utilities
417
+
418
+ ### compose
419
+
420
+ Combines multiple middleware into a single middleware:
421
+
422
+ ```typescript
423
+ import { compose } from '@fresho/router/middleware';
424
+
425
+ const security = compose(
426
+ cors(),
427
+ rateLimit({ max: 100 }),
428
+ errorHandler(),
429
+ );
430
+
431
+ const api = router({
432
+ users: router({ ... }),
433
+ }, security);
434
+ ```
435
+
436
+ ---
437
+
438
+ ## See Also
439
+
440
+ - [Authentication Guide](authentication.md) - JWT, cookies, token refresh
441
+ - [Main Documentation](../README.md)
package/package.json CHANGED
@@ -1,10 +1,68 @@
1
1
  {
2
2
  "name": "@fresho/router",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @fresho/router",
3
+ "engines": {
4
+ "node": ">=22"
5
+ },
6
+ "version": "0.1.0",
7
+ "description": "Type-safe route definitions with auto-generated clients and OpenAPI docs",
8
+ "license": "MIT",
9
+ "author": "fresho.dev",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/fresho-dev/router.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/fresho-dev/router/issues"
16
+ },
17
+ "homepage": "https://github.com/fresho-dev/router#README.md",
5
18
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
19
+ "router",
20
+ "routing",
21
+ "typescript",
22
+ "type-safe",
23
+ "cloudflare-workers",
24
+ "deno",
25
+ "bun",
26
+ "http",
27
+ "api",
28
+ "rest",
29
+ "middleware",
30
+ "openapi",
31
+ "validation",
32
+ "schema"
33
+ ],
34
+ "type": "module",
35
+ "sideEffects": false,
36
+ "main": "./dist/index.mjs",
37
+ "types": "./dist/index.d.mts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.mts",
41
+ "import": "./dist/index.mjs"
42
+ },
43
+ "./middleware": {
44
+ "types": "./dist/middleware/index.d.mts",
45
+ "import": "./dist/middleware/index.mjs"
46
+ }
47
+ },
48
+ "scripts": {
49
+ "build": "tsdown src/index.ts src/middleware/index.ts",
50
+ "clean": "rm -rf ./dist",
51
+ "typecheck": "tsc --noEmit",
52
+ "lint": "biome check . && npm run typecheck",
53
+ "lint:fix": "biome check --write .",
54
+ "test": "npm run lint && node --import tsx --test 'src/**/*.test.ts'"
55
+ },
56
+ "devDependencies": {
57
+ "@biomejs/biome": "2.3.8",
58
+ "@types/node": "^22.0.0",
59
+ "tsdown": "^0.17.0-beta.6",
60
+ "tsx": "^4.19.0",
61
+ "typescript": "^5.7.0"
62
+ },
63
+ "files": [
64
+ "dist",
65
+ "docs",
66
+ "LICENSE"
9
67
  ]
10
68
  }