@fresho/router 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.d.mts.map +1 -1
- package/dist/auth/index.mjs +12 -6
- package/dist/auth/index.mjs.map +1 -1
- package/dist/{docs-C77XQzF3.mjs → docs-BLjbxLe-.mjs} +13 -3
- package/dist/{docs-C77XQzF3.mjs.map → docs-BLjbxLe-.mjs.map} +1 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.d.mts +7 -0
- package/dist/mcp/index.d.mts.map +1 -1
- package/dist/mcp/index.mjs +5 -2
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/middleware/index.d.mts.map +1 -1
- package/dist/middleware/index.mjs +13 -13
- package/dist/middleware/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -413,6 +413,8 @@ const api = router({
|
|
|
413
413
|
|
|
414
414
|
`mcp()` requires endpoint middleware (usually auth). Pass `public: true` instead when the endpoint is open, or already guarded by middleware on its parent routers.
|
|
415
415
|
|
|
416
|
+
Besides `name` and `version`, the options take `instructions` for agents and `title`, `icons` (`{ src, mimeType?, sizes? }`) and `websiteUrl`, which clients may show in their UI.
|
|
417
|
+
|
|
416
418
|
- **Tools** — one per route, named from the method and path (`get_users`, `post_users`, `get_users_by_id`, ...) unless the route sets `operationId` (also used in OpenAPI). Descriptions come from `route.description`.
|
|
417
419
|
- **Arguments** — flat: path params, query fields and body fields side by side (`{ id, text }`), as JSON Schema derived from the route's schemas. Names must be unique across the three; a clash throws when `mcp()` is called. Use `[type, description]` tuples (e.g. `{ folder: ['string?', 'Mailbox folder'] }`) in `query` and `body` schemas to document fields for agents and OpenAPI. Array query params are left out, since the router can't parse them from query strings. Without a body schema, `post`/`put`/`patch` tools send any other arguments as the body.
|
|
418
420
|
- **Annotations** — `get` tools are marked `readOnlyHint`, `delete` tools `destructiveHint` and `idempotentHint`, and `put` tools `idempotentHint`, so clients can decide when to ask for confirmation. Override per route with `mcp: { annotations: { ... } }`.
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -210,7 +210,7 @@ interface RevokeTokenOptions {
|
|
|
210
210
|
* // Use in OAuth authorization URL
|
|
211
211
|
* ```
|
|
212
212
|
*/
|
|
213
|
-
declare function encodeOAuthState(data: Record<string, unknown>, secret: string): Promise<string>;
|
|
213
|
+
declare function encodeOAuthState(data: Record<string, unknown>, secret: string, ttlMs?: number): Promise<string>;
|
|
214
214
|
/**
|
|
215
215
|
* Decodes and verifies an HMAC-signed state parameter.
|
|
216
216
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/auth/basic.ts","../../src/auth/oauth.ts"],"sourcesContent":[],"mappings":";;;;;;;AA2BA;AA2BA;AAsCA;AAUA;;;;AClCA;AAgBA;AAYA;AA8CA;AAGA;AAcA;AAqCA;AAmDA;;;;;AAGU,UD/NO,gBAAA,CC+NP;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/auth/basic.ts","../../src/auth/oauth.ts"],"sourcesContent":[],"mappings":";;;;;;;AA2BA;AA2BA;AAsCA;AAUA;;;;AClCA;AAgBA;AAYA;AA8CA;AAGA;AAcA;AAqCA;AAmDA;;;;;AAGU,UD/NO,gBAAA,CC+NP;EAkEY;EAEX,QAAA,EAAA,MAAA;EACA;EAAR,QAAA,EAAA,MAAA;;AAqDH;;;;;;AAqDA;AAkCA;AAuCA;;;;;;;;iBD5bgB,cAAA,6BAA2C;;;;;;;;;;;;;;;;iBAsC3C,eAAA;;;;;;;iBAUA,qBAAA;;;;;;AA3EhB;AA2BA;AAsCA;AAUA;;;;AClCA;AAgBA;AAYA;AA8CA;AAGA;AAcA;AAqCA;AAmDA;;;;;;AAqEA;;;;;AAwDA;;;;;;AAqDA;AAkCA;AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;UA9aiB,kBAAA;;;;;;;;;;;;;;;UAgBA,aAAA;;;;;;;;;;;cAYJ,iBAAiB,eAAe;;KA8CjC,eAAA;;UAGK,mBAAA;;;;;;;;;;eAUF;;;UAIE,kBAAA;;;;;;;;;;;;eAYF;;;;;;;;;;;;;;;;;;;;iBAyBO,gBAAA,OACd,0DAGL;;;;;;;;;;;;;;;;iBA+CmB,2BAA2B,0BAA0B,yDAGxE,QAAQ;;;;;;;;;;;;;;;;;;;iBAkEW,YAAA,wBAEX,sBACR,QAAQ;;;;;;;;;;;;;;;;;;;iBAqDW,kBAAA,gCAEX,KAAK,sCACb,QAAQ;;;;;;;;;;;;;;;;;iBAkDW,WAAA,UAAqB,qBAAqB;;UAkC/C,uBAAA;;;;;;;;;;;;;;gBAcD;;;;;;;;;;;;;;;;;;;;;;;;iBAyBA,qBAAA,UAA+B"}
|
package/dist/auth/index.mjs
CHANGED
|
@@ -64,13 +64,15 @@ const OAUTH_PROVIDERS = {
|
|
|
64
64
|
* // Use in OAuth authorization URL
|
|
65
65
|
* ```
|
|
66
66
|
*/
|
|
67
|
-
async function encodeOAuthState(data, secret) {
|
|
67
|
+
async function encodeOAuthState(data, secret, ttlMs = 6e5) {
|
|
68
68
|
const nonceBytes = new Uint8Array(12);
|
|
69
69
|
crypto.getRandomValues(nonceBytes);
|
|
70
70
|
const nonce = btoa(String.fromCharCode(...nonceBytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
71
|
+
const exp = typeof data._exp === "number" ? data._exp : Date.now() + ttlMs;
|
|
71
72
|
const payload = JSON.stringify({
|
|
72
73
|
...data,
|
|
73
|
-
_nonce: nonce
|
|
74
|
+
_nonce: nonce,
|
|
75
|
+
_exp: exp
|
|
74
76
|
});
|
|
75
77
|
const encoder = new TextEncoder();
|
|
76
78
|
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
@@ -78,7 +80,7 @@ async function encodeOAuthState(data, secret) {
|
|
|
78
80
|
hash: "SHA-256"
|
|
79
81
|
}, false, ["sign"]);
|
|
80
82
|
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
81
|
-
const signatureHex = Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
83
|
+
const signatureHex = Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
82
84
|
const stateData = JSON.stringify({
|
|
83
85
|
payload,
|
|
84
86
|
sig: signatureHex
|
|
@@ -105,15 +107,19 @@ async function decodeOAuthState(state, secret) {
|
|
|
105
107
|
const base64 = state.replace(/-/g, "+").replace(/_/g, "/");
|
|
106
108
|
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
107
109
|
const { payload, sig } = JSON.parse(atob(padded));
|
|
110
|
+
if (typeof payload !== "string" || typeof sig !== "string" || !/^[0-9a-f]{64}$/i.test(sig)) return null;
|
|
111
|
+
const sigBytes = new Uint8Array(32);
|
|
112
|
+
for (let i = 0; i < 32; i++) sigBytes[i] = Number.parseInt(sig.slice(i * 2, i * 2 + 2), 16);
|
|
108
113
|
const encoder = new TextEncoder();
|
|
109
114
|
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
110
115
|
name: "HMAC",
|
|
111
116
|
hash: "SHA-256"
|
|
112
|
-
}, false, ["
|
|
113
|
-
|
|
114
|
-
if (sig !== Array.from(new Uint8Array(expectedSignature)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16)) return null;
|
|
117
|
+
}, false, ["verify"]);
|
|
118
|
+
if (!await crypto.subtle.verify("HMAC", key, sigBytes, encoder.encode(payload))) return null;
|
|
115
119
|
const payloadData = JSON.parse(payload);
|
|
120
|
+
if (typeof payloadData._exp !== "number" || Date.now() >= payloadData._exp) return null;
|
|
116
121
|
delete payloadData._nonce;
|
|
122
|
+
delete payloadData._exp;
|
|
117
123
|
return payloadData;
|
|
118
124
|
} catch {
|
|
119
125
|
return null;
|
package/dist/auth/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["OAUTH_PROVIDERS: Record<string, OAuthProvider>","headers: Record<string, string>"],"sources":["../../src/auth/oauth.ts"],"sourcesContent":["/**\n * @fileoverview OAuth 2.0 utilities for @fresho/router.\n *\n * Provides reusable primitives for implementing OAuth 2.0 Authorization Code flows.\n * This implementation is designed as a confidential client suitable for server-side\n * applications that can securely store client secrets.\n *\n * ## Design Decisions\n *\n * ### Why Not PKCE?\n *\n * PKCE (Proof Key for Code Exchange, RFC 7636) is a security extension designed to\n * protect against authorization code interception attacks. However:\n *\n * - PKCE is **required** only for public clients (mobile apps, SPAs without backend)\n * - PKCE is **optional** for confidential clients that can securely store secrets\n * - These utilities assume a confidential client with a `client_secret`\n * - The `client_secret` provides equivalent protection during token exchange\n *\n * ### HMAC-Signed State Parameter\n *\n * Instead of PKCE, we provide HMAC-signed state utilities that:\n *\n * 1. **Prevent CSRF attacks** - The signature is verified on callback\n * 2. **Carry custom data** - Embed arbitrary data (e.g., user ID) in the state\n * 3. **Include a nonce** - Random bytes prevent replay attacks\n * 4. **Use Web Crypto API** - Works in Cloudflare Workers, Deno, and browsers\n *\n * ## Provider Compatibility\n *\n * This implementation pattern works with ~60-70% of OAuth 2.0 providers:\n *\n * | Provider | PKCE | Auth Method | Compatible |\n * |------------|----------|-------------|------------|\n * | Google | Optional | Body | ✅ Yes |\n * | GitHub | None | Basic Auth | ✅ Minor tweak |\n * | Slack | None | Body | ✅ Yes |\n * | LinkedIn | None | Body | ✅ Yes |\n * | Spotify | Optional | Basic Auth | ✅ Yes |\n * | Discord | Optional | Body | ✅ Yes |\n * | Dropbox | None | Basic Auth | ✅ Minor tweak |\n * | Twitter | Required | - | ❌ Needs PKCE |\n * | TikTok | Required | - | ❌ Needs PKCE |\n *\n * @see https://tools.ietf.org/html/rfc6749 - OAuth 2.0 Authorization Framework\n * @see https://tools.ietf.org/html/rfc7636 - PKCE Extension\n *\n * @example\n * ```typescript\n * import { encodeOAuthState, decodeOAuthState, exchangeCode } from '@fresho/router/middleware';\n *\n * // In your authorize route:\n * const state = await encodeOAuthState({ uid: 'user-123' }, SECRET);\n * const authUrl = `https://provider.com/oauth/authorize?state=${state}&...`;\n *\n * // In your callback route:\n * const data = await decodeOAuthState(state, SECRET);\n * if (data) {\n * const tokens = await exchangeCode(code, { ... });\n * }\n * ```\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** OAuth 2.0 token response as per RFC 6749. */\nexport interface OAuthTokenResponse {\n /** The access token issued by the authorization server. */\n access_token: string;\n /** The refresh token (optional, returned with offline access). */\n refresh_token?: string;\n /** The type of token (typically \"Bearer\"). */\n token_type: string;\n /** The lifetime in seconds of the access token. */\n expires_in: number;\n /** The scopes granted by the authorization server. */\n scope?: string;\n /** OpenID Connect ID token (if openid scope was requested). */\n id_token?: string;\n}\n\n/** OAuth 2.0 provider configuration. */\nexport interface OAuthProvider {\n /** Authorization endpoint URL. */\n authorizationUrl: string;\n /** Token endpoint URL. */\n tokenUrl: string;\n /** Token revocation endpoint URL (optional). */\n revokeUrl?: string;\n /** User info endpoint URL (optional). */\n userInfoUrl?: string;\n}\n\n/** Common OAuth 2.0 providers with their endpoints. */\nexport const OAUTH_PROVIDERS: Record<string, OAuthProvider> = {\n google: {\n authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',\n tokenUrl: 'https://oauth2.googleapis.com/token',\n revokeUrl: 'https://oauth2.googleapis.com/revoke',\n userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',\n },\n github: {\n authorizationUrl: 'https://github.com/login/oauth/authorize',\n tokenUrl: 'https://github.com/login/oauth/access_token',\n revokeUrl: undefined,\n userInfoUrl: 'https://api.github.com/user',\n },\n discord: {\n authorizationUrl: 'https://discord.com/oauth2/authorize',\n tokenUrl: 'https://discord.com/api/oauth2/token',\n revokeUrl: 'https://discord.com/api/oauth2/token/revoke',\n userInfoUrl: 'https://discord.com/api/users/@me',\n },\n slack: {\n authorizationUrl: 'https://slack.com/openid/connect/authorize',\n tokenUrl: 'https://slack.com/api/openid.connect.token',\n revokeUrl: undefined,\n userInfoUrl: undefined,\n },\n linkedin: {\n authorizationUrl: 'https://www.linkedin.com/oauth/v2/authorization',\n tokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',\n revokeUrl: undefined,\n userInfoUrl: 'https://api.linkedin.com/v2/userinfo',\n },\n spotify: {\n authorizationUrl: 'https://accounts.spotify.com/authorize',\n tokenUrl: 'https://accounts.spotify.com/api/token',\n revokeUrl: undefined,\n userInfoUrl: 'https://api.spotify.com/v1/me',\n },\n dropbox: {\n authorizationUrl: 'https://www.dropbox.com/oauth2/authorize',\n tokenUrl: 'https://api.dropboxapi.com/oauth2/token',\n revokeUrl: 'https://api.dropboxapi.com/2/auth/token/revoke',\n userInfoUrl: undefined,\n },\n};\n\n/** Token endpoint authentication method. */\nexport type TokenAuthMethod = 'body' | 'basic';\n\n/** Configuration for token exchange. */\nexport interface ExchangeCodeOptions {\n /** Token endpoint URL. */\n tokenUrl: string;\n /** OAuth client ID. */\n clientId: string;\n /** OAuth client secret. */\n clientSecret: string;\n /** Redirect URI used in authorization request. */\n redirectUri: string;\n /** Authentication method (default: 'body'). */\n authMethod?: TokenAuthMethod;\n}\n\n/** Configuration for token revocation. */\nexport interface RevokeTokenOptions {\n /** Revocation endpoint URL. */\n revokeUrl: string;\n /** Token to revoke. */\n token: string;\n /** Token type hint (optional, e.g., 'refresh_token' or 'access_token'). */\n tokenTypeHint?: string;\n /** OAuth client ID (for providers that require it). */\n clientId?: string;\n /** OAuth client secret (for providers that require it). */\n clientSecret?: string;\n /** Authentication method (default: 'body'). */\n authMethod?: TokenAuthMethod;\n}\n\n// =============================================================================\n// State Encoding/Decoding (HMAC-signed)\n// =============================================================================\n\n/**\n * Encodes arbitrary data into an HMAC-signed state parameter.\n *\n * The state is base64url-encoded and includes:\n * - The JSON-serialized payload\n * - A random nonce (12 bytes) for replay protection\n * - An HMAC-SHA256 signature (truncated to 16 hex chars)\n *\n * @param data - Arbitrary data to encode in the state\n * @param secret - Secret key for HMAC signing\n * @returns Base64url-encoded state string\n *\n * @example\n * ```typescript\n * const state = await encodeOAuthState({ uid: 'user-123', returnTo: '/dashboard' }, SECRET);\n * // Use in OAuth authorization URL\n * ```\n */\nexport async function encodeOAuthState(\n data: Record<string, unknown>,\n secret: string,\n): Promise<string> {\n // Generate random nonce.\n const nonceBytes = new Uint8Array(12);\n crypto.getRandomValues(nonceBytes);\n const nonce = btoa(String.fromCharCode(...nonceBytes))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '');\n\n // Create payload with nonce.\n const payload = JSON.stringify({ ...data, _nonce: nonce });\n\n // Generate HMAC signature.\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));\n const signatureHex = Array.from(new Uint8Array(signature))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n .slice(0, 16);\n\n // Encode the full state.\n const stateData = JSON.stringify({ payload, sig: signatureHex });\n return btoa(stateData).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\n/**\n * Decodes and verifies an HMAC-signed state parameter.\n *\n * @param state - Base64url-encoded state string\n * @param secret - Secret key for HMAC verification\n * @returns The decoded data, or null if verification fails\n *\n * @example\n * ```typescript\n * const data = await decodeOAuthState(state, SECRET);\n * if (data) {\n * console.log(data.uid); // 'user-123'\n * }\n * ```\n */\nexport async function decodeOAuthState<T extends Record<string, unknown> = Record<string, unknown>>(\n state: string,\n secret: string,\n): Promise<T | null> {\n try {\n // Decode base64url.\n const base64 = state.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');\n const stateData = JSON.parse(atob(padded));\n\n const { payload, sig } = stateData;\n\n // Verify HMAC signature.\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const expectedSignature = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));\n const expectedSigHex = Array.from(new Uint8Array(expectedSignature))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n .slice(0, 16);\n\n // Constant-time comparison (simple version).\n if (sig !== expectedSigHex) {\n return null;\n }\n\n // Extract data from payload (remove internal _nonce field).\n const payloadData = JSON.parse(payload);\n delete payloadData._nonce;\n return payloadData as T;\n } catch {\n return null;\n }\n}\n\n// =============================================================================\n// Token Exchange\n// =============================================================================\n\n/**\n * Exchanges an authorization code for tokens.\n *\n * @param code - Authorization code from OAuth callback\n * @param options - Token exchange configuration\n * @returns Token response from the OAuth provider\n * @throws Error if token exchange fails\n *\n * @example\n * ```typescript\n * const tokens = await exchangeCode(code, {\n * tokenUrl: OAUTH_PROVIDERS.google.tokenUrl,\n * clientId: env.CLIENT_ID,\n * clientSecret: env.CLIENT_SECRET,\n * redirectUri: 'https://example.com/callback',\n * });\n * ```\n */\nexport async function exchangeCode(\n code: string,\n options: ExchangeCodeOptions,\n): Promise<OAuthTokenResponse> {\n const { tokenUrl, clientId, clientSecret, redirectUri, authMethod = 'body' } = options;\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n });\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n };\n\n if (authMethod === 'basic') {\n headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;\n } else {\n body.set('client_id', clientId);\n body.set('client_secret', clientSecret);\n }\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers,\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed: ${response.status} ${errorText}`);\n }\n\n return response.json();\n}\n\n/**\n * Refreshes an access token using a refresh token.\n *\n * @param refreshToken - The refresh token\n * @param options - Token exchange configuration (same as exchangeCode)\n * @returns New token response from the OAuth provider\n * @throws Error if token refresh fails\n *\n * @example\n * ```typescript\n * const tokens = await refreshAccessToken(refreshToken, {\n * tokenUrl: OAUTH_PROVIDERS.google.tokenUrl,\n * clientId: env.CLIENT_ID,\n * clientSecret: env.CLIENT_SECRET,\n * redirectUri: 'https://example.com/callback',\n * });\n * ```\n */\nexport async function refreshAccessToken(\n refreshToken: string,\n options: Omit<ExchangeCodeOptions, 'redirectUri'>,\n): Promise<OAuthTokenResponse> {\n const { tokenUrl, clientId, clientSecret, authMethod = 'body' } = options;\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n });\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n };\n\n if (authMethod === 'basic') {\n headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;\n } else {\n body.set('client_id', clientId);\n body.set('client_secret', clientSecret);\n }\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers,\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed: ${response.status} ${errorText}`);\n }\n\n return response.json();\n}\n\n/**\n * Revokes an OAuth token.\n *\n * @param options - Token revocation configuration\n * @returns True if revocation succeeded\n * @throws Error if revocation fails\n *\n * @example\n * ```typescript\n * await revokeToken({\n * revokeUrl: OAUTH_PROVIDERS.google.revokeUrl!,\n * token: refreshToken,\n * tokenTypeHint: 'refresh_token',\n * });\n * ```\n */\nexport async function revokeToken(options: RevokeTokenOptions): Promise<boolean> {\n const { revokeUrl, token, tokenTypeHint, clientId, clientSecret, authMethod = 'body' } = options;\n\n const body = new URLSearchParams({ token });\n if (tokenTypeHint) {\n body.set('token_type_hint', tokenTypeHint);\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (authMethod === 'basic' && clientId && clientSecret) {\n headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;\n } else if (clientId && clientSecret) {\n body.set('client_id', clientId);\n body.set('client_secret', clientSecret);\n }\n\n const response = await fetch(revokeUrl, {\n method: 'POST',\n headers,\n body: body.toString(),\n });\n\n // Some providers return 200, others return 204 for successful revocation.\n return response.ok;\n}\n\n// =============================================================================\n// Authorization URL Builder\n// =============================================================================\n\n/** Options for building an authorization URL. */\nexport interface AuthorizationUrlOptions {\n /** Authorization endpoint URL. */\n authorizationUrl: string;\n /** OAuth client ID. */\n clientId: string;\n /** Redirect URI for OAuth callback. */\n redirectUri: string;\n /** Requested scopes. */\n scopes: string[];\n /** State parameter (use encodeOAuthState to generate). */\n state: string;\n /** Response type (default: 'code'). */\n responseType?: string;\n /** Additional provider-specific parameters. */\n extraParams?: Record<string, string>;\n}\n\n/**\n * Builds an OAuth authorization URL.\n *\n * @param options - Authorization URL configuration\n * @returns The complete authorization URL\n *\n * @example\n * ```typescript\n * const state = await encodeOAuthState({ uid: 'user-123' }, SECRET);\n * const authUrl = buildAuthorizationUrl({\n * authorizationUrl: OAUTH_PROVIDERS.google.authorizationUrl,\n * clientId: env.CLIENT_ID,\n * redirectUri: 'https://example.com/callback',\n * scopes: ['openid', 'email', 'profile'],\n * state,\n * extraParams: {\n * access_type: 'offline',\n * prompt: 'consent',\n * },\n * });\n * ```\n */\nexport function buildAuthorizationUrl(options: AuthorizationUrlOptions): string {\n const {\n authorizationUrl,\n clientId,\n redirectUri,\n scopes,\n state,\n responseType = 'code',\n extraParams = {},\n } = options;\n\n const params = new URLSearchParams({\n response_type: responseType,\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: scopes.join(' '),\n state,\n ...extraParams,\n });\n\n return `${authorizationUrl}?${params.toString()}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["OAUTH_PROVIDERS: Record<string, OAuthProvider>","headers: Record<string, string>"],"sources":["../../src/auth/oauth.ts"],"sourcesContent":["/**\n * @fileoverview OAuth 2.0 utilities for @fresho/router.\n *\n * Provides reusable primitives for implementing OAuth 2.0 Authorization Code flows.\n * This implementation is designed as a confidential client suitable for server-side\n * applications that can securely store client secrets.\n *\n * ## Design Decisions\n *\n * ### Why Not PKCE?\n *\n * PKCE (Proof Key for Code Exchange, RFC 7636) is a security extension designed to\n * protect against authorization code interception attacks. However:\n *\n * - PKCE is **required** only for public clients (mobile apps, SPAs without backend)\n * - PKCE is **optional** for confidential clients that can securely store secrets\n * - These utilities assume a confidential client with a `client_secret`\n * - The `client_secret` provides equivalent protection during token exchange\n *\n * ### HMAC-Signed State Parameter\n *\n * Instead of PKCE, we provide HMAC-signed state utilities that:\n *\n * 1. **Prevent CSRF attacks** - The signature is verified on callback\n * 2. **Carry custom data** - Embed arbitrary data (e.g., user ID) in the state\n * 3. **Include a nonce** - Random bytes prevent replay attacks\n * 4. **Use Web Crypto API** - Works in Cloudflare Workers, Deno, and browsers\n *\n * ## Provider Compatibility\n *\n * This implementation pattern works with ~60-70% of OAuth 2.0 providers:\n *\n * | Provider | PKCE | Auth Method | Compatible |\n * |------------|----------|-------------|------------|\n * | Google | Optional | Body | ✅ Yes |\n * | GitHub | None | Basic Auth | ✅ Minor tweak |\n * | Slack | None | Body | ✅ Yes |\n * | LinkedIn | None | Body | ✅ Yes |\n * | Spotify | Optional | Basic Auth | ✅ Yes |\n * | Discord | Optional | Body | ✅ Yes |\n * | Dropbox | None | Basic Auth | ✅ Minor tweak |\n * | Twitter | Required | - | ❌ Needs PKCE |\n * | TikTok | Required | - | ❌ Needs PKCE |\n *\n * @see https://tools.ietf.org/html/rfc6749 - OAuth 2.0 Authorization Framework\n * @see https://tools.ietf.org/html/rfc7636 - PKCE Extension\n *\n * @example\n * ```typescript\n * import { encodeOAuthState, decodeOAuthState, exchangeCode } from '@fresho/router/middleware';\n *\n * // In your authorize route:\n * const state = await encodeOAuthState({ uid: 'user-123' }, SECRET);\n * const authUrl = `https://provider.com/oauth/authorize?state=${state}&...`;\n *\n * // In your callback route:\n * const data = await decodeOAuthState(state, SECRET);\n * if (data) {\n * const tokens = await exchangeCode(code, { ... });\n * }\n * ```\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** OAuth 2.0 token response as per RFC 6749. */\nexport interface OAuthTokenResponse {\n /** The access token issued by the authorization server. */\n access_token: string;\n /** The refresh token (optional, returned with offline access). */\n refresh_token?: string;\n /** The type of token (typically \"Bearer\"). */\n token_type: string;\n /** The lifetime in seconds of the access token. */\n expires_in: number;\n /** The scopes granted by the authorization server. */\n scope?: string;\n /** OpenID Connect ID token (if openid scope was requested). */\n id_token?: string;\n}\n\n/** OAuth 2.0 provider configuration. */\nexport interface OAuthProvider {\n /** Authorization endpoint URL. */\n authorizationUrl: string;\n /** Token endpoint URL. */\n tokenUrl: string;\n /** Token revocation endpoint URL (optional). */\n revokeUrl?: string;\n /** User info endpoint URL (optional). */\n userInfoUrl?: string;\n}\n\n/** Common OAuth 2.0 providers with their endpoints. */\nexport const OAUTH_PROVIDERS: Record<string, OAuthProvider> = {\n google: {\n authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',\n tokenUrl: 'https://oauth2.googleapis.com/token',\n revokeUrl: 'https://oauth2.googleapis.com/revoke',\n userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',\n },\n github: {\n authorizationUrl: 'https://github.com/login/oauth/authorize',\n tokenUrl: 'https://github.com/login/oauth/access_token',\n revokeUrl: undefined,\n userInfoUrl: 'https://api.github.com/user',\n },\n discord: {\n authorizationUrl: 'https://discord.com/oauth2/authorize',\n tokenUrl: 'https://discord.com/api/oauth2/token',\n revokeUrl: 'https://discord.com/api/oauth2/token/revoke',\n userInfoUrl: 'https://discord.com/api/users/@me',\n },\n slack: {\n authorizationUrl: 'https://slack.com/openid/connect/authorize',\n tokenUrl: 'https://slack.com/api/openid.connect.token',\n revokeUrl: undefined,\n userInfoUrl: undefined,\n },\n linkedin: {\n authorizationUrl: 'https://www.linkedin.com/oauth/v2/authorization',\n tokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',\n revokeUrl: undefined,\n userInfoUrl: 'https://api.linkedin.com/v2/userinfo',\n },\n spotify: {\n authorizationUrl: 'https://accounts.spotify.com/authorize',\n tokenUrl: 'https://accounts.spotify.com/api/token',\n revokeUrl: undefined,\n userInfoUrl: 'https://api.spotify.com/v1/me',\n },\n dropbox: {\n authorizationUrl: 'https://www.dropbox.com/oauth2/authorize',\n tokenUrl: 'https://api.dropboxapi.com/oauth2/token',\n revokeUrl: 'https://api.dropboxapi.com/2/auth/token/revoke',\n userInfoUrl: undefined,\n },\n};\n\n/** Token endpoint authentication method. */\nexport type TokenAuthMethod = 'body' | 'basic';\n\n/** Configuration for token exchange. */\nexport interface ExchangeCodeOptions {\n /** Token endpoint URL. */\n tokenUrl: string;\n /** OAuth client ID. */\n clientId: string;\n /** OAuth client secret. */\n clientSecret: string;\n /** Redirect URI used in authorization request. */\n redirectUri: string;\n /** Authentication method (default: 'body'). */\n authMethod?: TokenAuthMethod;\n}\n\n/** Configuration for token revocation. */\nexport interface RevokeTokenOptions {\n /** Revocation endpoint URL. */\n revokeUrl: string;\n /** Token to revoke. */\n token: string;\n /** Token type hint (optional, e.g., 'refresh_token' or 'access_token'). */\n tokenTypeHint?: string;\n /** OAuth client ID (for providers that require it). */\n clientId?: string;\n /** OAuth client secret (for providers that require it). */\n clientSecret?: string;\n /** Authentication method (default: 'body'). */\n authMethod?: TokenAuthMethod;\n}\n\n// =============================================================================\n// State Encoding/Decoding (HMAC-signed)\n// =============================================================================\n\n/**\n * Encodes arbitrary data into an HMAC-signed state parameter.\n *\n * The state is base64url-encoded and includes:\n * - The JSON-serialized payload\n * - A random nonce (12 bytes) for replay protection\n * - An HMAC-SHA256 signature (truncated to 16 hex chars)\n *\n * @param data - Arbitrary data to encode in the state\n * @param secret - Secret key for HMAC signing\n * @returns Base64url-encoded state string\n *\n * @example\n * ```typescript\n * const state = await encodeOAuthState({ uid: 'user-123', returnTo: '/dashboard' }, SECRET);\n * // Use in OAuth authorization URL\n * ```\n */\nexport async function encodeOAuthState(\n data: Record<string, unknown>,\n secret: string,\n ttlMs = 600_000,\n): Promise<string> {\n // Generate random nonce.\n const nonceBytes = new Uint8Array(12);\n crypto.getRandomValues(nonceBytes);\n const nonce = btoa(String.fromCharCode(...nonceBytes))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '');\n\n const exp = typeof data._exp === 'number' ? data._exp : Date.now() + ttlMs;\n const payload = JSON.stringify({ ...data, _nonce: nonce, _exp: exp });\n\n // Generate HMAC signature.\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));\n const signatureHex = Array.from(new Uint8Array(signature))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n\n // Encode the full state.\n const stateData = JSON.stringify({ payload, sig: signatureHex });\n return btoa(stateData).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\n/**\n * Decodes and verifies an HMAC-signed state parameter.\n *\n * @param state - Base64url-encoded state string\n * @param secret - Secret key for HMAC verification\n * @returns The decoded data, or null if verification fails\n *\n * @example\n * ```typescript\n * const data = await decodeOAuthState(state, SECRET);\n * if (data) {\n * console.log(data.uid); // 'user-123'\n * }\n * ```\n */\nexport async function decodeOAuthState<T extends Record<string, unknown> = Record<string, unknown>>(\n state: string,\n secret: string,\n): Promise<T | null> {\n try {\n // Decode base64url.\n const base64 = state.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');\n const stateData = JSON.parse(atob(padded));\n\n const { payload, sig } = stateData;\n if (typeof payload !== 'string' || typeof sig !== 'string' || !/^[0-9a-f]{64}$/i.test(sig)) {\n return null;\n }\n\n const sigBytes = new Uint8Array(32);\n for (let i = 0; i < 32; i++) {\n sigBytes[i] = Number.parseInt(sig.slice(i * 2, i * 2 + 2), 16);\n }\n\n // Verify HMAC signature.\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['verify'],\n );\n\n const valid = await crypto.subtle.verify('HMAC', key, sigBytes, encoder.encode(payload));\n if (!valid) {\n return null;\n }\n\n const payloadData = JSON.parse(payload);\n if (typeof payloadData._exp !== 'number' || Date.now() >= payloadData._exp) {\n return null;\n }\n delete payloadData._nonce;\n delete payloadData._exp;\n return payloadData as T;\n } catch {\n return null;\n }\n}\n\n// =============================================================================\n// Token Exchange\n// =============================================================================\n\n/**\n * Exchanges an authorization code for tokens.\n *\n * @param code - Authorization code from OAuth callback\n * @param options - Token exchange configuration\n * @returns Token response from the OAuth provider\n * @throws Error if token exchange fails\n *\n * @example\n * ```typescript\n * const tokens = await exchangeCode(code, {\n * tokenUrl: OAUTH_PROVIDERS.google.tokenUrl,\n * clientId: env.CLIENT_ID,\n * clientSecret: env.CLIENT_SECRET,\n * redirectUri: 'https://example.com/callback',\n * });\n * ```\n */\nexport async function exchangeCode(\n code: string,\n options: ExchangeCodeOptions,\n): Promise<OAuthTokenResponse> {\n const { tokenUrl, clientId, clientSecret, redirectUri, authMethod = 'body' } = options;\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n });\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n };\n\n if (authMethod === 'basic') {\n headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;\n } else {\n body.set('client_id', clientId);\n body.set('client_secret', clientSecret);\n }\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers,\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed: ${response.status} ${errorText}`);\n }\n\n return response.json();\n}\n\n/**\n * Refreshes an access token using a refresh token.\n *\n * @param refreshToken - The refresh token\n * @param options - Token exchange configuration (same as exchangeCode)\n * @returns New token response from the OAuth provider\n * @throws Error if token refresh fails\n *\n * @example\n * ```typescript\n * const tokens = await refreshAccessToken(refreshToken, {\n * tokenUrl: OAUTH_PROVIDERS.google.tokenUrl,\n * clientId: env.CLIENT_ID,\n * clientSecret: env.CLIENT_SECRET,\n * redirectUri: 'https://example.com/callback',\n * });\n * ```\n */\nexport async function refreshAccessToken(\n refreshToken: string,\n options: Omit<ExchangeCodeOptions, 'redirectUri'>,\n): Promise<OAuthTokenResponse> {\n const { tokenUrl, clientId, clientSecret, authMethod = 'body' } = options;\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n });\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n };\n\n if (authMethod === 'basic') {\n headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;\n } else {\n body.set('client_id', clientId);\n body.set('client_secret', clientSecret);\n }\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers,\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed: ${response.status} ${errorText}`);\n }\n\n return response.json();\n}\n\n/**\n * Revokes an OAuth token.\n *\n * @param options - Token revocation configuration\n * @returns True if revocation succeeded\n * @throws Error if revocation fails\n *\n * @example\n * ```typescript\n * await revokeToken({\n * revokeUrl: OAUTH_PROVIDERS.google.revokeUrl!,\n * token: refreshToken,\n * tokenTypeHint: 'refresh_token',\n * });\n * ```\n */\nexport async function revokeToken(options: RevokeTokenOptions): Promise<boolean> {\n const { revokeUrl, token, tokenTypeHint, clientId, clientSecret, authMethod = 'body' } = options;\n\n const body = new URLSearchParams({ token });\n if (tokenTypeHint) {\n body.set('token_type_hint', tokenTypeHint);\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (authMethod === 'basic' && clientId && clientSecret) {\n headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;\n } else if (clientId && clientSecret) {\n body.set('client_id', clientId);\n body.set('client_secret', clientSecret);\n }\n\n const response = await fetch(revokeUrl, {\n method: 'POST',\n headers,\n body: body.toString(),\n });\n\n // Some providers return 200, others return 204 for successful revocation.\n return response.ok;\n}\n\n// =============================================================================\n// Authorization URL Builder\n// =============================================================================\n\n/** Options for building an authorization URL. */\nexport interface AuthorizationUrlOptions {\n /** Authorization endpoint URL. */\n authorizationUrl: string;\n /** OAuth client ID. */\n clientId: string;\n /** Redirect URI for OAuth callback. */\n redirectUri: string;\n /** Requested scopes. */\n scopes: string[];\n /** State parameter (use encodeOAuthState to generate). */\n state: string;\n /** Response type (default: 'code'). */\n responseType?: string;\n /** Additional provider-specific parameters. */\n extraParams?: Record<string, string>;\n}\n\n/**\n * Builds an OAuth authorization URL.\n *\n * @param options - Authorization URL configuration\n * @returns The complete authorization URL\n *\n * @example\n * ```typescript\n * const state = await encodeOAuthState({ uid: 'user-123' }, SECRET);\n * const authUrl = buildAuthorizationUrl({\n * authorizationUrl: OAUTH_PROVIDERS.google.authorizationUrl,\n * clientId: env.CLIENT_ID,\n * redirectUri: 'https://example.com/callback',\n * scopes: ['openid', 'email', 'profile'],\n * state,\n * extraParams: {\n * access_type: 'offline',\n * prompt: 'consent',\n * },\n * });\n * ```\n */\nexport function buildAuthorizationUrl(options: AuthorizationUrlOptions): string {\n const {\n authorizationUrl,\n clientId,\n redirectUri,\n scopes,\n state,\n responseType = 'code',\n extraParams = {},\n } = options;\n\n const params = new URLSearchParams({\n response_type: responseType,\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: scopes.join(' '),\n state,\n ...extraParams,\n });\n\n return `${authorizationUrl}?${params.toString()}`;\n}\n"],"mappings":"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"}
|
|
@@ -411,7 +411,13 @@ function createHandler(routerDef) {
|
|
|
411
411
|
const match = compiled.pattern.exec(url.pathname);
|
|
412
412
|
if (!match) continue;
|
|
413
413
|
const path = {};
|
|
414
|
-
|
|
414
|
+
let invalidPath = false;
|
|
415
|
+
for (let i = 0; i < compiled.paramNames.length; i++) try {
|
|
416
|
+
path[compiled.paramNames[i]] = decodeURIComponent(match[i + 1]);
|
|
417
|
+
} catch {
|
|
418
|
+
invalidPath = true;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
415
421
|
const context = {
|
|
416
422
|
request,
|
|
417
423
|
path,
|
|
@@ -420,8 +426,12 @@ function createHandler(routerDef) {
|
|
|
420
426
|
env,
|
|
421
427
|
executionCtx: ctx
|
|
422
428
|
};
|
|
423
|
-
if (compiled.middleware.length > 0 || compiled.handler || compiled.querySchema || compiled.bodySchema) return stripBodyForHead(await runMiddleware(compiled.middleware, context, async () => {
|
|
429
|
+
if (invalidPath || compiled.middleware.length > 0 || compiled.handler || compiled.querySchema || compiled.bodySchema) return stripBodyForHead(await runMiddleware(compiled.middleware, context, async () => {
|
|
424
430
|
const routeId = `${compiled.method} ${url.pathname}`;
|
|
431
|
+
if (invalidPath) return Response.json({
|
|
432
|
+
error: "Invalid path parameters",
|
|
433
|
+
route: routeId
|
|
434
|
+
}, { status: 400 });
|
|
425
435
|
let query = {};
|
|
426
436
|
if (compiled.querySchema) {
|
|
427
437
|
const raw = Object.fromEntries(url.searchParams);
|
|
@@ -735,4 +745,4 @@ function generateDocs(config) {
|
|
|
735
745
|
|
|
736
746
|
//#endregion
|
|
737
747
|
export { isFunction as a, compileSchema as c, unpackField as d, router as i, isNestedSchema as l, generateDocs as n, isRoute as o, route as r, isRouter as s, collectRoutes as t, toJsonSchema as u };
|
|
738
|
-
//# sourceMappingURL=docs-
|
|
748
|
+
//# sourceMappingURL=docs-BLjbxLe-.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs-C77XQzF3.mjs","names":["errors: Record<string, string[]>","resultArray: unknown[]","success","coerced","errorMsg","result: Record<string, unknown>","allErrors: Record<string, string[]>","properties: Record<string, unknown>","required: string[]","paramNames: string[]","propertyToSegment","compiled: CompiledRoute[]","path: Record<string, string>","context: MiddlewareContext","query: unknown","body: unknown","collectedRoutes: CollectedRoute[]","paths: Record<string, Record<string, object>>","operation: Record<string, unknown>","route"],"sources":["../src/schema.ts","../src/types.ts","../src/handler.ts","../src/core.ts","../src/docs.ts"],"sourcesContent":["/**\n * @fileoverview Schema types and validation utilities.\n *\n * Provides shorthand schema definitions for validating query parameters and request bodies.\n * No external dependencies - uses simple string-based type definitions.\n *\n * Supported types:\n * - Primitives: `'string'`, `'number'`, `'boolean'`\n * - Optional: `'string?'`, `'number?'`, `'boolean?'`\n * - Arrays: `'string[]'`, `'number[]'`, `'boolean[]'`\n * - Optional arrays: `'string[]?'`, `'number[]?'`, `'boolean[]?'`\n * - Described fields: `['string', 'Description']`, `['number?', 'Optional count']`\n * - Nested objects: `{ field: { nested: 'string' } }`\n *\n * @example\n * ```typescript\n * // In route definitions\n * const api = router({\n * users: router({\n * post: route({\n * query: { page: 'number?', limit: ['number?', 'Max results'] },\n * body: {\n * name: ['string', 'Full name'],\n * age: 'number',\n * tags: 'string[]?',\n * address: { city: 'string', zip: 'string?' },\n * },\n * handler: async (c) => {\n * c.query.page; // number | undefined\n * c.body.name; // string\n * c.body.address.city // string\n * },\n * }),\n * }),\n * });\n * ```\n */\n\n/** Primitive type strings. */\nexport type PrimitiveType = 'string' | 'number' | 'boolean' | 'unknown';\n\n/** Optional primitive type strings. */\nexport type OptionalPrimitiveType = 'string?' | 'number?' | 'boolean?' | 'unknown?';\n\n/** Array type strings. */\nexport type ArrayType = 'string[]' | 'number[]' | 'boolean[]' | 'unknown[]';\n\n/** Optional array type strings. */\nexport type OptionalArrayType = 'string[]?' | 'number[]?' | 'boolean[]?' | 'unknown[]?';\n\n/** All supported type strings. */\nexport type SchemaType = PrimitiveType | OptionalPrimitiveType | ArrayType | OptionalArrayType;\n\n/** A schema field: a type string, or a `[type, description]` tuple. */\nexport type SchemaField = SchemaType | readonly [type: SchemaType, description: string];\n\n/** Schema definition using shorthand syntax. Supports nested objects. */\nexport interface SchemaDefinition {\n [key: string]: SchemaField | SchemaDefinition;\n}\n\n/** Maps primitive type strings to TypeScript types. */\nexport type SchemaTypeMap = {\n string: string;\n 'string?': string | undefined;\n number: number;\n 'number?': number | undefined;\n boolean: boolean;\n 'boolean?': boolean | undefined;\n unknown: unknown;\n 'unknown?': unknown | undefined;\n 'string[]': string[];\n 'string[]?': string[] | undefined;\n 'number[]': number[];\n 'number[]?': number[] | undefined;\n 'boolean[]': boolean[];\n 'boolean[]?': boolean[] | undefined;\n 'unknown[]': unknown[];\n 'unknown[]?': unknown[] | undefined;\n};\n\n/** Checks if a schema field type is optional (ends with ?). */\ntype IsOptionalField<T> = T extends `${string}?`\n ? true\n : T extends readonly [`${string}?`, string]\n ? true\n : false;\n\n/** Infers TypeScript type from a schema field (type string, tuple, or nested object). */\nexport type InferSchemaField<T> = T extends SchemaType\n ? SchemaTypeMap[T]\n : T extends readonly [infer S extends SchemaType, string]\n ? SchemaTypeMap[S]\n : T extends SchemaDefinition\n ? InferSchema<T>\n : never;\n\n/** Gets keys of required fields (non-optional types). */\ntype RequiredKeys<T extends SchemaDefinition> = {\n [K in keyof T]: IsOptionalField<T[K]> extends true ? never : K;\n}[keyof T];\n\n/** Gets keys of optional fields (types ending with ?). */\ntype OptionalKeys<T extends SchemaDefinition> = {\n [K in keyof T]: IsOptionalField<T[K]> extends true ? K : never;\n}[keyof T];\n\n/**\n * Infers TypeScript type from schema definition.\n *\n * Required fields become required properties, optional fields (ending with ?)\n * become optional properties with `| undefined`.\n */\nexport type InferSchema<T extends SchemaDefinition> = {\n [K in RequiredKeys<T>]: InferSchemaField<T[K]>;\n} & {\n [K in OptionalKeys<T>]?: InferSchemaField<T[K]>;\n};\n\n/** Error object returned when validation fails. */\nexport interface ValidationError {\n flatten: () => { fieldErrors: Record<string, string[]> };\n}\n\n/** Validation result matching Zod's safeParse API for compatibility. */\nexport type ValidationResult<T> =\n | { success: true; data: T; error?: undefined }\n | { success: false; data?: undefined; error: ValidationError };\n\n/** Compiled schema with validate method. */\nexport interface CompiledSchema<T> {\n safeParse: (data: object) => ValidationResult<T>;\n}\n\n/** Valid base types for primitives and arrays. */\nconst PRIMITIVE_TYPES = ['string', 'number', 'boolean', 'unknown'];\n\n/** Checks if a schema field is a nested object (not a type string or [type, description] tuple). */\nexport function isNestedSchema(field: SchemaField | SchemaDefinition): field is SchemaDefinition {\n return typeof field === 'object' && field !== null && !Array.isArray(field);\n}\n\n/** Unpacks a schema field into its type string and optional description. */\nexport function unpackField(field: SchemaField): [type: SchemaType, description?: string] {\n return Array.isArray(field) ? [field[0], field[1]] : [field as SchemaType, undefined];\n}\n\n/**\n * Validates and coerces a single primitive value.\n * Returns [success, result, errorMessage].\n */\nfunction validatePrimitive(value: unknown, baseType: string): [boolean, unknown, string | null] {\n switch (baseType) {\n case 'string':\n if (typeof value !== 'string') {\n return [false, undefined, 'Expected string'];\n }\n return [true, value, null];\n\n case 'number': {\n if (\n (typeof value !== 'number' && typeof value !== 'string') ||\n (typeof value === 'string' && value.trim() === '')\n ) {\n return [false, undefined, 'Expected number'];\n }\n const num = Number(value);\n if (!Number.isFinite(num)) {\n return [false, undefined, 'Expected number'];\n }\n return [true, num, null];\n }\n\n case 'boolean': {\n if (value === 'true' || value === '1' || value === true) {\n return [true, true, null];\n } else if (value === 'false' || value === '0' || value === false) {\n return [true, false, null];\n }\n return [false, undefined, 'Expected boolean (true/false)'];\n }\n\n case 'unknown':\n // Pass through any value without validation.\n return [true, value, null];\n\n default:\n return [false, undefined, `Unknown type: ${baseType}`];\n }\n}\n\n/**\n * Validates a schema field value (primitive, array, or nested object).\n * Returns [success, result, errors].\n */\nfunction validateField(\n key: string,\n field: SchemaField | SchemaDefinition,\n value: unknown,\n parentPath: string = '',\n): [boolean, unknown, Record<string, string[]>] {\n const fieldPath = parentPath ? `${parentPath}.${key}` : key;\n const errors: Record<string, string[]> = {};\n\n // Handle nested object schemas.\n if (isNestedSchema(field)) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n errors[fieldPath] = ['Expected object'];\n return [false, undefined, errors];\n }\n // Recursively validate nested schema.\n const nestedResult = validateSchema(field, value as Record<string, unknown>, fieldPath);\n return [nestedResult.success, nestedResult.data, nestedResult.errors];\n }\n\n // Extract type string from field or [type, description] tuple.\n const [typeStr] = unpackField(field);\n const isOptional = typeStr.endsWith('?');\n const isArray = typeStr.includes('[]');\n\n // Strip optional marker for base type extraction.\n let baseType = isOptional ? typeStr.slice(0, -1) : typeStr;\n\n // Strip array marker.\n if (isArray) {\n baseType = baseType.replace('[]', '');\n }\n\n // Handle missing/empty values.\n if (value === undefined || value === null || value === '') {\n if (!isOptional) {\n errors[fieldPath] = ['Required'];\n return [false, undefined, errors];\n }\n return [true, undefined, {}];\n }\n\n // Handle array types.\n if (isArray) {\n if (!Array.isArray(value)) {\n errors[fieldPath] = ['Expected array'];\n return [false, undefined, errors];\n }\n\n const resultArray: unknown[] = [];\n for (let i = 0; i < value.length; i++) {\n const [success, coerced, errorMsg] = validatePrimitive(value[i], baseType);\n if (!success) {\n errors[`${fieldPath}[${i}]`] = [errorMsg!];\n return [false, undefined, errors];\n }\n resultArray.push(coerced);\n }\n return [true, resultArray, {}];\n }\n\n // Handle primitive types.\n const [success, coerced, errorMsg] = validatePrimitive(value, baseType);\n if (!success) {\n errors[fieldPath] = [errorMsg!];\n return [false, undefined, errors];\n }\n return [true, coerced, {}];\n}\n\n/**\n * Validates a full schema against data.\n * Returns { success, data, errors }.\n */\nfunction validateSchema(\n schema: SchemaDefinition,\n data: Record<string, unknown>,\n parentPath: string = '',\n): { success: boolean; data: Record<string, unknown>; errors: Record<string, string[]> } {\n const result: Record<string, unknown> = {};\n const allErrors: Record<string, string[]> = {};\n const source = typeof data === 'object' && data !== null && !Array.isArray(data) ? data : {};\n\n for (const [key, field] of Object.entries(schema)) {\n const value = Object.hasOwn(source, key) ? source[key] : undefined;\n const [success, coerced, fieldErrors] = validateField(key, field, value, parentPath);\n\n if (!success) {\n Object.assign(allErrors, fieldErrors);\n } else if (coerced !== undefined) {\n result[key] = coerced;\n }\n }\n\n return {\n success: Object.keys(allErrors).length === 0,\n data: result,\n errors: allErrors,\n };\n}\n\n/** Validates schema definition at compile time. */\nfunction validateSchemaDefinition(schema: SchemaDefinition, path: string = ''): void {\n for (const [key, field] of Object.entries(schema)) {\n const fieldPath = path ? `${path}.${key}` : key;\n\n if (isNestedSchema(field)) {\n validateSchemaDefinition(field, fieldPath);\n } else {\n if (\n Array.isArray(field) &&\n (field.length !== 2 || typeof field[0] !== 'string' || typeof field[1] !== 'string')\n ) {\n throw new Error(`Invalid field definition at '${fieldPath}'`);\n }\n const [typeStr] = unpackField(field);\n if (typeof typeStr !== 'string') {\n throw new Error(`Unknown type '${String(typeStr)}' at '${fieldPath}'`);\n }\n const isOptional = typeStr.endsWith('?');\n let baseType = isOptional ? typeStr.slice(0, -1) : typeStr;\n baseType = baseType.replace('[]', '');\n\n if (!PRIMITIVE_TYPES.includes(baseType)) {\n throw new Error(`Unknown type '${typeStr}' at '${fieldPath}'`);\n }\n }\n }\n}\n\n/**\n * Compiles a schema definition into a validator.\n *\n * Used internally by the router to validate query parameters and request bodies.\n * Provides a Zod-compatible `safeParse` API for consistency.\n *\n * @param schema - The schema definition object\n * @returns A compiled schema with `safeParse` method\n *\n * @example\n * ```typescript\n * const userSchema = compileSchema({\n * name: 'string',\n * age: 'number',\n * email: 'string?',\n * });\n *\n * const result = userSchema.safeParse({ name: 'Alice', age: 30 });\n * if (result.success) {\n * console.log(result.data.name); // 'Alice'\n * } else {\n * console.log(result.error.flatten().fieldErrors);\n * }\n * ```\n */\nexport function compileSchema<const T extends SchemaDefinition>(\n schema: T,\n): CompiledSchema<InferSchema<T>> {\n // Validate schema structure at compile time.\n validateSchemaDefinition(schema);\n\n return {\n safeParse(data: object): ValidationResult<InferSchema<T>> {\n const {\n success,\n data: result,\n errors,\n } = validateSchema(schema, data as Record<string, unknown>);\n\n if (!success) {\n return {\n success: false,\n error: { flatten: () => ({ fieldErrors: errors }) },\n };\n }\n\n return { success: true, data: result as InferSchema<T> };\n },\n };\n}\n\n/** JSON Schema object produced by {@link toJsonSchema}. */\nexport interface JsonSchemaObject {\n type: 'object';\n properties: Record<string, unknown>;\n /** Omitted when empty, as OpenAPI 3.0 requires. */\n required?: string[];\n}\n\n/**\n * Converts a schema definition to JSON Schema, recursing into nested objects.\n */\nexport function toJsonSchema(schema: SchemaDefinition): JsonSchemaObject {\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n for (const [key, field] of Object.entries(schema)) {\n if (isNestedSchema(field)) {\n properties[key] = toJsonSchema(field);\n required.push(key);\n continue;\n }\n const [typeStr, description] = unpackField(field);\n const described = description === undefined ? {} : { description };\n const optional = typeStr.endsWith('?');\n const base = optional ? typeStr.slice(0, -1) : typeStr;\n const primitive = base.replace('[]', '');\n const item = primitive === 'unknown' ? {} : { type: primitive };\n properties[key] = base.endsWith('[]')\n ? { type: 'array', items: item, ...described }\n : { ...item, ...described };\n if (!optional) required.push(key);\n }\n return { type: 'object', properties, ...(required.length > 0 && { required }) };\n}\n","/**\n * @fileoverview Core type definitions for the routing library.\n *\n * **Path Convention:**\n * - Property names = URL path segments\n * - `$param` prefix = dynamic segment (`:param`)\n * - `get`, `post`, `put`, `patch`, `delete` = HTTP method handlers\n *\n * **Structure:**\n * - `router({ ... })` - groups paths, can have middleware\n * - `route({ query?, body?, handler })` - single method with schemas\n * - Bare functions as shorthand for handlers without schemas\n */\n\nimport type { Middleware } from './middleware.js';\nimport type { InferSchema, SchemaDefinition } from './schema.js';\n\n/** Standard HTTP methods. */\nexport type Method = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'head';\n\n/** HTTP method property names. */\nexport const HTTP_METHODS = new Set<string>([\n 'get',\n 'post',\n 'put',\n 'patch',\n 'delete',\n 'options',\n 'head',\n]);\n\n/** Symbol to mark an object as a route definition. */\nexport const ROUTE_MARKER = Symbol.for('@fresho/router:route');\n\n/** Symbol to mark an object as a router. */\nexport const ROUTER_MARKER = Symbol.for('@fresho/router:router');\n\n/**\n * Brand symbol for Router type identification across module boundaries.\n * This enables TypeScript to recognize Router types even when generic\n * type information is lost during cross-package imports.\n */\ndeclare const ROUTER_BRAND: unique symbol;\n\n/** Brand type for Router identification in conditional types. */\nexport type RouterBrand = { readonly [ROUTER_BRAND]: true };\n\n/**\n * Extracts the parameter name from a `$param` property name.\n */\nexport type ExtractParamFromProperty<T extends string> = T extends `$${infer Param}`\n ? Param\n : never;\n\n/**\n * Checks if a property name is a dynamic parameter (starts with $).\n */\nexport type IsParamProperty<T extends string> = T extends `$${string}` ? true : false;\n\n/**\n * Collects path params from a property path array.\n */\nexport type CollectPathParams<Path extends string[]> = Path extends [\n infer Head extends string,\n ...infer Rest extends string[],\n]\n ? (Head extends `$${infer Param}` ? { [K in Param]: string } : {}) & CollectPathParams<Rest>\n : {};\n\n/** Execution context for background tasks (Cloudflare Workers compatible). */\nexport interface ExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n passThroughOnException(): void;\n}\n\n/**\n * Unified context object passed to handlers.\n */\nexport type Context<\n Q = unknown,\n B = unknown,\n P extends Record<string, string> = Record<string, string>,\n Ctx = {},\n> = {\n /** The original Request. */\n request: Request;\n /** URL path parameters from `$param` segments. */\n path: P;\n /** Query string parameters. */\n query: Q;\n /** Request body. */\n body: B;\n /** Execution context for background tasks. */\n executionCtx?: ExecutionContext;\n /** Environment bindings. */\n env: Ctx extends { env: infer E } ? E : unknown;\n} & Omit<Ctx, 'env'>;\n\n/**\n * Typed response wrapper that carries the response body type.\n */\nexport type TypedResponse<T> = Response & { __responseType?: T };\n\n/**\n * Handler function type.\n */\nexport type TypedHandler<\n Q = unknown,\n B = unknown,\n P extends Record<string, string> = Record<string, string>,\n R = unknown,\n Ctx = {},\n> = (\n context: Context<Q, B, P, Ctx>,\n) => R | Response | TypedResponse<R> | Promise<R | Response | TypedResponse<R>>;\n\n/** MCP tool behavior hints; defaults come from the HTTP method. */\nexport interface ToolAnnotations {\n /** Human-readable tool title. */\n title?: string;\n /** The tool does not modify its environment. */\n readOnlyHint?: boolean;\n /** The tool may perform destructive updates. */\n destructiveHint?: boolean;\n /** Repeating the call with the same arguments has no additional effect. */\n idempotentHint?: boolean;\n /** The tool interacts with external entities. */\n openWorldHint?: boolean;\n}\n\n/**\n * Route definition - a single HTTP method handler with optional schemas.\n *\n * Use `route()` when you need query or body validation schemas.\n * For simple handlers without schemas, use bare functions in the router.\n *\n * @example\n * ```typescript\n * // With schemas\n * get: route({\n * query: { limit: 'number?' },\n * handler: async (c) => ({ items: [], limit: c.query.limit }),\n * })\n *\n * // Without schemas (bare function shorthand)\n * get: async (c) => ({ items: [] })\n * ```\n */\nexport interface RouteDefinition<\n Q extends SchemaDefinition = {},\n B extends SchemaDefinition = {},\n R = unknown,\n P extends Record<string, string> = Record<string, string>,\n Ctx = {},\n> {\n /** Query parameter schema for validation. */\n query?: Q;\n /** Request body schema for validation. */\n body?: B;\n /** Optional description for documentation. */\n description?: string;\n /** Operation name, used as the OpenAPI `operationId` and the MCP tool name. */\n operationId?: string;\n /** MCP tool settings. */\n mcp?: { annotations?: ToolAnnotations };\n /** The request handler function. */\n handler: TypedHandler<InferSchema<Q>, InferSchema<B>, P, R, Ctx>;\n}\n\n/** A bare handler function (shorthand for route without schemas). */\nexport type BareHandler = (\n context: Context<unknown, unknown, Record<string, string>, unknown>,\n) => unknown;\n\n/** Checks if a value is a function. */\nexport function isFunction(value: unknown): value is BareHandler {\n return typeof value === 'function';\n}\n\n/** Checks if entry is a Route (marked with ROUTE_MARKER). */\nexport function isRoute(entry: unknown): entry is RouteDefinition {\n return typeof entry === 'object' && entry !== null && ROUTE_MARKER in entry;\n}\n\n/** Checks if entry is a Router (marked with ROUTER_MARKER). */\nexport function isRouter(entry: unknown): entry is Router<RouterRoutes> {\n return typeof entry === 'object' && entry !== null && ROUTER_MARKER in entry;\n}\n\n/**\n * A method handler - either a route with schemas or a bare function.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type MethodEntry = RouteDefinition<any, any, any, any, any> | BareHandler;\n\n/**\n * Router routes record.\n *\n * Can contain:\n * - `get`, `post`, `put`, `patch`, `delete` - method handlers (route or function)\n * - Other property names - nested routers (path segments)\n * - `$param` properties - dynamic path segments\n */\nexport type RouterRoutes = {\n get?: MethodEntry;\n post?: MethodEntry;\n put?: MethodEntry;\n patch?: MethodEntry;\n delete?: MethodEntry;\n options?: MethodEntry;\n head?: MethodEntry;\n} & {\n [key: string]: MethodEntry | Router<RouterRoutes>;\n};\n\n/** Handler function signature for the standalone router. */\nexport type FetchHandler = (\n request: Request,\n env?: unknown,\n ctx?: ExecutionContext,\n) => Response | Promise<Response>;\n\n/**\n * Router - groups paths and can have middleware.\n *\n * @example\n * ```typescript\n * const api = router({\n * // Method handlers for this path\n * get: async (c) => [...],\n * post: route({ body: { name: 'string' }, handler: async (c) => c.body }),\n *\n * // Nested path segments\n * users: router({\n * get: async (c) => [...],\n * $id: router({\n * get: async (c) => ({ id: c.path.id }),\n * }),\n * }),\n * });\n * ```\n */\nexport interface Router<T extends RouterRoutes> extends RouterBrand {\n /** Route definitions and nested routers. */\n readonly routes: T;\n /** Middleware applied to all routes in this router. */\n readonly middleware?: Middleware[];\n /**\n * Creates a fetch handler for use with Cloudflare Workers, Deno, Bun, etc.\n */\n handler(): FetchHandler;\n}\n","/**\n * @fileoverview HTTP request handler for @fresho/router.\n *\n * Builds paths from property names:\n * - Regular properties = path segments\n * - `$param` properties = dynamic segments (`:param`)\n * - `get`, `post`, etc. = method handlers\n */\n\nimport type { Middleware, MiddlewareContext } from './middleware.js';\nimport { runMiddleware } from './middleware.js';\nimport type { CompiledSchema } from './schema.js';\nimport { compileSchema } from './schema.js';\nimport type {\n ExecutionContext,\n FetchHandler,\n RouteDefinition,\n Router,\n RouterRoutes,\n} from './types.js';\nimport { HTTP_METHODS, isFunction, isRoute, isRouter } from './types.js';\n\n/** Compiled route with pattern matcher and handler. */\ninterface CompiledRoute {\n method: string;\n pattern: RegExp;\n paramNames: string[];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (context: any) => unknown;\n middleware: Middleware[];\n querySchema?: CompiledSchema<unknown>;\n bodySchema?: CompiledSchema<unknown>;\n}\n\n/**\n * Escapes special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Converts a path with :param segments to a regex pattern.\n */\nfunction pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {\n const paramNames: string[] = [];\n const parts = path.split(/:(\\w+)/g);\n let regexStr = '';\n\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n regexStr += escapeRegex(parts[i]);\n } else {\n paramNames.push(parts[i]);\n regexStr += '([^/]+)';\n }\n }\n\n return {\n pattern: new RegExp(`^${regexStr}$`),\n paramNames,\n };\n}\n\n/**\n * Converts a property name to a path segment.\n * - `$param` becomes `:param`\n * - Other names become literal segments\n */\nfunction propertyToSegment(prop: string): string {\n if (prop.startsWith('$')) {\n return `:${prop.slice(1)}`;\n }\n return prop;\n}\n\n/** Collects and compiles all routes from a router tree. */\nfunction compileRoutes(\n routerDef: Router<RouterRoutes>,\n parentPath = '',\n parentMiddleware: Middleware[] = [],\n): CompiledRoute[] {\n const currentMiddleware = [...parentMiddleware, ...(routerDef.middleware || [])];\n const compiled: CompiledRoute[] = [];\n\n for (const [prop, entry] of Object.entries(routerDef.routes)) {\n // Check if entry is a nested router first (takes precedence over method name check).\n // This allows path segments named 'get', 'post', etc. when they contain routers.\n if (isRouter(entry)) {\n // Nested router - recurse with updated path.\n const segment = propertyToSegment(prop);\n const newPath = parentPath ? `${parentPath}/${segment}` : `/${segment}`;\n compiled.push(...compileRoutes(entry, newPath, currentMiddleware));\n } else if (HTTP_METHODS.has(prop)) {\n // Method handler (get, post, etc.) - only when entry is a route or function.\n const method = prop.toUpperCase();\n const path = parentPath || '/';\n const { pattern, paramNames } = pathToRegex(path);\n\n if (isFunction(entry)) {\n // Bare function handler.\n compiled.push({\n method,\n pattern,\n paramNames,\n handler: entry,\n middleware: currentMiddleware,\n });\n } else if (isRoute(entry)) {\n // Route with schemas.\n const routeDef = entry as RouteDefinition;\n compiled.push({\n method,\n pattern,\n paramNames,\n handler: routeDef.handler,\n middleware: currentMiddleware,\n querySchema: routeDef.query ? compileSchema(routeDef.query) : undefined,\n bodySchema: routeDef.body ? compileSchema(routeDef.body) : undefined,\n });\n }\n }\n }\n\n return compiled;\n}\n\n/**\n * Strips the body from a response for HEAD requests.\n */\nfunction stripBodyForHead(response: Response, isHead: boolean): Response {\n if (!isHead) return response;\n return new Response(null, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n}\n\n/**\n * Creates a fetch handler from a router definition.\n *\n * @example\n * ```typescript\n * const api = router({\n * health: router({\n * get: async () => ({ status: 'ok' }),\n * }),\n * users: router({\n * get: async () => [...],\n * $id: router({\n * get: async (c) => ({ id: c.path.id }),\n * }),\n * }),\n * });\n *\n * export default { fetch: api.handler() };\n * ```\n */\nexport function createHandler<T extends RouterRoutes>(routerDef: Router<T>): FetchHandler {\n const compiledRoutes = compileRoutes(routerDef);\n\n return async (request: Request, env?: unknown, ctx?: ExecutionContext): Promise<Response> => {\n const url = new URL(request.url);\n const method = request.method.toUpperCase();\n const isHead = method === 'HEAD';\n\n // Find matching route.\n for (const compiled of compiledRoutes) {\n const methodMatches =\n compiled.method === method ||\n method === 'OPTIONS' ||\n (method === 'HEAD' && compiled.method === 'GET');\n if (!methodMatches) continue;\n\n const match = compiled.pattern.exec(url.pathname);\n if (!match) continue;\n\n // Extract path parameters.\n const path: Record<string, string> = {};\n for (let i = 0; i < compiled.paramNames.length; i++) {\n path[compiled.paramNames[i]] = decodeURIComponent(match[i + 1]);\n }\n\n // Create middleware context.\n const context: MiddlewareContext = {\n request,\n path,\n query: {},\n body: {},\n env,\n executionCtx: ctx,\n };\n\n // Run middleware and handler.\n if (\n compiled.middleware.length > 0 ||\n compiled.handler ||\n compiled.querySchema ||\n compiled.bodySchema\n ) {\n const response = await runMiddleware(compiled.middleware, context, async () => {\n const routeId = `${compiled.method} ${url.pathname}`;\n\n // Validate query params.\n let query: unknown = {};\n if (compiled.querySchema) {\n const raw = Object.fromEntries(url.searchParams);\n const result = compiled.querySchema.safeParse(raw);\n if (!result.success) {\n return Response.json(\n {\n error: 'Invalid query parameters',\n route: routeId,\n details: result.error.flatten(),\n },\n { status: 400 },\n );\n }\n query = result.data;\n }\n\n // Validate body.\n let body: unknown = {};\n if (compiled.bodySchema && ['POST', 'PUT', 'PATCH'].includes(method)) {\n const raw = await request.json().catch(() => ({}));\n const result = compiled.bodySchema.safeParse(raw);\n if (!result.success) {\n return Response.json(\n { error: 'Invalid request body', route: routeId, details: result.error.flatten() },\n { status: 400 },\n );\n }\n body = result.data;\n }\n\n context.query = query;\n context.body = body;\n\n if (compiled.handler) {\n // Build handler context with middleware extensions.\n const handlerContext = {\n request,\n path,\n query,\n body,\n env,\n executionCtx: ctx,\n };\n\n // Copy middleware extensions.\n for (const key of Object.keys(context)) {\n if (!['request', 'path', 'query', 'body', 'env', 'executionCtx'].includes(key)) {\n (handlerContext as Record<string, unknown>)[key] = (\n context as Record<string, unknown>\n )[key];\n }\n }\n\n const result = await compiled.handler(handlerContext);\n if (result instanceof Response) {\n return result;\n }\n return Response.json(result);\n }\n\n return Response.json({});\n });\n return stripBodyForHead(response, isHead);\n }\n\n return stripBodyForHead(Response.json({}), isHead);\n }\n\n return Response.json({ error: 'Not Found' }, { status: 404 });\n };\n}\n","/**\n * @fileoverview Core route and router creation functions.\n *\n * ## Path Convention\n * - Property names = URL path segments\n * - `$param` prefix = dynamic segment (`:param`)\n * - `get`, `post`, `put`, `patch`, `delete` = HTTP method handlers\n *\n * ## Typing Best Practices\n *\n * **Schemas** (`query`/`body`) provide runtime validation AND type inference.\n * Use them for any input that needs validation:\n * ```typescript\n * get: route({\n * query: { limit: 'number?' },\n * handler: async (c) => c.query.limit, // limit: number | undefined\n * })\n * ```\n *\n * **Context** (`route.ctx<T>()`) provides types only (no validation).\n * Use it for path params, env bindings, and middleware-injected values:\n * ```typescript\n * interface MyContext {\n * path: { id: string };\n * env: { DB: Database };\n * user: { name: string }; // from auth middleware\n * }\n * get: route.ctx<MyContext>()({\n * handler: async (c) => ({ id: c.path.id, user: c.user.name }),\n * })\n * ```\n *\n * **Important:** Don't add explicit type annotations to handler parameters.\n * Let types flow from schemas and context:\n * ```typescript\n * // GOOD: types inferred from schema\n * handler: async (c) => c.query.limit\n *\n * // BAD: redundant type annotation\n * handler: async (c: { query: { limit?: number } }) => c.query.limit\n * ```\n */\n\nimport { createHandler } from './handler.js';\nimport type { Middleware } from './middleware.js';\nimport type { SchemaDefinition } from './schema.js';\nimport type { RouteDefinition, Router, RouterRoutes } from './types.js';\nimport { ROUTE_MARKER, ROUTER_MARKER } from './types.js';\n\n/**\n * Creates a route with query/body validation schemas.\n *\n * Use `route()` when you need request validation. For simple handlers\n * without validation, you can use bare functions directly in the router.\n *\n * @example\n * ```typescript\n * // Route with query validation\n * get: route({\n * query: { limit: 'number?', offset: 'number?' },\n * handler: async (c) => {\n * const items = await db.list(c.query.limit, c.query.offset);\n * return { items };\n * },\n * })\n *\n * // Route with body validation\n * post: route({\n * body: { name: 'string', email: 'string' },\n * handler: async (c) => {\n * const user = await db.create(c.body);\n * return user;\n * },\n * })\n *\n * // Simple handler without validation (bare function)\n * get: async () => ({ status: 'ok' })\n * ```\n */\nexport function route<\n const Q extends SchemaDefinition = {},\n const B extends SchemaDefinition = {},\n R = unknown,\n>(definition: RouteDefinition<Q, B, R, {}, {}>): RouteDefinition<Q, B, R, {}, {}> {\n return { ...definition, [ROUTE_MARKER]: true } as RouteDefinition<Q, B, R, {}, {}>;\n}\n\n/** Return type of route.ctx<T>() - callable and chainable. */\ninterface CtxBuilder<Ctx> {\n <const Q extends SchemaDefinition = {}, const B extends SchemaDefinition = {}, R = unknown>(\n definition: RouteDefinition<Q, B, R, {}, Ctx>,\n ): RouteDefinition<Q, B, R, {}, Ctx>;\n\n ctx<AdditionalCtx>(): CtxBuilder<Ctx & AdditionalCtx>;\n}\n\n/** Creates a chainable context builder. */\nfunction createCtxBuilder<Ctx>(): CtxBuilder<Ctx> {\n const builder = <\n const Q extends SchemaDefinition = {},\n const B extends SchemaDefinition = {},\n R = unknown,\n >(\n definition: RouteDefinition<Q, B, R, {}, Ctx>,\n ): RouteDefinition<Q, B, R, {}, Ctx> =>\n ({ ...definition, [ROUTE_MARKER]: true }) as RouteDefinition<Q, B, R, {}, Ctx>;\n\n builder.ctx = <AdditionalCtx>() => createCtxBuilder<Ctx & AdditionalCtx>();\n\n return builder as CtxBuilder<Ctx>;\n}\n\n/**\n * Creates a route with typed context for values not covered by schemas.\n *\n * Use `route.ctx<T>()` to type:\n * - **Path params**: `{ path: { id: string } }` for `$id` segments\n * - **Env bindings**: `{ env: { DB: Database } }` for runtime environment\n * - **Middleware values**: `{ user: User }` for auth middleware, etc.\n *\n * Context provides types only, not runtime validation. For validated inputs,\n * use `query` and `body` schemas instead.\n *\n * @example\n * ```typescript\n * interface AppContext {\n * path: { id: string };\n * env: { DB: D1Database };\n * user: { id: string }; // injected by auth middleware\n * }\n *\n * $id: router({\n * get: route.ctx<AppContext>()({\n * query: { include: 'string?' }, // schema for validation\n * handler: async (c) => {\n * // c.path.id, c.env.DB, c.user.id - from context\n * // c.query.include - from schema\n * return c.env.DB.get(c.path.id);\n * },\n * }),\n * })\n * ```\n */\nroute.ctx = <Ctx>(): CtxBuilder<Ctx> => createCtxBuilder<Ctx>();\n\n/**\n * Creates a router that groups paths and applies middleware.\n *\n * @example\n * ```typescript\n * const api = router({\n * health: router({\n * get: async () => ({ status: 'ok' }),\n * }),\n *\n * users: router({\n * // GET /users - list users\n * get: route({\n * query: { limit: 'number?' },\n * handler: async (c) => db.users.list(c.query.limit),\n * }),\n *\n * // POST /users - create user\n * post: route({\n * body: { name: 'string', email: 'string' },\n * handler: async (c) => db.users.create(c.body),\n * }),\n *\n * // /users/:id\n * $id: router({\n * get: async (c) => db.users.get(c.path.id),\n * delete: async (c) => db.users.delete(c.path.id),\n * }),\n * }),\n * });\n *\n * export default { fetch: api.handler() };\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function router<T extends RouterRoutes, M extends Middleware<any>[]>(\n routes: T,\n ...middleware: M\n): Router<T> {\n // Cast needed because RouterBrand uses a type-only symbol declaration.\n const self = {\n routes,\n middleware: middleware.length > 0 ? middleware : undefined,\n handler() {\n return createHandler(self);\n },\n } as unknown as Router<T>;\n // Add marker for runtime type checking.\n (self as unknown as Record<symbol, boolean>)[ROUTER_MARKER] = true;\n return self;\n}\n","/**\n * @fileoverview OpenAPI documentation generation.\n *\n * Generates OpenAPI 3.0 documentation from router definitions.\n *\n * **Path Convention:**\n * - Property names = URL path segments\n * - `$param` prefix = dynamic segment (`{param}` in OpenAPI)\n * - `get`, `post`, `put`, `patch`, `delete` = HTTP method handlers\n */\n\nimport {\n isNestedSchema,\n type SchemaDefinition,\n type SchemaField,\n toJsonSchema,\n unpackField,\n} from './schema.js';\nimport type { RouteDefinition, Router, RouterRoutes } from './types.js';\nimport { HTTP_METHODS, isFunction, isRoute, isRouter } from './types.js';\n\n/** Collected route info for documentation. */\nexport interface CollectedRoute {\n path: string;\n method: string;\n route: RouteDefinition | null;\n}\n\n/** Converts property name to path segment, handling $param convention. */\nfunction propertyToSegment(prop: string): string {\n if (prop.startsWith('$')) {\n return `{${prop.slice(1)}}`;\n }\n return prop;\n}\n\n/** Collects all routes from a router tree, with `{param}` path segments. */\nexport function collectRoutes(routerDef: Router<RouterRoutes>, parentPath = ''): CollectedRoute[] {\n const collectedRoutes: CollectedRoute[] = [];\n\n for (const [prop, entry] of Object.entries(routerDef.routes)) {\n // Check if this is a method handler.\n if (HTTP_METHODS.has(prop)) {\n const path = parentPath || '/';\n if (isFunction(entry)) {\n collectedRoutes.push({ path, method: prop, route: null });\n } else if (isRoute(entry)) {\n collectedRoutes.push({ path, method: prop, route: entry });\n }\n } else if (isRouter(entry)) {\n // Nested router - recurse with updated path.\n const segment = propertyToSegment(prop);\n const newPath = parentPath ? `${parentPath}/${segment}` : `/${segment}`;\n collectedRoutes.push(...collectRoutes(entry, newPath));\n }\n }\n\n return collectedRoutes;\n}\n\n/** Generates OpenAPI documentation from a router. */\nexport function generateDocs(config: {\n title: string;\n version: string;\n description?: string;\n router: Router<RouterRoutes>;\n}): object {\n const paths: Record<string, Record<string, object>> = {};\n const allRoutes = collectRoutes(config.router);\n\n for (const { path, method, route } of allRoutes) {\n if (!paths[path]) {\n paths[path] = {};\n }\n\n const operation: Record<string, unknown> = {\n operationId: route?.operationId,\n description: route?.description,\n responses: {\n 200: { description: 'Successful response' },\n 400: { description: 'Validation error' },\n },\n };\n\n // Add query parameters (only primitive types supported in query strings).\n if (route?.query) {\n const querySchema = route.query as SchemaDefinition;\n operation.parameters = Object.entries(querySchema)\n .filter(([, field]) => !isNestedSchema(field))\n .map(([name, field]) => {\n const [type, description] = unpackField(field as SchemaField);\n return {\n name,\n in: 'query',\n required: !type.endsWith('?'),\n description,\n schema: { type: type.replace('?', '').replace('[]', '') },\n };\n });\n }\n\n // Add request body.\n if (route?.body && ['post', 'put', 'patch'].includes(method)) {\n operation.requestBody = {\n required: true,\n content: {\n 'application/json': {\n schema: toJsonSchema(route.body as SchemaDefinition),\n },\n },\n };\n }\n\n paths[path][method] = operation;\n }\n\n return {\n openapi: '3.0.0',\n info: {\n title: config.title,\n version: config.version,\n description: config.description,\n },\n paths,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"docs-BLjbxLe-.mjs","names":["errors: Record<string, string[]>","resultArray: unknown[]","success","coerced","errorMsg","result: Record<string, unknown>","allErrors: Record<string, string[]>","properties: Record<string, unknown>","required: string[]","paramNames: string[]","propertyToSegment","compiled: CompiledRoute[]","path: Record<string, string>","context: MiddlewareContext","query: unknown","body: unknown","collectedRoutes: CollectedRoute[]","paths: Record<string, Record<string, object>>","operation: Record<string, unknown>","route"],"sources":["../src/schema.ts","../src/types.ts","../src/handler.ts","../src/core.ts","../src/docs.ts"],"sourcesContent":["/**\n * @fileoverview Schema types and validation utilities.\n *\n * Provides shorthand schema definitions for validating query parameters and request bodies.\n * No external dependencies - uses simple string-based type definitions.\n *\n * Supported types:\n * - Primitives: `'string'`, `'number'`, `'boolean'`\n * - Optional: `'string?'`, `'number?'`, `'boolean?'`\n * - Arrays: `'string[]'`, `'number[]'`, `'boolean[]'`\n * - Optional arrays: `'string[]?'`, `'number[]?'`, `'boolean[]?'`\n * - Described fields: `['string', 'Description']`, `['number?', 'Optional count']`\n * - Nested objects: `{ field: { nested: 'string' } }`\n *\n * @example\n * ```typescript\n * // In route definitions\n * const api = router({\n * users: router({\n * post: route({\n * query: { page: 'number?', limit: ['number?', 'Max results'] },\n * body: {\n * name: ['string', 'Full name'],\n * age: 'number',\n * tags: 'string[]?',\n * address: { city: 'string', zip: 'string?' },\n * },\n * handler: async (c) => {\n * c.query.page; // number | undefined\n * c.body.name; // string\n * c.body.address.city // string\n * },\n * }),\n * }),\n * });\n * ```\n */\n\n/** Primitive type strings. */\nexport type PrimitiveType = 'string' | 'number' | 'boolean' | 'unknown';\n\n/** Optional primitive type strings. */\nexport type OptionalPrimitiveType = 'string?' | 'number?' | 'boolean?' | 'unknown?';\n\n/** Array type strings. */\nexport type ArrayType = 'string[]' | 'number[]' | 'boolean[]' | 'unknown[]';\n\n/** Optional array type strings. */\nexport type OptionalArrayType = 'string[]?' | 'number[]?' | 'boolean[]?' | 'unknown[]?';\n\n/** All supported type strings. */\nexport type SchemaType = PrimitiveType | OptionalPrimitiveType | ArrayType | OptionalArrayType;\n\n/** A schema field: a type string, or a `[type, description]` tuple. */\nexport type SchemaField = SchemaType | readonly [type: SchemaType, description: string];\n\n/** Schema definition using shorthand syntax. Supports nested objects. */\nexport interface SchemaDefinition {\n [key: string]: SchemaField | SchemaDefinition;\n}\n\n/** Maps primitive type strings to TypeScript types. */\nexport type SchemaTypeMap = {\n string: string;\n 'string?': string | undefined;\n number: number;\n 'number?': number | undefined;\n boolean: boolean;\n 'boolean?': boolean | undefined;\n unknown: unknown;\n 'unknown?': unknown | undefined;\n 'string[]': string[];\n 'string[]?': string[] | undefined;\n 'number[]': number[];\n 'number[]?': number[] | undefined;\n 'boolean[]': boolean[];\n 'boolean[]?': boolean[] | undefined;\n 'unknown[]': unknown[];\n 'unknown[]?': unknown[] | undefined;\n};\n\n/** Checks if a schema field type is optional (ends with ?). */\ntype IsOptionalField<T> = T extends `${string}?`\n ? true\n : T extends readonly [`${string}?`, string]\n ? true\n : false;\n\n/** Infers TypeScript type from a schema field (type string, tuple, or nested object). */\nexport type InferSchemaField<T> = T extends SchemaType\n ? SchemaTypeMap[T]\n : T extends readonly [infer S extends SchemaType, string]\n ? SchemaTypeMap[S]\n : T extends SchemaDefinition\n ? InferSchema<T>\n : never;\n\n/** Gets keys of required fields (non-optional types). */\ntype RequiredKeys<T extends SchemaDefinition> = {\n [K in keyof T]: IsOptionalField<T[K]> extends true ? never : K;\n}[keyof T];\n\n/** Gets keys of optional fields (types ending with ?). */\ntype OptionalKeys<T extends SchemaDefinition> = {\n [K in keyof T]: IsOptionalField<T[K]> extends true ? K : never;\n}[keyof T];\n\n/**\n * Infers TypeScript type from schema definition.\n *\n * Required fields become required properties, optional fields (ending with ?)\n * become optional properties with `| undefined`.\n */\nexport type InferSchema<T extends SchemaDefinition> = {\n [K in RequiredKeys<T>]: InferSchemaField<T[K]>;\n} & {\n [K in OptionalKeys<T>]?: InferSchemaField<T[K]>;\n};\n\n/** Error object returned when validation fails. */\nexport interface ValidationError {\n flatten: () => { fieldErrors: Record<string, string[]> };\n}\n\n/** Validation result matching Zod's safeParse API for compatibility. */\nexport type ValidationResult<T> =\n | { success: true; data: T; error?: undefined }\n | { success: false; data?: undefined; error: ValidationError };\n\n/** Compiled schema with validate method. */\nexport interface CompiledSchema<T> {\n safeParse: (data: object) => ValidationResult<T>;\n}\n\n/** Valid base types for primitives and arrays. */\nconst PRIMITIVE_TYPES = ['string', 'number', 'boolean', 'unknown'];\n\n/** Checks if a schema field is a nested object (not a type string or [type, description] tuple). */\nexport function isNestedSchema(field: SchemaField | SchemaDefinition): field is SchemaDefinition {\n return typeof field === 'object' && field !== null && !Array.isArray(field);\n}\n\n/** Unpacks a schema field into its type string and optional description. */\nexport function unpackField(field: SchemaField): [type: SchemaType, description?: string] {\n return Array.isArray(field) ? [field[0], field[1]] : [field as SchemaType, undefined];\n}\n\n/**\n * Validates and coerces a single primitive value.\n * Returns [success, result, errorMessage].\n */\nfunction validatePrimitive(value: unknown, baseType: string): [boolean, unknown, string | null] {\n switch (baseType) {\n case 'string':\n if (typeof value !== 'string') {\n return [false, undefined, 'Expected string'];\n }\n return [true, value, null];\n\n case 'number': {\n if (\n (typeof value !== 'number' && typeof value !== 'string') ||\n (typeof value === 'string' && value.trim() === '')\n ) {\n return [false, undefined, 'Expected number'];\n }\n const num = Number(value);\n if (!Number.isFinite(num)) {\n return [false, undefined, 'Expected number'];\n }\n return [true, num, null];\n }\n\n case 'boolean': {\n if (value === 'true' || value === '1' || value === true) {\n return [true, true, null];\n } else if (value === 'false' || value === '0' || value === false) {\n return [true, false, null];\n }\n return [false, undefined, 'Expected boolean (true/false)'];\n }\n\n case 'unknown':\n // Pass through any value without validation.\n return [true, value, null];\n\n default:\n return [false, undefined, `Unknown type: ${baseType}`];\n }\n}\n\n/**\n * Validates a schema field value (primitive, array, or nested object).\n * Returns [success, result, errors].\n */\nfunction validateField(\n key: string,\n field: SchemaField | SchemaDefinition,\n value: unknown,\n parentPath: string = '',\n): [boolean, unknown, Record<string, string[]>] {\n const fieldPath = parentPath ? `${parentPath}.${key}` : key;\n const errors: Record<string, string[]> = {};\n\n // Handle nested object schemas.\n if (isNestedSchema(field)) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n errors[fieldPath] = ['Expected object'];\n return [false, undefined, errors];\n }\n // Recursively validate nested schema.\n const nestedResult = validateSchema(field, value as Record<string, unknown>, fieldPath);\n return [nestedResult.success, nestedResult.data, nestedResult.errors];\n }\n\n // Extract type string from field or [type, description] tuple.\n const [typeStr] = unpackField(field);\n const isOptional = typeStr.endsWith('?');\n const isArray = typeStr.includes('[]');\n\n // Strip optional marker for base type extraction.\n let baseType = isOptional ? typeStr.slice(0, -1) : typeStr;\n\n // Strip array marker.\n if (isArray) {\n baseType = baseType.replace('[]', '');\n }\n\n // Handle missing/empty values.\n if (value === undefined || value === null || value === '') {\n if (!isOptional) {\n errors[fieldPath] = ['Required'];\n return [false, undefined, errors];\n }\n return [true, undefined, {}];\n }\n\n // Handle array types.\n if (isArray) {\n if (!Array.isArray(value)) {\n errors[fieldPath] = ['Expected array'];\n return [false, undefined, errors];\n }\n\n const resultArray: unknown[] = [];\n for (let i = 0; i < value.length; i++) {\n const [success, coerced, errorMsg] = validatePrimitive(value[i], baseType);\n if (!success) {\n errors[`${fieldPath}[${i}]`] = [errorMsg!];\n return [false, undefined, errors];\n }\n resultArray.push(coerced);\n }\n return [true, resultArray, {}];\n }\n\n // Handle primitive types.\n const [success, coerced, errorMsg] = validatePrimitive(value, baseType);\n if (!success) {\n errors[fieldPath] = [errorMsg!];\n return [false, undefined, errors];\n }\n return [true, coerced, {}];\n}\n\n/**\n * Validates a full schema against data.\n * Returns { success, data, errors }.\n */\nfunction validateSchema(\n schema: SchemaDefinition,\n data: Record<string, unknown>,\n parentPath: string = '',\n): { success: boolean; data: Record<string, unknown>; errors: Record<string, string[]> } {\n const result: Record<string, unknown> = {};\n const allErrors: Record<string, string[]> = {};\n const source = typeof data === 'object' && data !== null && !Array.isArray(data) ? data : {};\n\n for (const [key, field] of Object.entries(schema)) {\n const value = Object.hasOwn(source, key) ? source[key] : undefined;\n const [success, coerced, fieldErrors] = validateField(key, field, value, parentPath);\n\n if (!success) {\n Object.assign(allErrors, fieldErrors);\n } else if (coerced !== undefined) {\n result[key] = coerced;\n }\n }\n\n return {\n success: Object.keys(allErrors).length === 0,\n data: result,\n errors: allErrors,\n };\n}\n\n/** Validates schema definition at compile time. */\nfunction validateSchemaDefinition(schema: SchemaDefinition, path: string = ''): void {\n for (const [key, field] of Object.entries(schema)) {\n const fieldPath = path ? `${path}.${key}` : key;\n\n if (isNestedSchema(field)) {\n validateSchemaDefinition(field, fieldPath);\n } else {\n if (\n Array.isArray(field) &&\n (field.length !== 2 || typeof field[0] !== 'string' || typeof field[1] !== 'string')\n ) {\n throw new Error(`Invalid field definition at '${fieldPath}'`);\n }\n const [typeStr] = unpackField(field);\n if (typeof typeStr !== 'string') {\n throw new Error(`Unknown type '${String(typeStr)}' at '${fieldPath}'`);\n }\n const isOptional = typeStr.endsWith('?');\n let baseType = isOptional ? typeStr.slice(0, -1) : typeStr;\n baseType = baseType.replace('[]', '');\n\n if (!PRIMITIVE_TYPES.includes(baseType)) {\n throw new Error(`Unknown type '${typeStr}' at '${fieldPath}'`);\n }\n }\n }\n}\n\n/**\n * Compiles a schema definition into a validator.\n *\n * Used internally by the router to validate query parameters and request bodies.\n * Provides a Zod-compatible `safeParse` API for consistency.\n *\n * @param schema - The schema definition object\n * @returns A compiled schema with `safeParse` method\n *\n * @example\n * ```typescript\n * const userSchema = compileSchema({\n * name: 'string',\n * age: 'number',\n * email: 'string?',\n * });\n *\n * const result = userSchema.safeParse({ name: 'Alice', age: 30 });\n * if (result.success) {\n * console.log(result.data.name); // 'Alice'\n * } else {\n * console.log(result.error.flatten().fieldErrors);\n * }\n * ```\n */\nexport function compileSchema<const T extends SchemaDefinition>(\n schema: T,\n): CompiledSchema<InferSchema<T>> {\n // Validate schema structure at compile time.\n validateSchemaDefinition(schema);\n\n return {\n safeParse(data: object): ValidationResult<InferSchema<T>> {\n const {\n success,\n data: result,\n errors,\n } = validateSchema(schema, data as Record<string, unknown>);\n\n if (!success) {\n return {\n success: false,\n error: { flatten: () => ({ fieldErrors: errors }) },\n };\n }\n\n return { success: true, data: result as InferSchema<T> };\n },\n };\n}\n\n/** JSON Schema object produced by {@link toJsonSchema}. */\nexport interface JsonSchemaObject {\n type: 'object';\n properties: Record<string, unknown>;\n /** Omitted when empty, as OpenAPI 3.0 requires. */\n required?: string[];\n}\n\n/**\n * Converts a schema definition to JSON Schema, recursing into nested objects.\n */\nexport function toJsonSchema(schema: SchemaDefinition): JsonSchemaObject {\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n for (const [key, field] of Object.entries(schema)) {\n if (isNestedSchema(field)) {\n properties[key] = toJsonSchema(field);\n required.push(key);\n continue;\n }\n const [typeStr, description] = unpackField(field);\n const described = description === undefined ? {} : { description };\n const optional = typeStr.endsWith('?');\n const base = optional ? typeStr.slice(0, -1) : typeStr;\n const primitive = base.replace('[]', '');\n const item = primitive === 'unknown' ? {} : { type: primitive };\n properties[key] = base.endsWith('[]')\n ? { type: 'array', items: item, ...described }\n : { ...item, ...described };\n if (!optional) required.push(key);\n }\n return { type: 'object', properties, ...(required.length > 0 && { required }) };\n}\n","/**\n * @fileoverview Core type definitions for the routing library.\n *\n * **Path Convention:**\n * - Property names = URL path segments\n * - `$param` prefix = dynamic segment (`:param`)\n * - `get`, `post`, `put`, `patch`, `delete` = HTTP method handlers\n *\n * **Structure:**\n * - `router({ ... })` - groups paths, can have middleware\n * - `route({ query?, body?, handler })` - single method with schemas\n * - Bare functions as shorthand for handlers without schemas\n */\n\nimport type { Middleware } from './middleware.js';\nimport type { InferSchema, SchemaDefinition } from './schema.js';\n\n/** Standard HTTP methods. */\nexport type Method = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'head';\n\n/** HTTP method property names. */\nexport const HTTP_METHODS = new Set<string>([\n 'get',\n 'post',\n 'put',\n 'patch',\n 'delete',\n 'options',\n 'head',\n]);\n\n/** Symbol to mark an object as a route definition. */\nexport const ROUTE_MARKER = Symbol.for('@fresho/router:route');\n\n/** Symbol to mark an object as a router. */\nexport const ROUTER_MARKER = Symbol.for('@fresho/router:router');\n\n/**\n * Brand symbol for Router type identification across module boundaries.\n * This enables TypeScript to recognize Router types even when generic\n * type information is lost during cross-package imports.\n */\ndeclare const ROUTER_BRAND: unique symbol;\n\n/** Brand type for Router identification in conditional types. */\nexport type RouterBrand = { readonly [ROUTER_BRAND]: true };\n\n/**\n * Extracts the parameter name from a `$param` property name.\n */\nexport type ExtractParamFromProperty<T extends string> = T extends `$${infer Param}`\n ? Param\n : never;\n\n/**\n * Checks if a property name is a dynamic parameter (starts with $).\n */\nexport type IsParamProperty<T extends string> = T extends `$${string}` ? true : false;\n\n/**\n * Collects path params from a property path array.\n */\nexport type CollectPathParams<Path extends string[]> = Path extends [\n infer Head extends string,\n ...infer Rest extends string[],\n]\n ? (Head extends `$${infer Param}` ? { [K in Param]: string } : {}) & CollectPathParams<Rest>\n : {};\n\n/** Execution context for background tasks (Cloudflare Workers compatible). */\nexport interface ExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n passThroughOnException(): void;\n}\n\n/**\n * Unified context object passed to handlers.\n */\nexport type Context<\n Q = unknown,\n B = unknown,\n P extends Record<string, string> = Record<string, string>,\n Ctx = {},\n> = {\n /** The original Request. */\n request: Request;\n /** URL path parameters from `$param` segments. */\n path: P;\n /** Query string parameters. */\n query: Q;\n /** Request body. */\n body: B;\n /** Execution context for background tasks. */\n executionCtx?: ExecutionContext;\n /** Environment bindings. */\n env: Ctx extends { env: infer E } ? E : unknown;\n} & Omit<Ctx, 'env'>;\n\n/**\n * Typed response wrapper that carries the response body type.\n */\nexport type TypedResponse<T> = Response & { __responseType?: T };\n\n/**\n * Handler function type.\n */\nexport type TypedHandler<\n Q = unknown,\n B = unknown,\n P extends Record<string, string> = Record<string, string>,\n R = unknown,\n Ctx = {},\n> = (\n context: Context<Q, B, P, Ctx>,\n) => R | Response | TypedResponse<R> | Promise<R | Response | TypedResponse<R>>;\n\n/** MCP tool behavior hints; defaults come from the HTTP method. */\nexport interface ToolAnnotations {\n /** Human-readable tool title. */\n title?: string;\n /** The tool does not modify its environment. */\n readOnlyHint?: boolean;\n /** The tool may perform destructive updates. */\n destructiveHint?: boolean;\n /** Repeating the call with the same arguments has no additional effect. */\n idempotentHint?: boolean;\n /** The tool interacts with external entities. */\n openWorldHint?: boolean;\n}\n\n/**\n * Route definition - a single HTTP method handler with optional schemas.\n *\n * Use `route()` when you need query or body validation schemas.\n * For simple handlers without schemas, use bare functions in the router.\n *\n * @example\n * ```typescript\n * // With schemas\n * get: route({\n * query: { limit: 'number?' },\n * handler: async (c) => ({ items: [], limit: c.query.limit }),\n * })\n *\n * // Without schemas (bare function shorthand)\n * get: async (c) => ({ items: [] })\n * ```\n */\nexport interface RouteDefinition<\n Q extends SchemaDefinition = {},\n B extends SchemaDefinition = {},\n R = unknown,\n P extends Record<string, string> = Record<string, string>,\n Ctx = {},\n> {\n /** Query parameter schema for validation. */\n query?: Q;\n /** Request body schema for validation. */\n body?: B;\n /** Optional description for documentation. */\n description?: string;\n /** Operation name, used as the OpenAPI `operationId` and the MCP tool name. */\n operationId?: string;\n /** MCP tool settings. */\n mcp?: { annotations?: ToolAnnotations };\n /** The request handler function. */\n handler: TypedHandler<InferSchema<Q>, InferSchema<B>, P, R, Ctx>;\n}\n\n/** A bare handler function (shorthand for route without schemas). */\nexport type BareHandler = (\n context: Context<unknown, unknown, Record<string, string>, unknown>,\n) => unknown;\n\n/** Checks if a value is a function. */\nexport function isFunction(value: unknown): value is BareHandler {\n return typeof value === 'function';\n}\n\n/** Checks if entry is a Route (marked with ROUTE_MARKER). */\nexport function isRoute(entry: unknown): entry is RouteDefinition {\n return typeof entry === 'object' && entry !== null && ROUTE_MARKER in entry;\n}\n\n/** Checks if entry is a Router (marked with ROUTER_MARKER). */\nexport function isRouter(entry: unknown): entry is Router<RouterRoutes> {\n return typeof entry === 'object' && entry !== null && ROUTER_MARKER in entry;\n}\n\n/**\n * A method handler - either a route with schemas or a bare function.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type MethodEntry = RouteDefinition<any, any, any, any, any> | BareHandler;\n\n/**\n * Router routes record.\n *\n * Can contain:\n * - `get`, `post`, `put`, `patch`, `delete` - method handlers (route or function)\n * - Other property names - nested routers (path segments)\n * - `$param` properties - dynamic path segments\n */\nexport type RouterRoutes = {\n get?: MethodEntry;\n post?: MethodEntry;\n put?: MethodEntry;\n patch?: MethodEntry;\n delete?: MethodEntry;\n options?: MethodEntry;\n head?: MethodEntry;\n} & {\n [key: string]: MethodEntry | Router<RouterRoutes>;\n};\n\n/** Handler function signature for the standalone router. */\nexport type FetchHandler = (\n request: Request,\n env?: unknown,\n ctx?: ExecutionContext,\n) => Response | Promise<Response>;\n\n/**\n * Router - groups paths and can have middleware.\n *\n * @example\n * ```typescript\n * const api = router({\n * // Method handlers for this path\n * get: async (c) => [...],\n * post: route({ body: { name: 'string' }, handler: async (c) => c.body }),\n *\n * // Nested path segments\n * users: router({\n * get: async (c) => [...],\n * $id: router({\n * get: async (c) => ({ id: c.path.id }),\n * }),\n * }),\n * });\n * ```\n */\nexport interface Router<T extends RouterRoutes> extends RouterBrand {\n /** Route definitions and nested routers. */\n readonly routes: T;\n /** Middleware applied to all routes in this router. */\n readonly middleware?: Middleware[];\n /**\n * Creates a fetch handler for use with Cloudflare Workers, Deno, Bun, etc.\n */\n handler(): FetchHandler;\n}\n","/**\n * @fileoverview HTTP request handler for @fresho/router.\n *\n * Builds paths from property names:\n * - Regular properties = path segments\n * - `$param` properties = dynamic segments (`:param`)\n * - `get`, `post`, etc. = method handlers\n */\n\nimport type { Middleware, MiddlewareContext } from './middleware.js';\nimport { runMiddleware } from './middleware.js';\nimport type { CompiledSchema } from './schema.js';\nimport { compileSchema } from './schema.js';\nimport type {\n ExecutionContext,\n FetchHandler,\n RouteDefinition,\n Router,\n RouterRoutes,\n} from './types.js';\nimport { HTTP_METHODS, isFunction, isRoute, isRouter } from './types.js';\n\n/** Compiled route with pattern matcher and handler. */\ninterface CompiledRoute {\n method: string;\n pattern: RegExp;\n paramNames: string[];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (context: any) => unknown;\n middleware: Middleware[];\n querySchema?: CompiledSchema<unknown>;\n bodySchema?: CompiledSchema<unknown>;\n}\n\n/**\n * Escapes special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Converts a path with :param segments to a regex pattern.\n */\nfunction pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {\n const paramNames: string[] = [];\n const parts = path.split(/:(\\w+)/g);\n let regexStr = '';\n\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n regexStr += escapeRegex(parts[i]);\n } else {\n paramNames.push(parts[i]);\n regexStr += '([^/]+)';\n }\n }\n\n return {\n pattern: new RegExp(`^${regexStr}$`),\n paramNames,\n };\n}\n\n/**\n * Converts a property name to a path segment.\n * - `$param` becomes `:param`\n * - Other names become literal segments\n */\nfunction propertyToSegment(prop: string): string {\n if (prop.startsWith('$')) {\n return `:${prop.slice(1)}`;\n }\n return prop;\n}\n\n/** Collects and compiles all routes from a router tree. */\nfunction compileRoutes(\n routerDef: Router<RouterRoutes>,\n parentPath = '',\n parentMiddleware: Middleware[] = [],\n): CompiledRoute[] {\n const currentMiddleware = [...parentMiddleware, ...(routerDef.middleware || [])];\n const compiled: CompiledRoute[] = [];\n\n for (const [prop, entry] of Object.entries(routerDef.routes)) {\n // Check if entry is a nested router first (takes precedence over method name check).\n // This allows path segments named 'get', 'post', etc. when they contain routers.\n if (isRouter(entry)) {\n // Nested router - recurse with updated path.\n const segment = propertyToSegment(prop);\n const newPath = parentPath ? `${parentPath}/${segment}` : `/${segment}`;\n compiled.push(...compileRoutes(entry, newPath, currentMiddleware));\n } else if (HTTP_METHODS.has(prop)) {\n // Method handler (get, post, etc.) - only when entry is a route or function.\n const method = prop.toUpperCase();\n const path = parentPath || '/';\n const { pattern, paramNames } = pathToRegex(path);\n\n if (isFunction(entry)) {\n // Bare function handler.\n compiled.push({\n method,\n pattern,\n paramNames,\n handler: entry,\n middleware: currentMiddleware,\n });\n } else if (isRoute(entry)) {\n // Route with schemas.\n const routeDef = entry as RouteDefinition;\n compiled.push({\n method,\n pattern,\n paramNames,\n handler: routeDef.handler,\n middleware: currentMiddleware,\n querySchema: routeDef.query ? compileSchema(routeDef.query) : undefined,\n bodySchema: routeDef.body ? compileSchema(routeDef.body) : undefined,\n });\n }\n }\n }\n\n return compiled;\n}\n\n/**\n * Strips the body from a response for HEAD requests.\n */\nfunction stripBodyForHead(response: Response, isHead: boolean): Response {\n if (!isHead) return response;\n return new Response(null, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n}\n\n/**\n * Creates a fetch handler from a router definition.\n *\n * @example\n * ```typescript\n * const api = router({\n * health: router({\n * get: async () => ({ status: 'ok' }),\n * }),\n * users: router({\n * get: async () => [...],\n * $id: router({\n * get: async (c) => ({ id: c.path.id }),\n * }),\n * }),\n * });\n *\n * export default { fetch: api.handler() };\n * ```\n */\nexport function createHandler<T extends RouterRoutes>(routerDef: Router<T>): FetchHandler {\n const compiledRoutes = compileRoutes(routerDef);\n\n return async (request: Request, env?: unknown, ctx?: ExecutionContext): Promise<Response> => {\n const url = new URL(request.url);\n const method = request.method.toUpperCase();\n const isHead = method === 'HEAD';\n\n // Find matching route.\n for (const compiled of compiledRoutes) {\n const methodMatches =\n compiled.method === method ||\n method === 'OPTIONS' ||\n (method === 'HEAD' && compiled.method === 'GET');\n if (!methodMatches) continue;\n\n const match = compiled.pattern.exec(url.pathname);\n if (!match) continue;\n\n // Extract path parameters.\n const path: Record<string, string> = {};\n let invalidPath = false;\n for (let i = 0; i < compiled.paramNames.length; i++) {\n try {\n path[compiled.paramNames[i]] = decodeURIComponent(match[i + 1]);\n } catch {\n invalidPath = true;\n break;\n }\n }\n\n // Create middleware context.\n const context: MiddlewareContext = {\n request,\n path,\n query: {},\n body: {},\n env,\n executionCtx: ctx,\n };\n\n // Run middleware and handler.\n if (\n invalidPath ||\n compiled.middleware.length > 0 ||\n compiled.handler ||\n compiled.querySchema ||\n compiled.bodySchema\n ) {\n const response = await runMiddleware(compiled.middleware, context, async () => {\n const routeId = `${compiled.method} ${url.pathname}`;\n if (invalidPath) {\n return Response.json(\n { error: 'Invalid path parameters', route: routeId },\n { status: 400 },\n );\n }\n\n // Validate query params.\n let query: unknown = {};\n if (compiled.querySchema) {\n const raw = Object.fromEntries(url.searchParams);\n const result = compiled.querySchema.safeParse(raw);\n if (!result.success) {\n return Response.json(\n {\n error: 'Invalid query parameters',\n route: routeId,\n details: result.error.flatten(),\n },\n { status: 400 },\n );\n }\n query = result.data;\n }\n\n // Validate body.\n let body: unknown = {};\n if (compiled.bodySchema && ['POST', 'PUT', 'PATCH'].includes(method)) {\n const raw = await request.json().catch(() => ({}));\n const result = compiled.bodySchema.safeParse(raw);\n if (!result.success) {\n return Response.json(\n { error: 'Invalid request body', route: routeId, details: result.error.flatten() },\n { status: 400 },\n );\n }\n body = result.data;\n }\n\n context.query = query;\n context.body = body;\n\n if (compiled.handler) {\n // Build handler context with middleware extensions.\n const handlerContext = {\n request,\n path,\n query,\n body,\n env,\n executionCtx: ctx,\n };\n\n // Copy middleware extensions.\n for (const key of Object.keys(context)) {\n if (!['request', 'path', 'query', 'body', 'env', 'executionCtx'].includes(key)) {\n (handlerContext as Record<string, unknown>)[key] = (\n context as Record<string, unknown>\n )[key];\n }\n }\n\n const result = await compiled.handler(handlerContext);\n if (result instanceof Response) {\n return result;\n }\n return Response.json(result);\n }\n\n return Response.json({});\n });\n return stripBodyForHead(response, isHead);\n }\n\n return stripBodyForHead(Response.json({}), isHead);\n }\n\n return Response.json({ error: 'Not Found' }, { status: 404 });\n };\n}\n","/**\n * @fileoverview Core route and router creation functions.\n *\n * ## Path Convention\n * - Property names = URL path segments\n * - `$param` prefix = dynamic segment (`:param`)\n * - `get`, `post`, `put`, `patch`, `delete` = HTTP method handlers\n *\n * ## Typing Best Practices\n *\n * **Schemas** (`query`/`body`) provide runtime validation AND type inference.\n * Use them for any input that needs validation:\n * ```typescript\n * get: route({\n * query: { limit: 'number?' },\n * handler: async (c) => c.query.limit, // limit: number | undefined\n * })\n * ```\n *\n * **Context** (`route.ctx<T>()`) provides types only (no validation).\n * Use it for path params, env bindings, and middleware-injected values:\n * ```typescript\n * interface MyContext {\n * path: { id: string };\n * env: { DB: Database };\n * user: { name: string }; // from auth middleware\n * }\n * get: route.ctx<MyContext>()({\n * handler: async (c) => ({ id: c.path.id, user: c.user.name }),\n * })\n * ```\n *\n * **Important:** Don't add explicit type annotations to handler parameters.\n * Let types flow from schemas and context:\n * ```typescript\n * // GOOD: types inferred from schema\n * handler: async (c) => c.query.limit\n *\n * // BAD: redundant type annotation\n * handler: async (c: { query: { limit?: number } }) => c.query.limit\n * ```\n */\n\nimport { createHandler } from './handler.js';\nimport type { Middleware } from './middleware.js';\nimport type { SchemaDefinition } from './schema.js';\nimport type { RouteDefinition, Router, RouterRoutes } from './types.js';\nimport { ROUTE_MARKER, ROUTER_MARKER } from './types.js';\n\n/**\n * Creates a route with query/body validation schemas.\n *\n * Use `route()` when you need request validation. For simple handlers\n * without validation, you can use bare functions directly in the router.\n *\n * @example\n * ```typescript\n * // Route with query validation\n * get: route({\n * query: { limit: 'number?', offset: 'number?' },\n * handler: async (c) => {\n * const items = await db.list(c.query.limit, c.query.offset);\n * return { items };\n * },\n * })\n *\n * // Route with body validation\n * post: route({\n * body: { name: 'string', email: 'string' },\n * handler: async (c) => {\n * const user = await db.create(c.body);\n * return user;\n * },\n * })\n *\n * // Simple handler without validation (bare function)\n * get: async () => ({ status: 'ok' })\n * ```\n */\nexport function route<\n const Q extends SchemaDefinition = {},\n const B extends SchemaDefinition = {},\n R = unknown,\n>(definition: RouteDefinition<Q, B, R, {}, {}>): RouteDefinition<Q, B, R, {}, {}> {\n return { ...definition, [ROUTE_MARKER]: true } as RouteDefinition<Q, B, R, {}, {}>;\n}\n\n/** Return type of route.ctx<T>() - callable and chainable. */\ninterface CtxBuilder<Ctx> {\n <const Q extends SchemaDefinition = {}, const B extends SchemaDefinition = {}, R = unknown>(\n definition: RouteDefinition<Q, B, R, {}, Ctx>,\n ): RouteDefinition<Q, B, R, {}, Ctx>;\n\n ctx<AdditionalCtx>(): CtxBuilder<Ctx & AdditionalCtx>;\n}\n\n/** Creates a chainable context builder. */\nfunction createCtxBuilder<Ctx>(): CtxBuilder<Ctx> {\n const builder = <\n const Q extends SchemaDefinition = {},\n const B extends SchemaDefinition = {},\n R = unknown,\n >(\n definition: RouteDefinition<Q, B, R, {}, Ctx>,\n ): RouteDefinition<Q, B, R, {}, Ctx> =>\n ({ ...definition, [ROUTE_MARKER]: true }) as RouteDefinition<Q, B, R, {}, Ctx>;\n\n builder.ctx = <AdditionalCtx>() => createCtxBuilder<Ctx & AdditionalCtx>();\n\n return builder as CtxBuilder<Ctx>;\n}\n\n/**\n * Creates a route with typed context for values not covered by schemas.\n *\n * Use `route.ctx<T>()` to type:\n * - **Path params**: `{ path: { id: string } }` for `$id` segments\n * - **Env bindings**: `{ env: { DB: Database } }` for runtime environment\n * - **Middleware values**: `{ user: User }` for auth middleware, etc.\n *\n * Context provides types only, not runtime validation. For validated inputs,\n * use `query` and `body` schemas instead.\n *\n * @example\n * ```typescript\n * interface AppContext {\n * path: { id: string };\n * env: { DB: D1Database };\n * user: { id: string }; // injected by auth middleware\n * }\n *\n * $id: router({\n * get: route.ctx<AppContext>()({\n * query: { include: 'string?' }, // schema for validation\n * handler: async (c) => {\n * // c.path.id, c.env.DB, c.user.id - from context\n * // c.query.include - from schema\n * return c.env.DB.get(c.path.id);\n * },\n * }),\n * })\n * ```\n */\nroute.ctx = <Ctx>(): CtxBuilder<Ctx> => createCtxBuilder<Ctx>();\n\n/**\n * Creates a router that groups paths and applies middleware.\n *\n * @example\n * ```typescript\n * const api = router({\n * health: router({\n * get: async () => ({ status: 'ok' }),\n * }),\n *\n * users: router({\n * // GET /users - list users\n * get: route({\n * query: { limit: 'number?' },\n * handler: async (c) => db.users.list(c.query.limit),\n * }),\n *\n * // POST /users - create user\n * post: route({\n * body: { name: 'string', email: 'string' },\n * handler: async (c) => db.users.create(c.body),\n * }),\n *\n * // /users/:id\n * $id: router({\n * get: async (c) => db.users.get(c.path.id),\n * delete: async (c) => db.users.delete(c.path.id),\n * }),\n * }),\n * });\n *\n * export default { fetch: api.handler() };\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function router<T extends RouterRoutes, M extends Middleware<any>[]>(\n routes: T,\n ...middleware: M\n): Router<T> {\n // Cast needed because RouterBrand uses a type-only symbol declaration.\n const self = {\n routes,\n middleware: middleware.length > 0 ? middleware : undefined,\n handler() {\n return createHandler(self);\n },\n } as unknown as Router<T>;\n // Add marker for runtime type checking.\n (self as unknown as Record<symbol, boolean>)[ROUTER_MARKER] = true;\n return self;\n}\n","/**\n * @fileoverview OpenAPI documentation generation.\n *\n * Generates OpenAPI 3.0 documentation from router definitions.\n *\n * **Path Convention:**\n * - Property names = URL path segments\n * - `$param` prefix = dynamic segment (`{param}` in OpenAPI)\n * - `get`, `post`, `put`, `patch`, `delete` = HTTP method handlers\n */\n\nimport {\n isNestedSchema,\n type SchemaDefinition,\n type SchemaField,\n toJsonSchema,\n unpackField,\n} from './schema.js';\nimport type { RouteDefinition, Router, RouterRoutes } from './types.js';\nimport { HTTP_METHODS, isFunction, isRoute, isRouter } from './types.js';\n\n/** Collected route info for documentation. */\nexport interface CollectedRoute {\n path: string;\n method: string;\n route: RouteDefinition | null;\n}\n\n/** Converts property name to path segment, handling $param convention. */\nfunction propertyToSegment(prop: string): string {\n if (prop.startsWith('$')) {\n return `{${prop.slice(1)}}`;\n }\n return prop;\n}\n\n/** Collects all routes from a router tree, with `{param}` path segments. */\nexport function collectRoutes(routerDef: Router<RouterRoutes>, parentPath = ''): CollectedRoute[] {\n const collectedRoutes: CollectedRoute[] = [];\n\n for (const [prop, entry] of Object.entries(routerDef.routes)) {\n // Check if this is a method handler.\n if (HTTP_METHODS.has(prop)) {\n const path = parentPath || '/';\n if (isFunction(entry)) {\n collectedRoutes.push({ path, method: prop, route: null });\n } else if (isRoute(entry)) {\n collectedRoutes.push({ path, method: prop, route: entry });\n }\n } else if (isRouter(entry)) {\n // Nested router - recurse with updated path.\n const segment = propertyToSegment(prop);\n const newPath = parentPath ? `${parentPath}/${segment}` : `/${segment}`;\n collectedRoutes.push(...collectRoutes(entry, newPath));\n }\n }\n\n return collectedRoutes;\n}\n\n/** Generates OpenAPI documentation from a router. */\nexport function generateDocs(config: {\n title: string;\n version: string;\n description?: string;\n router: Router<RouterRoutes>;\n}): object {\n const paths: Record<string, Record<string, object>> = {};\n const allRoutes = collectRoutes(config.router);\n\n for (const { path, method, route } of allRoutes) {\n if (!paths[path]) {\n paths[path] = {};\n }\n\n const operation: Record<string, unknown> = {\n operationId: route?.operationId,\n description: route?.description,\n responses: {\n 200: { description: 'Successful response' },\n 400: { description: 'Validation error' },\n },\n };\n\n // Add query parameters (only primitive types supported in query strings).\n if (route?.query) {\n const querySchema = route.query as SchemaDefinition;\n operation.parameters = Object.entries(querySchema)\n .filter(([, field]) => !isNestedSchema(field))\n .map(([name, field]) => {\n const [type, description] = unpackField(field as SchemaField);\n return {\n name,\n in: 'query',\n required: !type.endsWith('?'),\n description,\n schema: { type: type.replace('?', '').replace('[]', '') },\n };\n });\n }\n\n // Add request body.\n if (route?.body && ['post', 'put', 'patch'].includes(method)) {\n operation.requestBody = {\n required: true,\n content: {\n 'application/json': {\n schema: toJsonSchema(route.body as SchemaDefinition),\n },\n },\n };\n }\n\n paths[path][method] = operation;\n }\n\n return {\n openapi: '3.0.0',\n info: {\n title: config.title,\n version: config.version,\n description: config.description,\n },\n paths,\n };\n}\n"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as runMiddleware } from "./middleware-B-_0PLA6.mjs";
|
|
2
|
-
import { a as isFunction, c as compileSchema, i as router, n as generateDocs, o as isRoute, r as route, s as isRouter } from "./docs-
|
|
2
|
+
import { a as isFunction, c as compileSchema, i as router, n as generateDocs, o as isRoute, r as route, s as isRouter } from "./docs-BLjbxLe-.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/client-proxy.ts
|
|
5
5
|
const PREFIXED_HTTP_METHODS = new Set([
|
|
@@ -384,8 +384,8 @@ async function invokeHandler(sharedConfig, routerDef, segments, method, options)
|
|
|
384
384
|
*/
|
|
385
385
|
function formatSSEMessage(message) {
|
|
386
386
|
const lines = [];
|
|
387
|
-
if (message.event) lines.push(`event: ${message.event}`);
|
|
388
|
-
if (message.id) lines.push(`id: ${message.id}`);
|
|
387
|
+
if (message.event) lines.push(`event: ${message.event.replace(/[\r\n]/g, "")}`);
|
|
388
|
+
if (message.id) lines.push(`id: ${message.id.replace(/[\r\n]/g, "")}`);
|
|
389
389
|
if (message.retry !== void 0) lines.push(`retry: ${message.retry}`);
|
|
390
390
|
const data = typeof message.data === "string" ? message.data : JSON.stringify(message.data);
|
|
391
391
|
for (const line of data.split("\n")) lines.push(`data: ${line}`);
|