@fresho/router 0.5.0 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["status: number","info: LogInfo","windowMs: number","contentType","cookies: Record<string, string>","cookies","pending: string[]","config: Required<CorsOptions>","headers: Record<string, string>","response","headers"],"sources":["../../src/middleware/auth.ts","../../src/middleware/common.ts","../../src/middleware/cookies.ts","../../src/middleware/cors.ts"],"sourcesContent":["/**\n * @fileoverview Authentication middleware for @fresho/router.\n *\n * Provides middleware wrappers for Basic HTTP, JWT, and Bearer token authentication.\n * The underlying authentication utilities are in `@fresho/router/auth`.\n */\n\nimport { parseBasicAuth } from '../auth/basic.js';\nimport { type JwtAlgorithm, type JwtPayload, type JwtSecret, jwtVerify } from '../auth/jwt.js';\nimport type { Middleware, MiddlewareContext } from '../middleware.js';\n\n// Re-export types that middleware users need\nexport type {\n JwtAlgorithm,\n JwtPayload,\n JwtSecret,\n SignJwtOptions,\n VerifyJwtOptions,\n} from '../auth/jwt.js';\n\n// Re-export utilities that are commonly used alongside middleware\nexport { jwtSign, jwtVerify } from '../auth/jwt.js';\n\n// =============================================================================\n// Basic Auth Middleware\n// =============================================================================\n\n/** Basic authentication middleware configuration. */\nexport interface BasicAuthOptions {\n /** Realm name for the authentication challenge. */\n realm?: string;\n /** Function to validate credentials. Returns claims to merge into context, or null to reject. */\n verify: (\n username: string,\n password: string,\n context: MiddlewareContext,\n ) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null;\n}\n\n/**\n * Creates a Basic HTTP authentication middleware.\n *\n * @example\n * ```typescript\n * const adminRouter = router('/admin', routes,\n * basicAuth({\n * verify: async (username, password) => {\n * if (username === 'admin' && password === 'secret') {\n * return { user: { name: username } };\n * }\n * return null;\n * }\n * })\n * );\n * ```\n */\nexport function basicAuth(options: BasicAuthOptions): Middleware {\n const realm = options.realm || 'Secure Area';\n\n return async (context, next) => {\n const { request } = context;\n\n const credentials = parseBasicAuth(request.headers.get('Authorization'));\n if (!credentials) {\n return new Response('Unauthorized', {\n status: 401,\n headers: {\n 'WWW-Authenticate': `Basic realm=\"${realm}\", charset=\"UTF-8\"`,\n },\n });\n }\n\n try {\n const claims = await options.verify(credentials.username, credentials.password, context);\n if (claims === null) {\n return new Response('Invalid credentials', {\n status: 401,\n headers: {\n 'WWW-Authenticate': `Basic realm=\"${realm}\", charset=\"UTF-8\"`,\n },\n });\n }\n\n Object.assign(context, claims);\n return next();\n } catch (_error) {\n return new Response('Invalid credentials', {\n status: 401,\n headers: {\n 'WWW-Authenticate': `Basic realm=\"${realm}\", charset=\"UTF-8\"`,\n },\n });\n }\n };\n}\n\n// =============================================================================\n// JWT Auth Middleware\n// =============================================================================\n\n/** JWT authentication middleware configuration. */\nexport interface JwtAuthOptions<Ctx = {}> {\n /** Secret key or function to get the secret from context. */\n secret: JwtSecret | ((context: MiddlewareContext<Ctx>) => JwtSecret);\n /**\n * Map JWT payload to context properties. Return null to reject the token.\n * The returned object is merged into the middleware context.\n */\n claims: (payload: JwtPayload) => Record<string, unknown> | null;\n /** Allowed algorithms (default: ['HS256']). */\n algorithms?: JwtAlgorithm[];\n /** Function to extract token from request (default: Authorization header or 'token' cookie). */\n getToken?: (request: Request) => string | null;\n}\n\n/**\n * Creates a JWT authentication middleware.\n *\n * @example\n * ```typescript\n * interface AppContext {\n * env: { JWT_SECRET: string };\n * user: { id: string; email: string };\n * }\n *\n * jwtAuth<AppContext>({\n * secret: (ctx) => ctx.env.JWT_SECRET, // ctx.env is typed\n * claims: (payload) => ({\n * user: { id: payload.sub, email: payload.email },\n * }),\n * })\n *\n * // In your route handler:\n * route.ctx<AppContext>()({\n * handler: async (c) => {\n * return { email: c.user.email }; // Fully typed\n * },\n * })\n * ```\n */\nexport function jwtAuth<Ctx = {}>(options: JwtAuthOptions<Ctx>): Middleware<Ctx> {\n const getToken =\n options.getToken ||\n ((req) => {\n const auth = req.headers.get('Authorization');\n if (auth?.startsWith('Bearer ')) {\n return auth.slice(7);\n }\n // Also check for token in cookie.\n const cookie = req.headers.get('Cookie');\n if (cookie) {\n const match = cookie.match(/(?:^|;\\s*)token=([^;]*)/);\n if (match) {\n return match[1];\n }\n }\n return null;\n });\n\n return async (context, next) => {\n const { request } = context;\n\n const token = getToken(request);\n if (!token) {\n return new Response('Missing token', {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer' },\n });\n }\n\n try {\n const secret =\n typeof options.secret === 'function' ? options.secret(context) : options.secret;\n\n const payload = await jwtVerify(token, secret, { algorithms: options.algorithms });\n\n // Map payload to context properties.\n const claims = options.claims(payload);\n if (claims === null) {\n throw new Error('Token validation failed');\n }\n Object.assign(context, claims);\n\n return next();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Invalid token';\n return new Response(message, {\n status: 401,\n headers: {\n 'WWW-Authenticate': `Bearer error=\"invalid_token\", error_description=\"${message}\"`,\n },\n });\n }\n };\n}\n\n// =============================================================================\n// Bearer Token Auth Middleware (Simple)\n// =============================================================================\n\n/** Bearer token authentication options. */\nexport interface BearerAuthOptions {\n /** Function to validate the token. Returns claims to merge into context, or null to reject. */\n verify: (\n token: string,\n context: MiddlewareContext,\n ) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null;\n}\n\n/**\n * Creates a simple bearer token authentication middleware.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes,\n * bearerAuth({\n * verify: async (token) => {\n * if (token === process.env.API_TOKEN) {\n * return { apiClient: 'trusted' };\n * }\n * return null;\n * }\n * })\n * );\n * ```\n */\nexport function bearerAuth(options: BearerAuthOptions): Middleware {\n return async (context, next) => {\n const { request } = context;\n\n const authHeader = request.headers.get('Authorization');\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Missing token', {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer' },\n });\n }\n\n const token = authHeader.slice(7);\n const claims = await options.verify(token, context);\n\n if (claims === null) {\n return new Response('Invalid token', {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer error=\"invalid_token\"' },\n });\n }\n\n Object.assign(context, claims);\n return next();\n };\n}\n","/**\n * @fileoverview Common middleware utilities for @fresho/router.\n *\n * Provides error handling, logging, rate limiting, and other utilities.\n */\n\nimport type { Middleware, MiddlewareContext } from '../middleware.js';\n\n// =============================================================================\n// Error Handler\n// =============================================================================\n\n/**\n * HTTP error with status code for use with errorHandler middleware.\n *\n * Throw this in route handlers to return a specific HTTP status code.\n * The errorHandler middleware will catch it and format the response.\n *\n * @example\n * ```typescript\n * import { HttpError } from '@fresho/router/middleware';\n *\n * const getUser = route({\n * method: 'get',\n * path: '/users/:id',\n * handler: async (c) => {\n * const user = await db.findUser(c.path.id);\n * if (!user) throw new HttpError('User not found', 404);\n * return user;\n * },\n * });\n * ```\n */\nexport class HttpError extends Error {\n constructor(\n message: string,\n public status: number,\n ) {\n super(message);\n this.name = 'HttpError';\n }\n}\n\n/**\n * Extracts HTTP status code from an error object.\n *\n * Supports errors with `status` or `statusCode` properties (common patterns\n * in frameworks like Express, Koa, Hono, and custom HTTP error classes).\n */\nfunction getErrorStatus(error: unknown): number {\n if (typeof error !== 'object' || error === null) {\n return 500;\n }\n if ('status' in error && typeof error.status === 'number') {\n return error.status;\n }\n if ('statusCode' in error && typeof error.statusCode === 'number') {\n return error.statusCode;\n }\n return 500;\n}\n\n/** Error handler middleware configuration. */\nexport interface ErrorHandlerOptions {\n /** Custom error logger. */\n log?: (error: Error, context: MiddlewareContext) => void | Promise<void>;\n /** Expose error details in response (dangerous in production). */\n expose?: boolean;\n /** Custom error response formatter. */\n formatter?: (error: Error, context: MiddlewareContext) => Response | Promise<Response>;\n}\n\n/**\n * Creates an error handling middleware that catches and formats errors.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, errorHandler({ expose: false }));\n * ```\n */\nexport function errorHandler(options: ErrorHandlerOptions = {}): Middleware {\n return async (context, next) => {\n try {\n return await next();\n } catch (error) {\n const err = error as Error;\n\n // Log error if logger provided\n if (options.log) {\n await options.log(err, context);\n } else {\n console.error(`Error in ${context.request.method} ${context.request.url}:`, err);\n }\n\n // Use custom formatter if provided\n if (options.formatter) {\n return await options.formatter(err, context);\n }\n\n // Default error response\n const message = options.expose ? err.message : 'Internal Server Error';\n const status = getErrorStatus(error);\n\n const errorBody = {\n error: message,\n ...(options.expose && {\n stack: err.stack,\n name: err.name,\n }),\n };\n\n return new Response(JSON.stringify(errorBody), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n}\n\n// =============================================================================\n// Logger\n// =============================================================================\n\n/** Logger middleware configuration. */\nexport interface LoggerOptions {\n /** Custom logger function. */\n log?: (message: string) => void;\n /** Include request headers in log. */\n includeHeaders?: boolean;\n /** Include request body in log. */\n includeBody?: boolean;\n /** Custom log formatter. */\n formatter?: (info: LogInfo) => string;\n}\n\n/** Information logged for each request. */\nexport interface LogInfo {\n method: string;\n url: string;\n status: number;\n duration: number;\n headers?: Record<string, string>;\n body?: unknown;\n error?: Error;\n}\n\n/**\n * Creates a request/response logging middleware.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, logger({ includeHeaders: true }));\n * ```\n */\nexport function logger(options: LoggerOptions = {}): Middleware {\n const log = options.log || console.log;\n\n return async (context, next) => {\n const { request } = context;\n const start = Date.now();\n const url = new URL(request.url);\n\n // Collect request info\n const info: LogInfo = {\n method: request.method,\n url: `${url.pathname}${url.search}`,\n status: 0,\n duration: 0,\n };\n\n if (options.includeHeaders) {\n info.headers = Object.fromEntries(request.headers.entries());\n }\n\n if (options.includeBody && request.body) {\n try {\n const clonedRequest = request.clone();\n const text = await clonedRequest.text();\n try {\n info.body = JSON.parse(text);\n } catch {\n info.body = text;\n }\n } catch {\n info.body = '[Body not readable]';\n }\n }\n\n // Log request\n const requestMessage = options.formatter\n ? options.formatter({ ...info, status: 0, duration: 0 })\n : `→ ${info.method} ${info.url}`;\n log(requestMessage);\n\n try {\n // Process request\n const response = await next();\n\n // Log response\n info.status = response.status;\n info.duration = Date.now() - start;\n\n const responseMessage = options.formatter\n ? options.formatter(info)\n : `← ${info.status} ${response.statusText || 'OK'} (${info.duration}ms)`;\n log(responseMessage);\n\n return response;\n } catch (error) {\n // Log error\n info.status = 500;\n info.duration = Date.now() - start;\n info.error = error as Error;\n\n const errorMessage = options.formatter\n ? options.formatter(info)\n : `✗ ${info.status} Error: ${(error as Error).message} (${info.duration}ms)`;\n log(errorMessage);\n\n throw error;\n }\n };\n}\n\n// =============================================================================\n// Rate Limiter\n// =============================================================================\n\n/** Rate limit store interface. */\nexport interface RateLimitStore {\n increment(key: string): Promise<number>;\n decrement(key: string): Promise<void>;\n reset(key: string): Promise<void>;\n}\n\n/** In-memory rate limit store with periodic cleanup of expired entries. */\nexport class MemoryRateLimitStore implements RateLimitStore {\n private counts = new Map<string, { count: number; resetAt: number }>();\n private lastCleanup = Date.now();\n\n /** Interval between cleanups (default: 60 seconds). */\n private readonly cleanupIntervalMs: number;\n\n constructor(\n private windowMs: number,\n options?: { cleanupIntervalMs?: number },\n ) {\n // Default cleanup interval is the window duration or 60 seconds, whichever is larger.\n this.cleanupIntervalMs = options?.cleanupIntervalMs ?? Math.max(windowMs, 60_000);\n }\n\n async increment(key: string): Promise<number> {\n const now = Date.now();\n\n // Periodic cleanup to prevent memory leaks.\n if (now - this.lastCleanup > this.cleanupIntervalMs) {\n this.cleanup(now);\n }\n\n const record = this.counts.get(key);\n\n if (!record || now > record.resetAt) {\n this.counts.set(key, { count: 1, resetAt: now + this.windowMs });\n return 1;\n }\n\n record.count++;\n return record.count;\n }\n\n async decrement(key: string): Promise<void> {\n const record = this.counts.get(key);\n if (record && record.count > 0) {\n record.count--;\n }\n }\n\n async reset(key: string): Promise<void> {\n this.counts.delete(key);\n }\n\n /** Removes all expired entries from the store. */\n private cleanup(now: number): void {\n for (const [key, record] of this.counts) {\n if (now > record.resetAt) {\n this.counts.delete(key);\n }\n }\n this.lastCleanup = now;\n }\n\n /** Returns the current number of tracked keys (useful for testing). */\n get size(): number {\n return this.counts.size;\n }\n}\n\n/** Rate limiter middleware configuration. */\nexport interface RateLimitOptions {\n /** Time window in milliseconds. */\n windowMs?: number;\n /** Maximum requests per window. */\n max?: number;\n /** Function to generate a unique key for each client. */\n keyGenerator?: (context: MiddlewareContext) => string;\n /** Store for rate limit data. */\n store?: RateLimitStore;\n /** Skip successful requests from rate limit. */\n skipSuccessfulRequests?: boolean;\n /** Skip failed requests from rate limit. */\n skipFailedRequests?: boolean;\n /** Custom handler for rate limited requests. */\n handler?: (context: MiddlewareContext) => Response | Promise<Response>;\n}\n\n/**\n * Creates a rate limiting middleware.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, rateLimit({ max: 100, windowMs: 60000 }));\n * ```\n */\nexport function rateLimit(options: RateLimitOptions = {}): Middleware {\n const config = {\n windowMs: 60 * 1000, // 1 minute\n max: 100,\n keyGenerator: (ctx: MiddlewareContext) => {\n // Use IP address as default key\n const cfIp = ctx.request.headers.get('CF-Connecting-IP');\n const xForwardedFor = ctx.request.headers.get('X-Forwarded-For');\n const xRealIp = ctx.request.headers.get('X-Real-IP');\n return cfIp || xForwardedFor?.split(',')[0] || xRealIp || 'unknown';\n },\n ...options,\n };\n\n const store = config.store || new MemoryRateLimitStore(config.windowMs);\n\n return async (context, next) => {\n const key = config.keyGenerator(context);\n const current = await store.increment(key);\n\n if (current > config.max) {\n if (config.handler) {\n return await config.handler(context);\n }\n\n return new Response('Too many requests', {\n status: 429,\n headers: {\n 'Retry-After': String(Math.ceil(config.windowMs / 1000)),\n 'X-RateLimit-Limit': String(config.max),\n 'X-RateLimit-Remaining': '0',\n 'X-RateLimit-Reset': new Date(Date.now() + config.windowMs).toISOString(),\n },\n });\n }\n\n const response = await next();\n\n // Optionally don't count successful/failed requests\n if (\n (config.skipSuccessfulRequests && response.status < 400) ||\n (config.skipFailedRequests && response.status >= 400)\n ) {\n await store.decrement(key);\n }\n\n // Add rate limit headers\n const headers = new Headers(response.headers);\n headers.set('X-RateLimit-Limit', String(config.max));\n headers.set('X-RateLimit-Remaining', String(Math.max(0, config.max - current)));\n headers.set('X-RateLimit-Reset', new Date(Date.now() + config.windowMs).toISOString());\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n };\n}\n\n// =============================================================================\n// Request ID\n// =============================================================================\n\n/** Request ID middleware configuration. */\nexport interface RequestIdOptions {\n /** Header name for request ID. */\n headerName?: string;\n /** Function to generate request ID. */\n generator?: () => string;\n}\n\n/**\n * Creates a middleware that adds a unique request ID to each request.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, requestId({ headerName: 'X-Request-ID' }));\n * ```\n */\nexport function requestId(options: RequestIdOptions = {}): Middleware {\n const headerName = options.headerName || 'X-Request-ID';\n const generator = options.generator || (() => crypto.randomUUID());\n\n return async (context, next) => {\n // Check if request already has an ID\n let id = context.request.headers.get(headerName);\n if (!id) {\n id = generator();\n }\n\n // Add to context\n context.requestId = id;\n\n // Process request\n const response = await next();\n\n // Add request ID to response headers\n const headers = new Headers(response.headers);\n headers.set(headerName, id);\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n };\n}\n\n// =============================================================================\n// Timeout\n// =============================================================================\n\n/** Timeout middleware configuration. */\nexport interface TimeoutOptions {\n /** Timeout in milliseconds. */\n timeout: number;\n /** Custom timeout response. */\n message?: string;\n}\n\n/**\n * Creates a middleware that times out requests after a specified duration.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, timeout({ timeout: 5000 }));\n * ```\n */\nexport function timeout(options: TimeoutOptions): Middleware {\n return async (context, next) => {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), options.timeout);\n\n try {\n const responsePromise = next();\n const timeoutPromise = new Promise<Response>((_, reject) => {\n controller.signal.addEventListener('abort', () => {\n reject(new Error('Request timeout'));\n });\n });\n\n const response = await Promise.race([responsePromise, timeoutPromise]);\n clearTimeout(timeoutId);\n return response;\n } catch (error) {\n clearTimeout(timeoutId);\n if ((error as Error).message === 'Request timeout') {\n return new Response(options.message || 'Request timeout', {\n status: 408,\n });\n }\n throw error;\n }\n };\n}\n\n// =============================================================================\n// Content Type Validation\n// =============================================================================\n\n/** Content type validation options. */\nexport interface ContentTypeOptions {\n /** Expected content types. */\n types: string[];\n /** Skip validation for these methods. */\n skipMethods?: string[];\n /** Custom error message. */\n message?: string;\n}\n\n/**\n * Creates a middleware that validates request content type.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, contentType({ types: ['application/json'] }));\n * ```\n */\nexport function contentType(options: ContentTypeOptions): Middleware {\n const skipMethods = new Set(\n options.skipMethods?.map((m) => m.toUpperCase()) || ['GET', 'HEAD', 'OPTIONS'],\n );\n\n return async (context, next) => {\n if (skipMethods.has(context.request.method.toUpperCase())) {\n return next();\n }\n\n const contentType = context.request.headers.get('Content-Type');\n if (!contentType) {\n return new Response(options.message || 'Missing Content-Type header', {\n status: 415,\n });\n }\n\n const matches = options.types.some((type) => contentType.includes(type));\n if (!matches) {\n return new Response(\n options.message || `Unsupported Media Type. Expected: ${options.types.join(', ')}`,\n { status: 415 },\n );\n }\n\n return next();\n };\n}\n","/**\n * @fileoverview Cookie parsing/serialization and the cookies() middleware.\n *\n * Reads the request `Cookie` header into `context.cookies` and installs a\n * `context.setCookie` helper that serializes per RFC 6265 and appends\n * `Set-Cookie` headers to the outgoing response. Dependency-free, Web APIs only.\n */\n\nimport type { Middleware } from '../middleware.js';\n\n/** Options for serializing a cookie into a `Set-Cookie` header value. */\nexport interface CookieOptions {\n /** Lifetime in seconds, serialized as `Max-Age`. */\n maxAge?: number;\n /** Absolute expiry, serialized as `Expires`. */\n expires?: Date;\n /** Restricts the cookie to a path prefix. */\n path?: string;\n /** Restricts the cookie to a domain. */\n domain?: string;\n /** Only send the cookie over HTTPS. */\n secure?: boolean;\n /** Hide the cookie from client-side JavaScript. */\n httpOnly?: boolean;\n /** Cross-site sending policy. */\n sameSite?: 'strict' | 'lax' | 'none';\n}\n\n/** Helper installed on the context to queue a cookie for the response. */\nexport type SetCookie = (name: string, value: string, options?: CookieOptions) => void;\n\n/**\n * Decodes a cookie value, falling back to the raw value if it is malformed.\n *\n * Cookies are URL-encoded on write (see {@link serializeCookie}); decoding here\n * keeps round-trips lossless without throwing on hand-crafted values.\n */\nfunction tryDecode(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/**\n * Parses a `Cookie` request header into a plain map of name to value.\n *\n * Returns an empty object for a missing or empty header. When a name appears\n * more than once the first value wins, matching the common `cookie` package.\n */\nexport function parseCookies(header: string | null): Record<string, string> {\n const cookies: Record<string, string> = {};\n if (!header) {\n return cookies;\n }\n\n // Split on the standard \"; \" delimiter, tolerating arbitrary surrounding space.\n for (const pair of header.split(';')) {\n const separator = pair.indexOf('=');\n if (separator < 0) {\n continue;\n }\n\n // Split on the first \"=\" only so values may themselves contain \"=\".\n const name = pair.slice(0, separator).trim();\n if (name && !(name in cookies)) {\n cookies[name] = tryDecode(pair.slice(separator + 1).trim());\n }\n }\n\n return cookies;\n}\n\n/**\n * Serializes a cookie into a `Set-Cookie` header value per RFC 6265.\n *\n * The value is URL-encoded; attributes are appended in a stable order.\n */\nexport function serializeCookie(name: string, value: string, options: CookieOptions = {}): string {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n\n // Attributes, in the conventional Set-Cookie order.\n if (options.maxAge !== undefined) {\n parts.push(`Max-Age=${Math.trunc(options.maxAge)}`);\n }\n if (options.expires) {\n parts.push(`Expires=${options.expires.toUTCString()}`);\n }\n if (options.domain) {\n parts.push(`Domain=${options.domain}`);\n }\n if (options.path) {\n parts.push(`Path=${options.path}`);\n }\n if (options.secure) {\n parts.push('Secure');\n }\n if (options.httpOnly) {\n parts.push('HttpOnly');\n }\n if (options.sameSite) {\n // Capitalize to match the spec's token casing (Strict / Lax / None).\n parts.push(`SameSite=${options.sameSite[0].toUpperCase()}${options.sameSite.slice(1)}`);\n }\n\n return parts.join('; ');\n}\n\n/**\n * Creates a middleware that parses request cookies and enables setting them.\n *\n * Merges the parsed cookies into `context.cookies` and installs a\n * `context.setCookie(name, value, options)` helper. Each `setCookie` call adds\n * a separate `Set-Cookie` header to the response (append, not overwrite), so a\n * handler may set multiple cookies.\n *\n * @example\n * ```typescript\n * const api = router('/api', routes, cookies());\n * // in a handler: c.cookies['session']; c.setCookie('session', id, { httpOnly: true });\n * ```\n */\nexport function cookies(): Middleware {\n return async (context, next) => {\n // Read side: parse the incoming Cookie header once.\n context.cookies = parseCookies(context.request.headers.get('Cookie'));\n\n // Write side: queue serialized cookies for the outgoing response.\n const pending: string[] = [];\n context.setCookie = ((name, value, options) => {\n pending.push(serializeCookie(name, value, options));\n }) satisfies SetCookie;\n\n const response = await next();\n\n // Leave the response untouched when no cookies were set.\n if (pending.length === 0) {\n return response;\n }\n\n // Append each cookie as its own Set-Cookie header.\n const headers = new Headers(response.headers);\n for (const cookie of pending) {\n headers.append('Set-Cookie', cookie);\n }\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n };\n}\n","/**\n * @fileoverview CORS middleware for @fresho/router.\n *\n * Provides Cross-Origin Resource Sharing support with configurable options.\n */\n\nimport type { Middleware } from '../middleware.js';\n\n/** CORS middleware configuration options. */\nexport interface CorsOptions {\n /** Allowed origins. Can be string, array, regex, or function. */\n origin?: string | string[] | RegExp | ((origin: string) => boolean);\n /** Allowed HTTP methods. */\n methods?: string[];\n /** Allowed request headers. */\n allowedHeaders?: string[];\n /** Headers to expose to the browser. */\n exposedHeaders?: string[];\n /** Allow credentials (cookies, authorization headers). */\n credentials?: boolean;\n /** Max age for preflight cache (seconds). */\n maxAge?: number;\n /** Pass the CORS preflight response to the next handler. */\n preflightContinue?: boolean;\n}\n\n/**\n * Determines if an origin is allowed based on the configuration.\n */\nfunction getAllowedOrigin(\n requestOrigin: string | null,\n configOrigin: CorsOptions['origin'],\n): string {\n if (!requestOrigin) {\n return '*';\n }\n\n if (!configOrigin || configOrigin === '*') {\n return '*';\n }\n\n if (typeof configOrigin === 'string') {\n return configOrigin === requestOrigin ? configOrigin : 'false';\n }\n\n if (Array.isArray(configOrigin)) {\n return configOrigin.includes(requestOrigin) ? requestOrigin : 'false';\n }\n\n if (configOrigin instanceof RegExp) {\n return configOrigin.test(requestOrigin) ? requestOrigin : 'false';\n }\n\n if (typeof configOrigin === 'function') {\n return configOrigin(requestOrigin) ? requestOrigin : 'false';\n }\n\n return 'false';\n}\n\n/**\n * Creates a CORS middleware with the specified options.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, cors({ origin: 'https://example.com', credentials: true }));\n * ```\n */\nexport function cors(options: CorsOptions = {}): Middleware {\n const config: Required<CorsOptions> = {\n origin: '*',\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],\n allowedHeaders: ['Content-Type', 'Authorization'],\n exposedHeaders: [],\n credentials: false,\n maxAge: 86400,\n preflightContinue: false,\n ...options,\n };\n\n return async (context, next) => {\n const { request } = context;\n const requestOrigin = request.headers.get('Origin');\n const allowedOrigin = getAllowedOrigin(requestOrigin, config.origin);\n\n // Handle preflight request\n if (request.method === 'OPTIONS') {\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Origin': allowedOrigin,\n 'Access-Control-Allow-Methods': config.methods.join(', '),\n Vary: 'Origin',\n };\n\n if (config.allowedHeaders.length > 0) {\n const requestHeaders = request.headers.get('Access-Control-Request-Headers');\n if (requestHeaders) {\n // Allow requested headers if they're in the allowed list\n const requested = requestHeaders.split(',').map((h) => h.trim().toLowerCase());\n const allowed = config.allowedHeaders.map((h) => h.toLowerCase());\n const toAllow = requested.filter((h) => allowed.includes(h));\n if (toAllow.length > 0) {\n headers['Access-Control-Allow-Headers'] = toAllow.join(', ');\n }\n } else {\n headers['Access-Control-Allow-Headers'] = config.allowedHeaders.join(', ');\n }\n }\n\n if (config.exposedHeaders.length > 0) {\n headers['Access-Control-Expose-Headers'] = config.exposedHeaders.join(', ');\n }\n\n if (config.credentials) {\n headers['Access-Control-Allow-Credentials'] = 'true';\n }\n\n if (config.maxAge) {\n headers['Access-Control-Max-Age'] = String(config.maxAge);\n }\n\n const response = new Response(null, {\n status: 204,\n headers,\n });\n\n if (config.preflightContinue) {\n // Continue to next middleware/handler even for preflight\n const nextResponse = await next();\n // Merge headers from next response\n const mergedHeaders = new Headers(nextResponse.headers);\n Object.entries(headers).forEach(([key, value]) => {\n mergedHeaders.set(key, value);\n });\n return new Response(nextResponse.body, {\n status: nextResponse.status,\n statusText: nextResponse.statusText,\n headers: mergedHeaders,\n });\n }\n\n return response;\n }\n\n // Handle actual request\n const response = await next();\n\n // Add CORS headers to response\n const headers = new Headers(response.headers);\n headers.set('Access-Control-Allow-Origin', allowedOrigin);\n headers.set('Vary', 'Origin');\n\n if (config.credentials && allowedOrigin !== '*') {\n headers.set('Access-Control-Allow-Credentials', 'true');\n }\n\n if (config.exposedHeaders.length > 0) {\n headers.set('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));\n }\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["status: number","info: LogInfo","windowMs: number","contentType","cookies: Record<string, string>","cookies","pending: string[]","config: Required<CorsOptions>","headers: Record<string, string>","response","headers"],"sources":["../../src/middleware/auth.ts","../../src/middleware/common.ts","../../src/middleware/cookies.ts","../../src/middleware/cors.ts"],"sourcesContent":["/**\n * @fileoverview Authentication middleware for @fresho/router.\n *\n * Provides middleware wrappers for Basic HTTP, JWT, and Bearer token authentication.\n * The underlying authentication utilities are in `@fresho/router/auth`.\n */\n\nimport { parseBasicAuth } from '../auth/basic.js';\nimport { type JwtAlgorithm, type JwtPayload, type JwtSecret, jwtVerify } from '../auth/jwt.js';\nimport type { Middleware, MiddlewareContext } from '../middleware.js';\n\n// Re-export types that middleware users need\nexport type {\n Jwks,\n JwtAlgorithm,\n JwtPayload,\n JwtSecret,\n SignJwtOptions,\n VerifyJwtOptions,\n} from '../auth/jwt.js';\n\n// Re-export utilities that are commonly used alongside middleware\nexport { createJwks, jwtSign, jwtVerify } from '../auth/jwt.js';\n\n// =============================================================================\n// Basic Auth Middleware\n// =============================================================================\n\n/** Basic authentication middleware configuration. */\nexport interface BasicAuthOptions {\n /** Realm name for the authentication challenge. */\n realm?: string;\n /** Function to validate credentials. Returns claims to merge into context, or null to reject. */\n verify: (\n username: string,\n password: string,\n context: MiddlewareContext,\n ) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null;\n}\n\n/**\n * Creates a Basic HTTP authentication middleware.\n *\n * @example\n * ```typescript\n * const adminRouter = router('/admin', routes,\n * basicAuth({\n * verify: async (username, password) => {\n * if (username === 'admin' && password === 'secret') {\n * return { user: { name: username } };\n * }\n * return null;\n * }\n * })\n * );\n * ```\n */\nexport function basicAuth(options: BasicAuthOptions): Middleware {\n const realm = options.realm || 'Secure Area';\n\n return async (context, next) => {\n const { request } = context;\n\n const credentials = parseBasicAuth(request.headers.get('Authorization'));\n if (!credentials) {\n return new Response('Unauthorized', {\n status: 401,\n headers: {\n 'WWW-Authenticate': `Basic realm=\"${realm}\", charset=\"UTF-8\"`,\n },\n });\n }\n\n try {\n const claims = await options.verify(credentials.username, credentials.password, context);\n if (claims === null) {\n return new Response('Invalid credentials', {\n status: 401,\n headers: {\n 'WWW-Authenticate': `Basic realm=\"${realm}\", charset=\"UTF-8\"`,\n },\n });\n }\n\n Object.assign(context, claims);\n return next();\n } catch (_error) {\n return new Response('Invalid credentials', {\n status: 401,\n headers: {\n 'WWW-Authenticate': `Basic realm=\"${realm}\", charset=\"UTF-8\"`,\n },\n });\n }\n };\n}\n\n// =============================================================================\n// JWT Auth Middleware\n// =============================================================================\n\n/** JWT authentication middleware configuration. */\nexport interface JwtAuthOptions<Ctx = {}> {\n /** Secret key or function to get the secret from context. */\n secret: JwtSecret | ((context: MiddlewareContext<Ctx>) => JwtSecret);\n /**\n * Map JWT payload to context properties. Return null to reject the token.\n * The returned object is merged into the middleware context.\n */\n claims: (payload: JwtPayload) => Record<string, unknown> | null;\n /** Allowed algorithms (default: ['HS256'] for HMAC secrets, ['RS256'] otherwise). */\n algorithms?: JwtAlgorithm[];\n /** Required issuer (`iss`). */\n issuer?: string | string[];\n /** Required audience (`aud`); the token must contain at least one of the given values. */\n audience?: string | string[];\n /** Seconds of clock skew allowed when checking `exp` and `nbf` (default: 0). */\n clockTolerance?: number;\n /** Function to extract token from request (default: Authorization header or 'token' cookie). */\n getToken?: (request: Request) => string | null;\n}\n\n/**\n * Creates a JWT authentication middleware.\n *\n * @example\n * ```typescript\n * interface AppContext {\n * env: { JWT_SECRET: string };\n * user: { id: string; email: string };\n * }\n *\n * jwtAuth<AppContext>({\n * secret: (ctx) => ctx.env.JWT_SECRET, // ctx.env is typed\n * claims: (payload) => ({\n * user: { id: payload.sub, email: payload.email },\n * }),\n * })\n *\n * // In your route handler:\n * route.ctx<AppContext>()({\n * handler: async (c) => {\n * return { email: c.user.email }; // Fully typed\n * },\n * })\n * ```\n */\nexport function jwtAuth<Ctx = {}>(options: JwtAuthOptions<Ctx>): Middleware<Ctx> {\n const getToken =\n options.getToken ||\n ((req) => {\n const auth = req.headers.get('Authorization');\n if (auth?.startsWith('Bearer ')) {\n return auth.slice(7);\n }\n // Also check for token in cookie.\n const cookie = req.headers.get('Cookie');\n if (cookie) {\n const match = cookie.match(/(?:^|;\\s*)token=([^;]*)/);\n if (match) {\n return match[1];\n }\n }\n return null;\n });\n\n return async (context, next) => {\n const { request } = context;\n\n const token = getToken(request);\n if (!token) {\n return new Response('Missing token', {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer' },\n });\n }\n\n try {\n const secret =\n typeof options.secret === 'function' ? options.secret(context) : options.secret;\n\n const payload = await jwtVerify(token, secret, {\n algorithms: options.algorithms,\n issuer: options.issuer,\n audience: options.audience,\n clockTolerance: options.clockTolerance,\n });\n\n // Map payload to context properties.\n const claims = options.claims(payload);\n if (claims === null) {\n throw new Error('Token validation failed');\n }\n Object.assign(context, claims);\n\n return next();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Invalid token';\n return new Response(message, {\n status: 401,\n headers: {\n // Fixed value: messages can carry token or JWKS content, unsafe in a header.\n 'WWW-Authenticate': 'Bearer error=\"invalid_token\"',\n },\n });\n }\n };\n}\n\n// =============================================================================\n// Bearer Token Auth Middleware (Simple)\n// =============================================================================\n\n/** Bearer token authentication options. */\nexport interface BearerAuthOptions {\n /** Function to validate the token. Returns claims to merge into context, or null to reject. */\n verify: (\n token: string,\n context: MiddlewareContext,\n ) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null;\n}\n\n/**\n * Creates a simple bearer token authentication middleware.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes,\n * bearerAuth({\n * verify: async (token) => {\n * if (token === process.env.API_TOKEN) {\n * return { apiClient: 'trusted' };\n * }\n * return null;\n * }\n * })\n * );\n * ```\n */\nexport function bearerAuth(options: BearerAuthOptions): Middleware {\n return async (context, next) => {\n const { request } = context;\n\n const authHeader = request.headers.get('Authorization');\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Missing token', {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer' },\n });\n }\n\n const token = authHeader.slice(7);\n const claims = await options.verify(token, context);\n\n if (claims === null) {\n return new Response('Invalid token', {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer error=\"invalid_token\"' },\n });\n }\n\n Object.assign(context, claims);\n return next();\n };\n}\n","/**\n * @fileoverview Common middleware utilities for @fresho/router.\n *\n * Provides error handling, logging, rate limiting, and other utilities.\n */\n\nimport type { Middleware, MiddlewareContext } from '../middleware.js';\n\n// =============================================================================\n// Error Handler\n// =============================================================================\n\n/**\n * HTTP error with status code for use with errorHandler middleware.\n *\n * Throw this in route handlers to return a specific HTTP status code.\n * The errorHandler middleware will catch it and format the response.\n *\n * @example\n * ```typescript\n * import { HttpError } from '@fresho/router/middleware';\n *\n * const getUser = route({\n * method: 'get',\n * path: '/users/:id',\n * handler: async (c) => {\n * const user = await db.findUser(c.path.id);\n * if (!user) throw new HttpError('User not found', 404);\n * return user;\n * },\n * });\n * ```\n */\nexport class HttpError extends Error {\n constructor(\n message: string,\n public status: number,\n ) {\n super(message);\n this.name = 'HttpError';\n }\n}\n\n/**\n * Extracts HTTP status code from an error object.\n *\n * Supports errors with `status` or `statusCode` properties (common patterns\n * in frameworks like Express, Koa, Hono, and custom HTTP error classes).\n */\nfunction getErrorStatus(error: unknown): number {\n if (typeof error !== 'object' || error === null) {\n return 500;\n }\n if ('status' in error && typeof error.status === 'number') {\n return error.status;\n }\n if ('statusCode' in error && typeof error.statusCode === 'number') {\n return error.statusCode;\n }\n return 500;\n}\n\n/** Error handler middleware configuration. */\nexport interface ErrorHandlerOptions {\n /** Custom error logger. */\n log?: (error: Error, context: MiddlewareContext) => void | Promise<void>;\n /** Expose error details in response (dangerous in production). */\n expose?: boolean;\n /** Custom error response formatter. */\n formatter?: (error: Error, context: MiddlewareContext) => Response | Promise<Response>;\n}\n\n/**\n * Creates an error handling middleware that catches and formats errors.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, errorHandler({ expose: false }));\n * ```\n */\nexport function errorHandler(options: ErrorHandlerOptions = {}): Middleware {\n return async (context, next) => {\n try {\n return await next();\n } catch (error) {\n const err = error as Error;\n\n // Log error if logger provided\n if (options.log) {\n await options.log(err, context);\n } else {\n console.error(`Error in ${context.request.method} ${context.request.url}:`, err);\n }\n\n // Use custom formatter if provided\n if (options.formatter) {\n return await options.formatter(err, context);\n }\n\n // Default error response\n const message = options.expose ? err.message : 'Internal Server Error';\n const status = getErrorStatus(error);\n\n const errorBody = {\n error: message,\n ...(options.expose && {\n stack: err.stack,\n name: err.name,\n }),\n };\n\n return new Response(JSON.stringify(errorBody), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n}\n\n// =============================================================================\n// Logger\n// =============================================================================\n\n/** Logger middleware configuration. */\nexport interface LoggerOptions {\n /** Custom logger function. */\n log?: (message: string) => void;\n /** Include request headers in log. */\n includeHeaders?: boolean;\n /** Include request body in log. */\n includeBody?: boolean;\n /** Custom log formatter. */\n formatter?: (info: LogInfo) => string;\n}\n\n/** Information logged for each request. */\nexport interface LogInfo {\n method: string;\n url: string;\n status: number;\n duration: number;\n headers?: Record<string, string>;\n body?: unknown;\n error?: Error;\n}\n\n/**\n * Creates a request/response logging middleware.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, logger({ includeHeaders: true }));\n * ```\n */\nexport function logger(options: LoggerOptions = {}): Middleware {\n const log = options.log || console.log;\n\n return async (context, next) => {\n const { request } = context;\n const start = Date.now();\n const url = new URL(request.url);\n\n // Collect request info\n const info: LogInfo = {\n method: request.method,\n url: `${url.pathname}${url.search}`,\n status: 0,\n duration: 0,\n };\n\n if (options.includeHeaders) {\n info.headers = Object.fromEntries(request.headers.entries());\n }\n\n if (options.includeBody && request.body) {\n try {\n const clonedRequest = request.clone();\n const text = await clonedRequest.text();\n try {\n info.body = JSON.parse(text);\n } catch {\n info.body = text;\n }\n } catch {\n info.body = '[Body not readable]';\n }\n }\n\n // Log request\n const requestMessage = options.formatter\n ? options.formatter({ ...info, status: 0, duration: 0 })\n : `→ ${info.method} ${info.url}`;\n log(requestMessage);\n\n try {\n // Process request\n const response = await next();\n\n // Log response\n info.status = response.status;\n info.duration = Date.now() - start;\n\n const responseMessage = options.formatter\n ? options.formatter(info)\n : `← ${info.status} ${response.statusText || 'OK'} (${info.duration}ms)`;\n log(responseMessage);\n\n return response;\n } catch (error) {\n // Log error\n info.status = 500;\n info.duration = Date.now() - start;\n info.error = error as Error;\n\n const errorMessage = options.formatter\n ? options.formatter(info)\n : `✗ ${info.status} Error: ${(error as Error).message} (${info.duration}ms)`;\n log(errorMessage);\n\n throw error;\n }\n };\n}\n\n// =============================================================================\n// Rate Limiter\n// =============================================================================\n\n/** Rate limit store interface. */\nexport interface RateLimitStore {\n increment(key: string): Promise<number>;\n decrement(key: string): Promise<void>;\n reset(key: string): Promise<void>;\n}\n\n/** In-memory rate limit store with periodic cleanup of expired entries. */\nexport class MemoryRateLimitStore implements RateLimitStore {\n private counts = new Map<string, { count: number; resetAt: number }>();\n private lastCleanup = Date.now();\n\n /** Interval between cleanups (default: 60 seconds). */\n private readonly cleanupIntervalMs: number;\n\n constructor(\n private windowMs: number,\n options?: { cleanupIntervalMs?: number },\n ) {\n // Default cleanup interval is the window duration or 60 seconds, whichever is larger.\n this.cleanupIntervalMs = options?.cleanupIntervalMs ?? Math.max(windowMs, 60_000);\n }\n\n async increment(key: string): Promise<number> {\n const now = Date.now();\n\n // Periodic cleanup to prevent memory leaks.\n if (now - this.lastCleanup > this.cleanupIntervalMs) {\n this.cleanup(now);\n }\n\n const record = this.counts.get(key);\n\n if (!record || now > record.resetAt) {\n this.counts.set(key, { count: 1, resetAt: now + this.windowMs });\n return 1;\n }\n\n record.count++;\n return record.count;\n }\n\n async decrement(key: string): Promise<void> {\n const record = this.counts.get(key);\n if (record && record.count > 0) {\n record.count--;\n }\n }\n\n async reset(key: string): Promise<void> {\n this.counts.delete(key);\n }\n\n /** Removes all expired entries from the store. */\n private cleanup(now: number): void {\n for (const [key, record] of this.counts) {\n if (now > record.resetAt) {\n this.counts.delete(key);\n }\n }\n this.lastCleanup = now;\n }\n\n /** Returns the current number of tracked keys (useful for testing). */\n get size(): number {\n return this.counts.size;\n }\n}\n\n/** Rate limiter middleware configuration. */\nexport interface RateLimitOptions {\n /** Time window in milliseconds. */\n windowMs?: number;\n /** Maximum requests per window. */\n max?: number;\n /** Function to generate a unique key for each client. */\n keyGenerator?: (context: MiddlewareContext) => string;\n /** Store for rate limit data. */\n store?: RateLimitStore;\n /** Skip successful requests from rate limit. */\n skipSuccessfulRequests?: boolean;\n /** Skip failed requests from rate limit. */\n skipFailedRequests?: boolean;\n /** Custom handler for rate limited requests. */\n handler?: (context: MiddlewareContext) => Response | Promise<Response>;\n}\n\n/**\n * Creates a rate limiting middleware.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, rateLimit({ max: 100, windowMs: 60000 }));\n * ```\n */\nexport function rateLimit(options: RateLimitOptions = {}): Middleware {\n const config = {\n windowMs: 60 * 1000, // 1 minute\n max: 100,\n keyGenerator: (ctx: MiddlewareContext) => {\n // Use IP address as default key\n const cfIp = ctx.request.headers.get('CF-Connecting-IP');\n const xForwardedFor = ctx.request.headers.get('X-Forwarded-For');\n const xRealIp = ctx.request.headers.get('X-Real-IP');\n return cfIp || xForwardedFor?.split(',')[0] || xRealIp || 'unknown';\n },\n ...options,\n };\n\n const store = config.store || new MemoryRateLimitStore(config.windowMs);\n\n return async (context, next) => {\n const key = config.keyGenerator(context);\n const current = await store.increment(key);\n\n if (current > config.max) {\n if (config.handler) {\n return await config.handler(context);\n }\n\n return new Response('Too many requests', {\n status: 429,\n headers: {\n 'Retry-After': String(Math.ceil(config.windowMs / 1000)),\n 'X-RateLimit-Limit': String(config.max),\n 'X-RateLimit-Remaining': '0',\n 'X-RateLimit-Reset': new Date(Date.now() + config.windowMs).toISOString(),\n },\n });\n }\n\n const response = await next();\n\n // Optionally don't count successful/failed requests\n if (\n (config.skipSuccessfulRequests && response.status < 400) ||\n (config.skipFailedRequests && response.status >= 400)\n ) {\n await store.decrement(key);\n }\n\n // Add rate limit headers\n const headers = new Headers(response.headers);\n headers.set('X-RateLimit-Limit', String(config.max));\n headers.set('X-RateLimit-Remaining', String(Math.max(0, config.max - current)));\n headers.set('X-RateLimit-Reset', new Date(Date.now() + config.windowMs).toISOString());\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n };\n}\n\n// =============================================================================\n// Request ID\n// =============================================================================\n\n/** Request ID middleware configuration. */\nexport interface RequestIdOptions {\n /** Header name for request ID. */\n headerName?: string;\n /** Function to generate request ID. */\n generator?: () => string;\n}\n\n/**\n * Creates a middleware that adds a unique request ID to each request.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, requestId({ headerName: 'X-Request-ID' }));\n * ```\n */\nexport function requestId(options: RequestIdOptions = {}): Middleware {\n const headerName = options.headerName || 'X-Request-ID';\n const generator = options.generator || (() => crypto.randomUUID());\n\n return async (context, next) => {\n // Check if request already has an ID\n let id = context.request.headers.get(headerName);\n if (!id) {\n id = generator();\n }\n\n // Add to context\n context.requestId = id;\n\n // Process request\n const response = await next();\n\n // Add request ID to response headers\n const headers = new Headers(response.headers);\n headers.set(headerName, id);\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n };\n}\n\n// =============================================================================\n// Timeout\n// =============================================================================\n\n/** Timeout middleware configuration. */\nexport interface TimeoutOptions {\n /** Timeout in milliseconds. */\n timeout: number;\n /** Custom timeout response. */\n message?: string;\n}\n\n/**\n * Creates a middleware that times out requests after a specified duration.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, timeout({ timeout: 5000 }));\n * ```\n */\nexport function timeout(options: TimeoutOptions): Middleware {\n return async (context, next) => {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), options.timeout);\n\n try {\n const responsePromise = next();\n const timeoutPromise = new Promise<Response>((_, reject) => {\n controller.signal.addEventListener('abort', () => {\n reject(new Error('Request timeout'));\n });\n });\n\n const response = await Promise.race([responsePromise, timeoutPromise]);\n clearTimeout(timeoutId);\n return response;\n } catch (error) {\n clearTimeout(timeoutId);\n if ((error as Error).message === 'Request timeout') {\n return new Response(options.message || 'Request timeout', {\n status: 408,\n });\n }\n throw error;\n }\n };\n}\n\n// =============================================================================\n// Content Type Validation\n// =============================================================================\n\n/** Content type validation options. */\nexport interface ContentTypeOptions {\n /** Expected content types. */\n types: string[];\n /** Skip validation for these methods. */\n skipMethods?: string[];\n /** Custom error message. */\n message?: string;\n}\n\n/**\n * Creates a middleware that validates request content type.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, contentType({ types: ['application/json'] }));\n * ```\n */\nexport function contentType(options: ContentTypeOptions): Middleware {\n const skipMethods = new Set(\n options.skipMethods?.map((m) => m.toUpperCase()) || ['GET', 'HEAD', 'OPTIONS'],\n );\n\n return async (context, next) => {\n if (skipMethods.has(context.request.method.toUpperCase())) {\n return next();\n }\n\n const contentType = context.request.headers.get('Content-Type');\n if (!contentType) {\n return new Response(options.message || 'Missing Content-Type header', {\n status: 415,\n });\n }\n\n const matches = options.types.some((type) => contentType.includes(type));\n if (!matches) {\n return new Response(\n options.message || `Unsupported Media Type. Expected: ${options.types.join(', ')}`,\n { status: 415 },\n );\n }\n\n return next();\n };\n}\n","/**\n * @fileoverview Cookie parsing/serialization and the cookies() middleware.\n *\n * Reads the request `Cookie` header into `context.cookies` and installs a\n * `context.setCookie` helper that serializes per RFC 6265 and appends\n * `Set-Cookie` headers to the outgoing response. Dependency-free, Web APIs only.\n */\n\nimport type { Middleware } from '../middleware.js';\n\n/** Options for serializing a cookie into a `Set-Cookie` header value. */\nexport interface CookieOptions {\n /** Lifetime in seconds, serialized as `Max-Age`. */\n maxAge?: number;\n /** Absolute expiry, serialized as `Expires`. */\n expires?: Date;\n /** Restricts the cookie to a path prefix. */\n path?: string;\n /** Restricts the cookie to a domain. */\n domain?: string;\n /** Only send the cookie over HTTPS. */\n secure?: boolean;\n /** Hide the cookie from client-side JavaScript. */\n httpOnly?: boolean;\n /** Cross-site sending policy. */\n sameSite?: 'strict' | 'lax' | 'none';\n}\n\n/** Helper installed on the context to queue a cookie for the response. */\nexport type SetCookie = (name: string, value: string, options?: CookieOptions) => void;\n\n/**\n * Decodes a cookie value, falling back to the raw value if it is malformed.\n *\n * Cookies are URL-encoded on write (see {@link serializeCookie}); decoding here\n * keeps round-trips lossless without throwing on hand-crafted values.\n */\nfunction tryDecode(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/**\n * Parses a `Cookie` request header into a plain map of name to value.\n *\n * Returns an empty object for a missing or empty header. When a name appears\n * more than once the first value wins, matching the common `cookie` package.\n */\nexport function parseCookies(header: string | null): Record<string, string> {\n const cookies: Record<string, string> = {};\n if (!header) {\n return cookies;\n }\n\n // Split on the standard \"; \" delimiter, tolerating arbitrary surrounding space.\n for (const pair of header.split(';')) {\n const separator = pair.indexOf('=');\n if (separator < 0) {\n continue;\n }\n\n // Split on the first \"=\" only so values may themselves contain \"=\".\n const name = pair.slice(0, separator).trim();\n if (name && !(name in cookies)) {\n cookies[name] = tryDecode(pair.slice(separator + 1).trim());\n }\n }\n\n return cookies;\n}\n\n/**\n * Serializes a cookie into a `Set-Cookie` header value per RFC 6265.\n *\n * The value is URL-encoded; attributes are appended in a stable order.\n */\nexport function serializeCookie(name: string, value: string, options: CookieOptions = {}): string {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n\n // Attributes, in the conventional Set-Cookie order.\n if (options.maxAge !== undefined) {\n parts.push(`Max-Age=${Math.trunc(options.maxAge)}`);\n }\n if (options.expires) {\n parts.push(`Expires=${options.expires.toUTCString()}`);\n }\n if (options.domain) {\n parts.push(`Domain=${options.domain}`);\n }\n if (options.path) {\n parts.push(`Path=${options.path}`);\n }\n if (options.secure) {\n parts.push('Secure');\n }\n if (options.httpOnly) {\n parts.push('HttpOnly');\n }\n if (options.sameSite) {\n // Capitalize to match the spec's token casing (Strict / Lax / None).\n parts.push(`SameSite=${options.sameSite[0].toUpperCase()}${options.sameSite.slice(1)}`);\n }\n\n return parts.join('; ');\n}\n\n/**\n * Creates a middleware that parses request cookies and enables setting them.\n *\n * Merges the parsed cookies into `context.cookies` and installs a\n * `context.setCookie(name, value, options)` helper. Each `setCookie` call adds\n * a separate `Set-Cookie` header to the response (append, not overwrite), so a\n * handler may set multiple cookies.\n *\n * @example\n * ```typescript\n * const api = router('/api', routes, cookies());\n * // in a handler: c.cookies['session']; c.setCookie('session', id, { httpOnly: true });\n * ```\n */\nexport function cookies(): Middleware {\n return async (context, next) => {\n // Read side: parse the incoming Cookie header once.\n context.cookies = parseCookies(context.request.headers.get('Cookie'));\n\n // Write side: queue serialized cookies for the outgoing response.\n const pending: string[] = [];\n context.setCookie = ((name, value, options) => {\n pending.push(serializeCookie(name, value, options));\n }) satisfies SetCookie;\n\n const response = await next();\n\n // Leave the response untouched when no cookies were set.\n if (pending.length === 0) {\n return response;\n }\n\n // Append each cookie as its own Set-Cookie header.\n const headers = new Headers(response.headers);\n for (const cookie of pending) {\n headers.append('Set-Cookie', cookie);\n }\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n };\n}\n","/**\n * @fileoverview CORS middleware for @fresho/router.\n *\n * Provides Cross-Origin Resource Sharing support with configurable options.\n */\n\nimport type { Middleware } from '../middleware.js';\n\n/** CORS middleware configuration options. */\nexport interface CorsOptions {\n /** Allowed origins. Can be string, array, regex, or function. */\n origin?: string | string[] | RegExp | ((origin: string) => boolean);\n /** Allowed HTTP methods. */\n methods?: string[];\n /** Allowed request headers. */\n allowedHeaders?: string[];\n /** Headers to expose to the browser. */\n exposedHeaders?: string[];\n /** Allow credentials (cookies, authorization headers). */\n credentials?: boolean;\n /** Max age for preflight cache (seconds). */\n maxAge?: number;\n /** Pass the CORS preflight response to the next handler. */\n preflightContinue?: boolean;\n}\n\n/**\n * Determines if an origin is allowed based on the configuration.\n */\nfunction getAllowedOrigin(\n requestOrigin: string | null,\n configOrigin: CorsOptions['origin'],\n): string {\n if (!requestOrigin) {\n return '*';\n }\n\n if (!configOrigin || configOrigin === '*') {\n return '*';\n }\n\n if (typeof configOrigin === 'string') {\n return configOrigin === requestOrigin ? configOrigin : 'false';\n }\n\n if (Array.isArray(configOrigin)) {\n return configOrigin.includes(requestOrigin) ? requestOrigin : 'false';\n }\n\n if (configOrigin instanceof RegExp) {\n return configOrigin.test(requestOrigin) ? requestOrigin : 'false';\n }\n\n if (typeof configOrigin === 'function') {\n return configOrigin(requestOrigin) ? requestOrigin : 'false';\n }\n\n return 'false';\n}\n\n/**\n * Creates a CORS middleware with the specified options.\n *\n * @example\n * ```typescript\n * const apiRouter = router('/api', routes, cors({ origin: 'https://example.com', credentials: true }));\n * ```\n */\nexport function cors(options: CorsOptions = {}): Middleware {\n const config: Required<CorsOptions> = {\n origin: '*',\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],\n allowedHeaders: ['Content-Type', 'Authorization'],\n exposedHeaders: [],\n credentials: false,\n maxAge: 86400,\n preflightContinue: false,\n ...options,\n };\n\n return async (context, next) => {\n const { request } = context;\n const requestOrigin = request.headers.get('Origin');\n const allowedOrigin = getAllowedOrigin(requestOrigin, config.origin);\n\n // Handle preflight request\n if (request.method === 'OPTIONS') {\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Origin': allowedOrigin,\n 'Access-Control-Allow-Methods': config.methods.join(', '),\n Vary: 'Origin',\n };\n\n if (config.allowedHeaders.length > 0) {\n const requestHeaders = request.headers.get('Access-Control-Request-Headers');\n if (requestHeaders) {\n // Allow requested headers if they're in the allowed list\n const requested = requestHeaders.split(',').map((h) => h.trim().toLowerCase());\n const allowed = config.allowedHeaders.map((h) => h.toLowerCase());\n const toAllow = requested.filter((h) => allowed.includes(h));\n if (toAllow.length > 0) {\n headers['Access-Control-Allow-Headers'] = toAllow.join(', ');\n }\n } else {\n headers['Access-Control-Allow-Headers'] = config.allowedHeaders.join(', ');\n }\n }\n\n if (config.exposedHeaders.length > 0) {\n headers['Access-Control-Expose-Headers'] = config.exposedHeaders.join(', ');\n }\n\n if (config.credentials) {\n headers['Access-Control-Allow-Credentials'] = 'true';\n }\n\n if (config.maxAge) {\n headers['Access-Control-Max-Age'] = String(config.maxAge);\n }\n\n const response = new Response(null, {\n status: 204,\n headers,\n });\n\n if (config.preflightContinue) {\n // Continue to next middleware/handler even for preflight\n const nextResponse = await next();\n // Merge headers from next response\n const mergedHeaders = new Headers(nextResponse.headers);\n Object.entries(headers).forEach(([key, value]) => {\n mergedHeaders.set(key, value);\n });\n return new Response(nextResponse.body, {\n status: nextResponse.status,\n statusText: nextResponse.statusText,\n headers: mergedHeaders,\n });\n }\n\n return response;\n }\n\n // Handle actual request\n const response = await next();\n\n // Add CORS headers to response\n const headers = new Headers(response.headers);\n headers.set('Access-Control-Allow-Origin', allowedOrigin);\n headers.set('Vary', 'Origin');\n\n if (config.credentials && allowedOrigin !== '*') {\n headers.set('Access-Control-Allow-Credentials', 'true');\n }\n\n if (config.exposedHeaders.length > 0) {\n headers.set('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));\n }\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n };\n}\n"],"mappings":"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"}
@@ -47,4 +47,4 @@ function compose(...middleware) {
47
47
 
48
48
  //#endregion
49
49
  export { runMiddleware as n, compose as t };
50
- //# sourceMappingURL=middleware-XMbIevSv.mjs.map
50
+ //# sourceMappingURL=middleware-B-_0PLA6.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware-XMbIevSv.mjs","names":["next: MiddlewareNext"],"sources":["../src/middleware.ts"],"sourcesContent":["/**\n * @fileoverview Core middleware types and utilities.\n *\n * Provides middleware support for @fresho/router with a simple, composable API.\n *\n * @example Type-hinting context properties\n * ```typescript\n * // Define your context type (env is required, other properties are optional in middleware)\n * interface AppContext {\n * env: { JWT_SECRET: string; DB: D1Database };\n * user: { id: string; name: string };\n * }\n *\n * // Middleware receives env as required, other properties as optional\n * const authMiddleware: Middleware<AppContext> = async (context, next) => {\n * context.env.JWT_SECRET; // typed, required\n * context.user; // typed as optional (may not exist yet)\n * context.user = await validateToken(context.request);\n * return next();\n * };\n * ```\n */\n\nimport type { ExecutionContext } from './types.js';\n\n/**\n * Base context properties always available to middleware.\n */\ninterface BaseMiddlewareContext {\n request: Request;\n path: Record<string, string>;\n query: unknown;\n body: unknown;\n executionCtx?: ExecutionContext;\n}\n\n/**\n * Context passed to middleware functions.\n *\n * The `env` property is always required (extracted from Ctx if provided).\n * Other properties from Ctx are optional since middleware builds them up.\n *\n * @typeParam Ctx - Context type including env and middleware-added properties\n *\n * @example\n * ```typescript\n * interface AppContext {\n * env: { JWT_SECRET: string };\n * user: { id: string };\n * }\n *\n * const middleware: Middleware<AppContext> = async (context, next) => {\n * context.env.JWT_SECRET; // typed, required\n * context.user; // typed as { id: string } | undefined\n * context.user = { id: '123' };\n * return next();\n * };\n * ```\n */\nexport type MiddlewareContext<Ctx = {}> = BaseMiddlewareContext & {\n env: Ctx extends { env: infer E } ? E : unknown;\n} & Partial<Omit<Ctx, 'env'>> & {\n [key: string]: unknown;\n };\n\n/**\n * Function to continue to the next middleware or handler.\n */\nexport type MiddlewareNext = () => Promise<Response>;\n\n/**\n * Middleware function signature.\n *\n * @typeParam Ctx - Context type (default: {})\n *\n * @example\n * ```typescript\n * // Simple middleware without typed context\n * const logger: Middleware = async (context, next) => {\n * console.log(context.request.url);\n * return next();\n * };\n *\n * // Middleware with typed context\n * interface AppContext {\n * env: { JWT_SECRET: string };\n * user: { id: string };\n * }\n * const auth: Middleware<AppContext> = async (context, next) => {\n * const secret = context.env.JWT_SECRET; // typed, required\n * context.user = { id: '123' }; // adds to context\n * return next();\n * };\n * ```\n */\nexport type Middleware<Ctx = {}> = (\n context: MiddlewareContext<Ctx>,\n next: MiddlewareNext,\n) => Promise<Response> | Response;\n\n/**\n * Runs a chain of middleware with a final handler.\n *\n * Middleware are executed in order. Each middleware receives the context and\n * a `next` function. Calling `next()` continues to the next middleware or the\n * final handler. Returning without calling `next()` short-circuits the chain.\n *\n * @param middleware - Array of middleware functions to run\n * @param context - The request context\n * @param finalHandler - Handler to call after all middleware complete\n * @returns The response from the middleware chain or final handler\n */\nexport async function runMiddleware(\n middleware: Middleware[],\n context: MiddlewareContext,\n finalHandler: () => Promise<Response>,\n): Promise<Response> {\n let index = 0;\n\n const next: MiddlewareNext = async () => {\n if (index >= middleware.length) {\n return finalHandler();\n }\n\n const currentMiddleware = middleware[index++];\n return await currentMiddleware(context, next);\n };\n\n return next();\n}\n\n/**\n * Composes multiple middleware into a single middleware.\n *\n * Useful for grouping related middleware together.\n *\n * @example\n * ```typescript\n * const security = compose(\n * cors(),\n * rateLimit({ max: 100 }),\n * errorHandler(),\n * );\n *\n * const api = router('/api', routes, security);\n * ```\n *\n * @param middleware - Middleware functions to compose\n * @returns A single middleware that runs all provided middleware in order\n */\nexport function compose(...middleware: Middleware[]): Middleware {\n return async (context, next) => {\n return runMiddleware(middleware, context, next);\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAgHA,eAAsB,cACpB,YACA,SACA,cACmB;CACnB,IAAI,QAAQ;CAEZ,MAAMA,OAAuB,YAAY;AACvC,MAAI,SAAS,WAAW,OACtB,QAAO,cAAc;EAGvB,MAAM,oBAAoB,WAAW;AACrC,SAAO,MAAM,kBAAkB,SAAS,KAAK;;AAG/C,QAAO,MAAM;;;;;;;;;;;;;;;;;;;;;AAsBf,SAAgB,QAAQ,GAAG,YAAsC;AAC/D,QAAO,OAAO,SAAS,SAAS;AAC9B,SAAO,cAAc,YAAY,SAAS,KAAK"}
1
+ {"version":3,"file":"middleware-B-_0PLA6.mjs","names":["next: MiddlewareNext"],"sources":["../src/middleware.ts"],"sourcesContent":["/**\n * @fileoverview Core middleware types and utilities.\n *\n * Provides middleware support for @fresho/router with a simple, composable API.\n *\n * @example Type-hinting context properties\n * ```typescript\n * // Define your context type (env is required, other properties are optional in middleware)\n * interface AppContext {\n * env: { JWT_SECRET: string; DB: D1Database };\n * user: { id: string; name: string };\n * }\n *\n * // Middleware receives env as required, other properties as optional\n * const authMiddleware: Middleware<AppContext> = async (context, next) => {\n * context.env.JWT_SECRET; // typed, required\n * context.user; // typed as optional (may not exist yet)\n * context.user = await validateToken(context.request);\n * return next();\n * };\n * ```\n */\n\nimport type { ExecutionContext } from './types.js';\n\n/**\n * Base context properties always available to middleware.\n */\ninterface BaseMiddlewareContext {\n request: Request;\n path: Record<string, string>;\n query: unknown;\n body: unknown;\n executionCtx?: ExecutionContext;\n}\n\n/**\n * Context passed to middleware functions.\n *\n * The `env` property is always required (extracted from Ctx if provided).\n * Other properties from Ctx are optional since middleware builds them up.\n *\n * @typeParam Ctx - Context type including env and middleware-added properties\n *\n * @example\n * ```typescript\n * interface AppContext {\n * env: { JWT_SECRET: string };\n * user: { id: string };\n * }\n *\n * const middleware: Middleware<AppContext> = async (context, next) => {\n * context.env.JWT_SECRET; // typed, required\n * context.user; // typed as { id: string } | undefined\n * context.user = { id: '123' };\n * return next();\n * };\n * ```\n */\nexport type MiddlewareContext<Ctx = {}> = BaseMiddlewareContext & {\n env: Ctx extends { env: infer E } ? E : unknown;\n} & Partial<Omit<Ctx, 'env'>> & {\n [key: string]: unknown;\n };\n\n/**\n * Function to continue to the next middleware or handler.\n */\nexport type MiddlewareNext = () => Promise<Response>;\n\n/**\n * Middleware function signature.\n *\n * @typeParam Ctx - Context type (default: {})\n *\n * @example\n * ```typescript\n * // Simple middleware without typed context\n * const logger: Middleware = async (context, next) => {\n * console.log(context.request.url);\n * return next();\n * };\n *\n * // Middleware with typed context\n * interface AppContext {\n * env: { JWT_SECRET: string };\n * user: { id: string };\n * }\n * const auth: Middleware<AppContext> = async (context, next) => {\n * const secret = context.env.JWT_SECRET; // typed, required\n * context.user = { id: '123' }; // adds to context\n * return next();\n * };\n * ```\n */\nexport type Middleware<Ctx = {}> = (\n context: MiddlewareContext<Ctx>,\n next: MiddlewareNext,\n) => Promise<Response> | Response;\n\n/**\n * Runs a chain of middleware with a final handler.\n *\n * Middleware are executed in order. Each middleware receives the context and\n * a `next` function. Calling `next()` continues to the next middleware or the\n * final handler. Returning without calling `next()` short-circuits the chain.\n *\n * @param middleware - Array of middleware functions to run\n * @param context - The request context\n * @param finalHandler - Handler to call after all middleware complete\n * @returns The response from the middleware chain or final handler\n */\nexport async function runMiddleware(\n middleware: Middleware[],\n context: MiddlewareContext,\n finalHandler: () => Promise<Response>,\n): Promise<Response> {\n let index = 0;\n\n const next: MiddlewareNext = async () => {\n if (index >= middleware.length) {\n return finalHandler();\n }\n\n const currentMiddleware = middleware[index++];\n return await currentMiddleware(context, next);\n };\n\n return next();\n}\n\n/**\n * Composes multiple middleware into a single middleware.\n *\n * Useful for grouping related middleware together.\n *\n * @example\n * ```typescript\n * const security = compose(\n * cors(),\n * rateLimit({ max: 100 }),\n * errorHandler(),\n * );\n *\n * const api = router('/api', routes, security);\n * ```\n *\n * @param middleware - Middleware functions to compose\n * @returns A single middleware that runs all provided middleware in order\n */\nexport function compose(...middleware: Middleware[]): Middleware {\n return async (context, next) => {\n return runMiddleware(middleware, context, next);\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAgHA,eAAsB,cACpB,YACA,SACA,cACmB;CACnB,IAAI,QAAQ;CAEZ,MAAMA,OAAuB,YAAY;AACvC,MAAI,SAAS,WAAW,OACtB,QAAO,cAAc;EAGvB,MAAM,oBAAoB,WAAW;AACrC,SAAO,MAAM,kBAAkB,SAAS,KAAK;;AAG/C,QAAO,MAAM;;;;;;;;;;;;;;;;;;;;;AAsBf,SAAgB,QAAQ,GAAG,YAAsC;AAC/D,QAAO,OAAO,SAAS,SAAS;AAC9B,SAAO,cAAc,YAAY,SAAS,KAAK"}
@@ -181,6 +181,19 @@ type TypedResponse<T> = Response & {
181
181
  * Handler function type.
182
182
  */
183
183
  type TypedHandler<Q = unknown, B = unknown, P extends Record<string, string> = Record<string, string>, R = unknown, Ctx = {}> = (context: Context<Q, B, P, Ctx>) => R | Response | TypedResponse<R> | Promise<R | Response | TypedResponse<R>>;
184
+ /** MCP tool behavior hints; defaults come from the HTTP method. */
185
+ interface ToolAnnotations {
186
+ /** Human-readable tool title. */
187
+ title?: string;
188
+ /** The tool does not modify its environment. */
189
+ readOnlyHint?: boolean;
190
+ /** The tool may perform destructive updates. */
191
+ destructiveHint?: boolean;
192
+ /** Repeating the call with the same arguments has no additional effect. */
193
+ idempotentHint?: boolean;
194
+ /** The tool interacts with external entities. */
195
+ openWorldHint?: boolean;
196
+ }
184
197
  /**
185
198
  * Route definition - a single HTTP method handler with optional schemas.
186
199
  *
@@ -206,6 +219,14 @@ interface RouteDefinition<Q extends SchemaDefinition = {}, B extends SchemaDefin
206
219
  body?: B;
207
220
  /** Optional description for documentation. */
208
221
  description?: string;
222
+ /** Operation name, used as the OpenAPI `operationId` and the MCP tool name. */
223
+ operationId?: string;
224
+ /** Field descriptions for docs and MCP, keyed by name (`address.city` for nested fields). */
225
+ descriptions?: Record<string, string>;
226
+ /** MCP tool settings. */
227
+ mcp?: {
228
+ annotations?: ToolAnnotations;
229
+ };
209
230
  /** The request handler function. */
210
231
  handler: TypedHandler<InferSchema<Q>, InferSchema<B>, P, R, Ctx>;
211
232
  }
@@ -378,5 +399,5 @@ declare function runMiddleware(middleware: Middleware[], context: MiddlewareCont
378
399
  */
379
400
  declare function compose(...middleware: Middleware[]): Middleware;
380
401
  //#endregion
381
- export { SchemaDefinition as C, compileSchema as D, ValidationResult as E, InferSchema as S, SchemaTypeMap as T, TypedResponse as _, runMiddleware as a, isRouter as b, ExecutionContext as c, MethodEntry as d, RouteDefinition as f, TypedHandler as g, RouterRoutes as h, compose as i, FetchHandler as l, RouterBrand as m, MiddlewareContext as n, CollectPathParams as o, Router as p, MiddlewareNext as r, Context as s, Middleware as t, Method as u, isFunction as v, SchemaType as w, CompiledSchema as x, isRoute as y };
382
- //# sourceMappingURL=middleware-Bxq0LHLS.d.mts.map
402
+ export { InferSchema as C, ValidationResult as D, SchemaTypeMap as E, compileSchema as O, CompiledSchema as S, SchemaType as T, TypedHandler as _, runMiddleware as a, isRoute as b, ExecutionContext as c, MethodEntry as d, RouteDefinition as f, ToolAnnotations as g, RouterRoutes as h, compose as i, FetchHandler as l, RouterBrand as m, MiddlewareContext as n, CollectPathParams as o, Router as p, MiddlewareNext as r, Context as s, Middleware as t, Method as u, TypedResponse as v, SchemaDefinition as w, isRouter as x, isFunction as y };
403
+ //# sourceMappingURL=middleware-zRMBpTLe.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware-zRMBpTLe.d.mts","names":[],"sources":["../src/schema.ts","../src/types.ts","../src/middleware.ts"],"sourcesContent":[],"mappings":"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"}
@@ -9,9 +9,11 @@
9
9
  - [Cookie-Based Auth](#cookie-based-auth)
10
10
  - [Token-Based Auth](#token-based-auth)
11
11
  - [Token Refresh](#token-refresh)
12
+ - [MCP Endpoints](#mcp-endpoints)
12
13
  - [API Reference](#api-reference)
13
14
  - [jwtSign](#jwtsign)
14
15
  - [jwtVerify](#jwtverify)
16
+ - [createJwks](#createjwks)
15
17
  - [jwtAuth](#jwtauth)
16
18
 
17
19
  ---
@@ -287,6 +289,38 @@ refresh: router({
287
289
 
288
290
  ---
289
291
 
292
+ ## MCP Endpoints
293
+
294
+ To let agents call your API through [`@fresho/router/mcp`](../README.md#mcp-server) with tokens from a third-party OAuth provider, verify them with the provider's JWKS on the MCP endpoint. An unauthenticated call then gets an HTTP 401 whose `WWW-Authenticate` header points the client at your authorization server.
295
+
296
+ ```typescript
297
+ import { router } from '@fresho/router';
298
+ import { mcp, oauthProtectedResource } from '@fresho/router/mcp';
299
+ import { createJwks, jwtAuth } from '@fresho/router/middleware';
300
+
301
+ const issuer = 'https://example.auth0.com/';
302
+ const oauth = { authorizationServers: [issuer], scopes: ['users:read'] };
303
+
304
+ const auth = jwtAuth({
305
+ secret: createJwks(`${issuer}.well-known/jwks.json`),
306
+ algorithms: ['RS256'],
307
+ issuer,
308
+ // Tokens must be issued for this server, not just by the provider.
309
+ audience: 'https://api.example.com/mcp',
310
+ // Allow for clock drift between the provider and this server.
311
+ clockTolerance: 60,
312
+ claims: (payload) => ({ user: { id: payload.sub } }),
313
+ });
314
+
315
+ export const app = router({
316
+ users,
317
+ mcp: mcp({ users }, { name: 'my-api', version: '1.0.0', oauth }, auth),
318
+ '.well-known': oauthProtectedResource(oauth),
319
+ });
320
+ ```
321
+
322
+ What `auth` puts on the context (`c.user`) reaches the `users` handlers on every tool call.
323
+
290
324
  ## API Reference
291
325
 
292
326
  ### jwtSign
@@ -301,7 +335,8 @@ const token = await jwtSign(
301
335
  { email: 'user@example.com', role: 'admin' }, // Custom claims
302
336
  'your-secret-key', // Secret
303
337
  {
304
- algorithm: 'HS256', // HS256, HS384, or HS512 (default: HS256)
338
+ algorithm: 'HS256', // HS256/384/512, or RS*, PS*, ES256/384 with a private CryptoKey
339
+ keyId: 'key-1', // kid header (optional)
305
340
  expiresIn: '7d', // Token lifetime
306
341
  subject: 'user-123', // sub claim (typically user ID)
307
342
  issuer: 'my-app', // iss claim
@@ -314,7 +349,8 @@ const token = await jwtSign(
314
349
 
315
350
  | Option | Type | Description |
316
351
  |--------|------|-------------|
317
- | `algorithm` | `'HS256' \| 'HS384' \| 'HS512'` | Signing algorithm (default: `'HS256'`) |
352
+ | `algorithm` | `JwtAlgorithm` | Signing algorithm (default: `'HS256'`); asymmetric algorithms need a private `CryptoKey` |
353
+ | `keyId` | `string` | `kid` header, used by verifiers to pick a key from a JWKS |
318
354
  | `expiresIn` | `string \| number` | Token lifetime: `'1h'`, `'7d'`, `'30m'`, or seconds |
319
355
  | `notBefore` | `string \| number` | Token not valid until this time from now |
320
356
  | `subject` | `string` | `sub` claim (typically user ID) |
@@ -348,14 +384,43 @@ const payload = await jwtVerify(token, secret, { algorithms: ['HS512'] });
348
384
 
349
385
  | Option | Type | Description |
350
386
  |--------|------|-------------|
351
- | `algorithms` | `JwtAlgorithm[]` | Allowed algorithms (default: `['HS256']`) |
387
+ | `algorithms` | `JwtAlgorithm[]` | Allowed algorithms (default: `['HS256']` for HMAC secrets, `['RS256']` otherwise) |
388
+ | `issuer` | `string \| string[]` | Required `iss`; any listed value is accepted |
389
+ | `audience` | `string \| string[]` | Required `aud`; the token must contain at least one listed value |
390
+ | `clockTolerance` | `number` | Seconds of clock skew allowed on `exp` and `nbf` (default: `0`); 30-60 is typical for third-party tokens |
391
+
392
+ **Supported algorithms:** `HS256`, `HS384`, `HS512` (string, `ArrayBuffer`/`Uint8Array` or HMAC `CryptoKey`), and `RS256/384/512`, `PS256/384/512`, `ES256/384` (public `CryptoKey` or JWKS). A key is only used for the algorithm it was created for, so an `HS256` token can never be checked against a public key. RSA keys shorter than 2048 bits are rejected.
352
393
 
353
394
  **Throws:**
354
- - `'Invalid JWT format'` - Token doesn't have 3 parts
395
+ - `'Invalid JWT format'` - Token doesn't have 3 parts or a segment isn't a JSON object
355
396
  - `'Unsupported algorithm'` - Token uses algorithm not in allowed list
397
+ - `'Key does not match algorithm'` - The key or JWKS can't verify the token's algorithm
398
+ - `'No matching key in JWKS'` - No key (or more than one, when the token has no `kid`) fits the token
356
399
  - `'Invalid signature'` - Signature verification failed
357
400
  - `'Token expired'` - Token's `exp` claim is in the past
358
401
  - `'Token not yet valid'` - Token's `nbf` claim is in the future
402
+ - `'Invalid issuer'` / `'Invalid audience'` - `iss` or `aud` doesn't match
403
+
404
+ ### createJwks
405
+
406
+ Creates a cached remote JSON Web Key Set for verifying tokens issued by an OAuth or OpenID Connect provider (Auth0, Okta, Entra ID, Cognito, Keycloak, Google, ...). Create it once, at module scope, and pass it wherever a secret is expected.
407
+
408
+ ```typescript
409
+ import { createJwks, jwtVerify } from '@fresho/router/auth';
410
+
411
+ const jwks = createJwks('https://example.auth0.com/.well-known/jwks.json');
412
+
413
+ const payload = await jwtVerify(token, jwks, {
414
+ algorithms: ['RS256'],
415
+ issuer: 'https://example.auth0.com/',
416
+ audience: 'https://api.example.com',
417
+ });
418
+ ```
419
+
420
+ - Keys are picked by the token's `kid` and must match its algorithm (`kty`, `crv`, and `alg`/`use` when present). A token without `kid` is accepted only if exactly one key fits.
421
+ - Keys are cached for 10 minutes. An unknown `kid` triggers a reload, so key rotation is picked up. The provider is fetched at most once every 30 seconds (with a 5 second timeout), whether fetches succeed or fail, so random `kid`s or an outage can't hammer it. Concurrent requests share one fetch.
422
+ - If a reload fails or returns no keys, the previously fetched keys stay in use, even past the 10 minutes, and the reload is retried at most every 30 seconds. An outage at the provider therefore doesn't take down auth, but a key the provider revokes keeps verifying until a reload succeeds.
423
+ - Always set `issuer` and `audience` for third-party tokens; otherwise any token the provider signs for another application is accepted.
359
424
 
360
425
  ### jwtAuth
361
426
 
@@ -378,9 +443,13 @@ jwtAuth({
378
443
  },
379
444
  }),
380
445
 
381
- // Optional: allowed algorithms (default: ['HS256'])
446
+ // Optional: allowed algorithms (default: ['HS256'] for HMAC secrets, ['RS256'] otherwise)
382
447
  algorithms: ['HS256', 'HS384', 'HS512'],
383
448
 
449
+ // Optional: required iss / aud claims
450
+ issuer: 'my-app',
451
+ audience: 'my-api',
452
+
384
453
  // Optional: custom token extraction (default: Authorization header or 'token' cookie)
385
454
  getToken: (request) => {
386
455
  return request.headers.get('X-Auth-Token');
@@ -388,6 +457,21 @@ jwtAuth({
388
457
  });
389
458
  ```
390
459
 
460
+ **Third-party providers:** pass a key set from [`createJwks`](#createjwks) as the secret:
461
+
462
+ ```typescript
463
+ const jwks = createJwks('https://example.auth0.com/.well-known/jwks.json');
464
+
465
+ jwtAuth({
466
+ secret: jwks,
467
+ algorithms: ['RS256'],
468
+ issuer: 'https://example.auth0.com/',
469
+ audience: 'https://api.example.com',
470
+ clockTolerance: 60,
471
+ claims: (payload) => ({ user: { id: payload.sub } }),
472
+ });
473
+ ```
474
+
391
475
  **Token extraction order (default):**
392
476
  1. `Authorization: Bearer <token>` header
393
477
  2. `token` cookie
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@fresho/router",
3
3
  "engines": {
4
+ "bun": ">=1.3.14",
4
5
  "node": ">=22"
5
6
  },
6
- "version": "0.5.0",
7
+ "version": "0.6.0",
7
8
  "description": "Type-safe route definitions with auto-generated clients and OpenAPI docs",
8
9
  "license": "MIT",
9
10
  "author": "fresho.dev",
@@ -47,22 +48,38 @@
47
48
  "./middleware": {
48
49
  "types": "./dist/middleware/index.d.mts",
49
50
  "import": "./dist/middleware/index.mjs"
51
+ },
52
+ "./mcp": {
53
+ "types": "./dist/mcp/index.d.mts",
54
+ "import": "./dist/mcp/index.mjs"
50
55
  }
51
56
  },
52
57
  "scripts": {
53
- "build": "tsdown src/index.ts src/auth/index.ts src/middleware/index.ts",
58
+ "build": "tsdown src/index.ts src/auth/index.ts src/middleware/index.ts src/mcp/index.ts",
54
59
  "clean": "rm -rf ./dist",
55
60
  "typecheck": "tsc --noEmit",
56
61
  "lint": "biome check . && npm run typecheck",
57
62
  "lint:fix": "biome check --write .",
58
- "test": "npm run lint && node --import tsx --test 'src/**/*.test.ts'"
63
+ "test": "npm run lint && node --import tsx --test 'src/**/*.test.ts'",
64
+ "test:bun": "bun test src",
65
+ "test:workers": "npm run build && node --test test/cloudflare-workers.test.mjs"
66
+ },
67
+ "peerDependencies": {
68
+ "@modelcontextprotocol/sdk": "^1.32.1"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@modelcontextprotocol/sdk": {
72
+ "optional": true
73
+ }
59
74
  },
60
75
  "devDependencies": {
61
76
  "@biomejs/biome": "2.3.8",
77
+ "@modelcontextprotocol/sdk": "^1.32.1",
62
78
  "@types/node": "^22.0.0",
63
79
  "tsdown": "^0.17.0-beta.6",
64
80
  "tsx": "^4.19.0",
65
- "typescript": "^5.7.0"
81
+ "typescript": "^5.7.0",
82
+ "wrangler": "^4.114.0"
66
83
  },
67
84
  "files": [
68
85
  "dist",
@@ -1 +0,0 @@
1
- {"version":3,"file":"jwt-DHM_nSwL.d.mts","names":[],"sources":["../src/auth/jwt.ts"],"sourcesContent":[],"mappings":";;AA4BA;AAkBA;AAGA;AAGA;AAkBA;AAwFA;;;;;;AA6FA;;;;;;;;;;;UA/NiB,UAAA;;;;;;;;;;;;;;;;;KAkBL,YAAA;;KAGA,SAAA,YAAqB,cAAc;;UAG9B,cAAA;;cAEH;;;;;;;;;;;;sBAYQ;;;UAIL,gBAAA;;eAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsFO,OAAA,UACX,iCACD,qBACC,iBACR;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyFmB,SAAA,wBAEZ,qBACC,mBACR,QAAQ"}