@fresho/router 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,373 @@
1
+ //#region src/auth/basic.ts
2
+ /**
3
+ * Parses Basic authentication credentials from an Authorization header value.
4
+ *
5
+ * @param authHeader - The full Authorization header value (e.g., 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')
6
+ * @returns The parsed credentials, or null if parsing fails
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const header = request.headers.get('Authorization');
11
+ * const credentials = parseBasicAuth(header);
12
+ * if (credentials) {
13
+ * // Verify credentials
14
+ * const isValid = await verifyPassword(credentials.username, credentials.password);
15
+ * }
16
+ * ```
17
+ */
18
+ function parseBasicAuth(authHeader) {
19
+ if (!authHeader?.startsWith("Basic ")) return null;
20
+ try {
21
+ const encoded = authHeader.slice(6);
22
+ const decoded = atob(encoded);
23
+ const colonIndex = decoded.indexOf(":");
24
+ if (colonIndex === -1) return null;
25
+ return {
26
+ username: decoded.substring(0, colonIndex),
27
+ password: decoded.substring(colonIndex + 1)
28
+ };
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Encodes credentials into a Basic authentication header value.
35
+ *
36
+ * @param username - The username
37
+ * @param password - The password
38
+ * @returns The Authorization header value (e.g., 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const authHeader = encodeBasicAuth('admin', 'secret');
43
+ * fetch('/api/endpoint', {
44
+ * headers: { Authorization: authHeader }
45
+ * });
46
+ * ```
47
+ */
48
+ function encodeBasicAuth(username, password) {
49
+ return `Basic ${btoa(`${username}:${password}`)}`;
50
+ }
51
+ /**
52
+ * Extracts the raw base64-encoded credentials from an Authorization header.
53
+ *
54
+ * @param authHeader - The full Authorization header value
55
+ * @returns The base64 string, or null if not a Basic auth header
56
+ */
57
+ function extractBasicAuthToken(authHeader) {
58
+ if (!authHeader?.startsWith("Basic ")) return null;
59
+ return authHeader.slice(6);
60
+ }
61
+
62
+ //#endregion
63
+ //#region src/auth/jwt.ts
64
+ const ALGORITHMS = {
65
+ HS256: {
66
+ name: "HMAC",
67
+ hash: "SHA-256"
68
+ },
69
+ HS384: {
70
+ name: "HMAC",
71
+ hash: "SHA-384"
72
+ },
73
+ HS512: {
74
+ name: "HMAC",
75
+ hash: "SHA-512"
76
+ },
77
+ RS256: {
78
+ name: "RSASSA-PKCS1-v1_5",
79
+ hash: "SHA-256"
80
+ },
81
+ RS384: {
82
+ name: "RSASSA-PKCS1-v1_5",
83
+ hash: "SHA-384"
84
+ },
85
+ RS512: {
86
+ name: "RSASSA-PKCS1-v1_5",
87
+ hash: "SHA-512"
88
+ },
89
+ PS256: {
90
+ name: "RSA-PSS",
91
+ hash: "SHA-256",
92
+ saltLength: 32
93
+ },
94
+ PS384: {
95
+ name: "RSA-PSS",
96
+ hash: "SHA-384",
97
+ saltLength: 48
98
+ },
99
+ PS512: {
100
+ name: "RSA-PSS",
101
+ hash: "SHA-512",
102
+ saltLength: 64
103
+ },
104
+ ES256: {
105
+ name: "ECDSA",
106
+ hash: "SHA-256",
107
+ namedCurve: "P-256"
108
+ },
109
+ ES384: {
110
+ name: "ECDSA",
111
+ hash: "SHA-384",
112
+ namedCurve: "P-384"
113
+ }
114
+ };
115
+ /** Checks that a CryptoKey was created for exactly this algorithm. */
116
+ function keyMatches(key, spec) {
117
+ const alg = key.algorithm;
118
+ if (alg.name !== spec.name) return false;
119
+ if (spec.name === "ECDSA") return alg.namedCurve === spec.namedCurve;
120
+ if (spec.name !== "HMAC" && (alg.modulusLength ?? 0) < 2048) return false;
121
+ return alg.hash?.name === spec.hash;
122
+ }
123
+ /** Checks that a JWK may be used to verify this algorithm. */
124
+ function jwkMatches(jwk, alg) {
125
+ const spec = ALGORITHMS[alg];
126
+ if (jwk.alg !== void 0 && jwk.alg !== alg) return false;
127
+ if (jwk.use !== void 0 && jwk.use !== "sig") return false;
128
+ if (spec.name === "ECDSA") return jwk.kty === "EC" && jwk.crv === spec.namedCurve;
129
+ return spec.name !== "HMAC" && jwk.kty === "RSA";
130
+ }
131
+ /** Resolves the CryptoKey for a token, rejecting keys that don't fit its algorithm. */
132
+ async function resolveKey(secret, alg, kid, usage) {
133
+ const spec = ALGORITHMS[alg];
134
+ let key;
135
+ if (secret instanceof Jwks) {
136
+ if (spec.name === "HMAC") throw new Error(`Key does not match algorithm: ${alg}`);
137
+ key = await secret.getKey(alg, kid);
138
+ } else if (secret instanceof CryptoKey) key = secret;
139
+ else {
140
+ if (spec.name !== "HMAC") throw new Error(`Key does not match algorithm: ${alg}`);
141
+ const keyData = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
142
+ key = await crypto.subtle.importKey("raw", keyData, spec, false, [usage]);
143
+ }
144
+ if (!keyMatches(key, spec)) throw new Error(`Key does not match algorithm: ${alg}`);
145
+ return key;
146
+ }
147
+ /** Decodes a base64url string to bytes. */
148
+ function base64urlDecode(data) {
149
+ if (!/^[A-Za-z0-9_-]*$/.test(data)) throw new Error("Invalid JWT format");
150
+ return Uint8Array.from(atob(data.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
151
+ }
152
+ /** Decodes a base64url JSON segment, without echoing its content in errors. */
153
+ function decodeSegment(data) {
154
+ let value;
155
+ try {
156
+ value = JSON.parse(new TextDecoder().decode(base64urlDecode(data)));
157
+ } catch {}
158
+ if (typeof value !== "object" || value === null) throw new Error("Invalid JWT format");
159
+ return value;
160
+ }
161
+ /** Returns true if `actual` (a claim value) contains any of `expected`. */
162
+ function claimMatches(actual, expected) {
163
+ const values = Array.isArray(actual) ? actual : [actual];
164
+ return [expected].flat().some((e) => values.includes(e));
165
+ }
166
+ /**
167
+ * Parses a duration string (e.g., '1h', '7d', '30m') to seconds.
168
+ */
169
+ function parseDuration(duration) {
170
+ if (typeof duration === "number") return duration;
171
+ const match = duration.match(/^(\d+)\s*(s|m|h|d|w)$/i);
172
+ if (!match) throw new Error(`Invalid duration format: ${duration}. Use formats like '1h', '7d', '30m', '60s'.`);
173
+ const value = parseInt(match[1], 10);
174
+ const unit = match[2].toLowerCase();
175
+ switch (unit) {
176
+ case "s": return value;
177
+ case "m": return value * 60;
178
+ case "h": return value * 60 * 60;
179
+ case "d": return value * 60 * 60 * 24;
180
+ case "w": return value * 60 * 60 * 24 * 7;
181
+ default: throw new Error(`Unknown duration unit: ${unit}`);
182
+ }
183
+ }
184
+ /**
185
+ * Base64url encodes a string or Uint8Array.
186
+ */
187
+ function base64urlEncode(data) {
188
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
189
+ const binary = String.fromCharCode(...bytes);
190
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
191
+ }
192
+ /**
193
+ * Signs a JWT token using Web Crypto API.
194
+ *
195
+ * Uses only Web Crypto API, compatible with Cloudflare Workers, Deno, and browsers.
196
+ *
197
+ * @param payload - The JWT payload (custom claims)
198
+ * @param secret - The signing secret (string, bytes, or private/HMAC CryptoKey)
199
+ * @param options - Optional signing options (algorithm, expiresIn, issuer, etc.)
200
+ * @returns The signed JWT token string
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * // Basic usage (HS256 by default)
205
+ * const token = await jwtSign(
206
+ * { uid: 'user-123', role: 'admin' },
207
+ * 'your-secret-key',
208
+ * { expiresIn: '1h' }
209
+ * );
210
+ *
211
+ * // With HS512 algorithm
212
+ * const token = await jwtSign(
213
+ * { uid: 'user-123' },
214
+ * process.env.JWT_SECRET,
215
+ * { algorithm: 'HS512', expiresIn: '7d' }
216
+ * );
217
+ * ```
218
+ */
219
+ async function jwtSign(payload, secret, options = {}) {
220
+ const algorithm = options.algorithm || "HS256";
221
+ const spec = ALGORITHMS[algorithm];
222
+ if (!spec) throw new Error(`Unknown algorithm: ${algorithm}`);
223
+ const now = options.issuedAt ? options.issuedAt instanceof Date ? Math.floor(options.issuedAt.getTime() / 1e3) : options.issuedAt : Math.floor(Date.now() / 1e3);
224
+ const finalPayload = {
225
+ ...payload,
226
+ iat: now
227
+ };
228
+ if (options.expiresIn !== void 0) finalPayload.exp = now + parseDuration(options.expiresIn);
229
+ if (options.notBefore !== void 0) finalPayload.nbf = now + parseDuration(options.notBefore);
230
+ if (options.issuer !== void 0) finalPayload.iss = options.issuer;
231
+ if (options.audience !== void 0) finalPayload.aud = options.audience;
232
+ if (options.subject !== void 0) finalPayload.sub = options.subject;
233
+ const header = {
234
+ alg: algorithm,
235
+ typ: "JWT",
236
+ ...options.keyId !== void 0 && { kid: options.keyId }
237
+ };
238
+ const data = `${base64urlEncode(JSON.stringify(header))}.${base64urlEncode(JSON.stringify(finalPayload))}`;
239
+ if (secret instanceof Jwks) throw new Error("Cannot sign with a JWKS");
240
+ const key = await resolveKey(secret, algorithm, void 0, "sign");
241
+ const signatureBuffer = await crypto.subtle.sign(spec, key, new TextEncoder().encode(data));
242
+ return `${data}.${base64urlEncode(new Uint8Array(signatureBuffer))}`;
243
+ }
244
+ /**
245
+ * Verifies a JWT token using Web Crypto API.
246
+ *
247
+ * Uses only Web Crypto API, compatible with Cloudflare Workers, Deno, and browsers.
248
+ *
249
+ * @param token - The JWT token string to verify
250
+ * @param secret - The verification key (string, bytes, CryptoKey, or JWKS)
251
+ * @param options - Optional verification options
252
+ * @returns The decoded JWT payload
253
+ * @throws Error if the token is invalid, expired, or not yet valid
254
+ *
255
+ * @example
256
+ * ```typescript
257
+ * import { jwtVerify } from '@fresho/router/auth';
258
+ *
259
+ * try {
260
+ * const payload = await jwtVerify(token, 'your-secret-key');
261
+ * console.log(payload.sub); // user ID
262
+ * } catch (error) {
263
+ * console.error('Invalid token:', error.message);
264
+ * }
265
+ *
266
+ * // With specific algorithms
267
+ * const payload = await jwtVerify(token, secret, { algorithms: ['HS512'] });
268
+ * ```
269
+ */
270
+ async function jwtVerify(token, secret, options = {}) {
271
+ const symmetric = !(secret instanceof Jwks) && !(secret instanceof CryptoKey && secret.algorithm.name !== "HMAC");
272
+ const algorithms = options.algorithms || [symmetric ? "HS256" : "RS256"];
273
+ const parts = token.split(".");
274
+ if (parts.length !== 3) throw new Error("Invalid JWT format");
275
+ const [headerB64, payloadB64, signatureB64] = parts;
276
+ const header = decodeSegment(headerB64);
277
+ if (!algorithms.includes(header.alg) || !Object.hasOwn(ALGORITHMS, header.alg)) throw new Error(`Unsupported algorithm: ${header.alg}`);
278
+ const alg = header.alg;
279
+ if (header.kid !== void 0 && typeof header.kid !== "string") throw new Error("Invalid JWT format");
280
+ if (header.crit !== void 0) throw new Error("Unsupported critical header");
281
+ const kid = header.kid;
282
+ const key = await resolveKey(secret, alg, kid, "verify");
283
+ const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
284
+ if (!await crypto.subtle.verify(ALGORITHMS[alg], key, base64urlDecode(signatureB64), data)) throw new Error("Invalid signature");
285
+ const payload = decodeSegment(payloadB64);
286
+ const now = Date.now() / 1e3;
287
+ const tolerance = options.clockTolerance ?? 0;
288
+ if (payload.exp !== void 0 && !(typeof payload.exp === "number" && payload.exp + tolerance >= now)) throw new Error("Token expired");
289
+ if (payload.nbf !== void 0 && !(typeof payload.nbf === "number" && payload.nbf - tolerance <= now)) throw new Error("Token not yet valid");
290
+ if (options.issuer !== void 0 && (typeof payload.iss !== "string" || !claimMatches(payload.iss, options.issuer))) throw new Error("Invalid issuer");
291
+ if (options.audience !== void 0 && !claimMatches(payload.aud, options.audience)) throw new Error("Invalid audience");
292
+ return payload;
293
+ }
294
+ /** How long fetched keys are trusted before the set is reloaded. */
295
+ const JWKS_MAX_AGE_MS = 600 * 1e3;
296
+ /** Minimum time between fetch attempts, successful or not. */
297
+ const JWKS_COOLDOWN_MS = 30 * 1e3;
298
+ /** Abort a JWKS fetch that takes longer than this. */
299
+ const JWKS_TIMEOUT_MS = 5 * 1e3;
300
+ /** A remote JSON Web Key Set, fetched lazily and cached. Create with {@link createJwks}. */
301
+ var Jwks = class {
302
+ keys = [];
303
+ fetchedAt = Number.NEGATIVE_INFINITY;
304
+ attemptedAt = Number.NEGATIVE_INFINITY;
305
+ pending;
306
+ constructor(url) {
307
+ this.url = url;
308
+ }
309
+ /** Returns the verification key for `alg` and `kid`, reloading when stale or the kid is unknown. */
310
+ async getKey(alg, kid) {
311
+ if (Date.now() - this.fetchedAt > JWKS_MAX_AGE_MS) await this.refresh();
312
+ let candidates = this.match(alg, kid);
313
+ if (candidates.length === 0) {
314
+ await this.refresh();
315
+ candidates = this.match(alg, kid);
316
+ }
317
+ if (candidates.length !== 1) throw new Error("No matching key in JWKS");
318
+ return crypto.subtle.importKey("jwk", candidates[0], ALGORITHMS[alg], false, ["verify"]);
319
+ }
320
+ match(alg, kid) {
321
+ return this.keys.filter((k) => (kid === void 0 || k.kid === kid) && jwkMatches(k, alg));
322
+ }
323
+ /** Joins an in-flight load or starts one at most once per cooldown; keeps old keys on failure. */
324
+ async refresh() {
325
+ if (!this.pending) {
326
+ if (Date.now() - this.attemptedAt < JWKS_COOLDOWN_MS) return;
327
+ this.attemptedAt = Date.now();
328
+ this.pending = this.load().finally(() => {
329
+ this.pending = void 0;
330
+ });
331
+ }
332
+ try {
333
+ await this.pending;
334
+ } catch (error) {
335
+ if (this.keys.length === 0) throw error;
336
+ }
337
+ }
338
+ async load() {
339
+ const response = await fetch(this.url, {
340
+ headers: { Accept: "application/json" },
341
+ signal: AbortSignal.timeout(JWKS_TIMEOUT_MS)
342
+ });
343
+ if (!response.ok) throw new Error(`JWKS fetch failed: ${response.status}`);
344
+ const body = await response.json().catch(() => ({}));
345
+ const keys = Array.isArray(body.keys) ? body.keys.filter((k) => typeof k === "object" && k !== null) : [];
346
+ if (keys.length === 0) throw new Error("JWKS response has no keys");
347
+ this.keys = keys;
348
+ this.fetchedAt = Date.now();
349
+ }
350
+ };
351
+ /**
352
+ * Creates a cached remote key set for verifying tokens from an OAuth/OIDC provider.
353
+ *
354
+ * Create it once (e.g. at module scope) and pass it as the secret. Keys are selected by the
355
+ * token's `kid` and must match its algorithm; an unknown `kid` triggers a rate-limited reload.
356
+ *
357
+ * @example
358
+ * ```typescript
359
+ * const jwks = createJwks('https://example.auth0.com/.well-known/jwks.json');
360
+ * const payload = await jwtVerify(token, jwks, {
361
+ * algorithms: ['RS256'],
362
+ * issuer: 'https://example.auth0.com/',
363
+ * audience: 'https://api.example.com',
364
+ * });
365
+ * ```
366
+ */
367
+ function createJwks(url) {
368
+ return new Jwks(String(url));
369
+ }
370
+
371
+ //#endregion
372
+ export { extractBasicAuthToken as a, encodeBasicAuth as i, jwtSign as n, parseBasicAuth as o, jwtVerify as r, createJwks as t };
373
+ //# sourceMappingURL=jwt-ikNuUBcp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt-ikNuUBcp.mjs","names":["ALGORITHMS: Record<JwtAlgorithm, AlgorithmSpec>","key: CryptoKey","value: unknown","finalPayload: JwtPayload","kid: string | undefined","url: string"],"sources":["../src/auth/basic.ts","../src/auth/jwt.ts"],"sourcesContent":["/**\n * @fileoverview Basic HTTP authentication utilities.\n *\n * Provides functions for parsing and encoding Basic authentication credentials.\n *\n * @example\n * ```typescript\n * import { parseBasicAuth, encodeBasicAuth } from '@fresho/router/auth';\n *\n * // Parse credentials from Authorization header\n * const credentials = parseBasicAuth('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');\n * if (credentials) {\n * console.log(credentials.username); // 'username'\n * console.log(credentials.password); // 'password'\n * }\n *\n * // Create an Authorization header value\n * const header = encodeBasicAuth('username', 'password');\n * // 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='\n * ```\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Parsed Basic authentication credentials. */\nexport interface BasicCredentials {\n /** The username from the credentials. */\n username: string;\n /** The password from the credentials. */\n password: string;\n}\n\n// =============================================================================\n// Public API\n// =============================================================================\n\n/**\n * Parses Basic authentication credentials from an Authorization header value.\n *\n * @param authHeader - The full Authorization header value (e.g., 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')\n * @returns The parsed credentials, or null if parsing fails\n *\n * @example\n * ```typescript\n * const header = request.headers.get('Authorization');\n * const credentials = parseBasicAuth(header);\n * if (credentials) {\n * // Verify credentials\n * const isValid = await verifyPassword(credentials.username, credentials.password);\n * }\n * ```\n */\nexport function parseBasicAuth(authHeader: string | null): BasicCredentials | null {\n if (!authHeader?.startsWith('Basic ')) {\n return null;\n }\n\n try {\n const encoded = authHeader.slice(6);\n const decoded = atob(encoded);\n const colonIndex = decoded.indexOf(':');\n\n if (colonIndex === -1) {\n return null;\n }\n\n return {\n username: decoded.substring(0, colonIndex),\n password: decoded.substring(colonIndex + 1),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Encodes credentials into a Basic authentication header value.\n *\n * @param username - The username\n * @param password - The password\n * @returns The Authorization header value (e.g., 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')\n *\n * @example\n * ```typescript\n * const authHeader = encodeBasicAuth('admin', 'secret');\n * fetch('/api/endpoint', {\n * headers: { Authorization: authHeader }\n * });\n * ```\n */\nexport function encodeBasicAuth(username: string, password: string): string {\n return `Basic ${btoa(`${username}:${password}`)}`;\n}\n\n/**\n * Extracts the raw base64-encoded credentials from an Authorization header.\n *\n * @param authHeader - The full Authorization header value\n * @returns The base64 string, or null if not a Basic auth header\n */\nexport function extractBasicAuthToken(authHeader: string | null): string | null {\n if (!authHeader?.startsWith('Basic ')) {\n return null;\n }\n return authHeader.slice(6);\n}\n","/**\n * @fileoverview JWT signing and verification utilities.\n *\n * Provides functions for creating and validating JSON Web Tokens using\n * the Web Crypto API. Compatible with Cloudflare Workers, Deno, and browsers.\n *\n * @example\n * ```typescript\n * import { jwtSign, jwtVerify } from '@fresho/router/auth';\n *\n * // Sign a token\n * const token = await jwtSign(\n * { uid: 'user-123', role: 'admin' },\n * 'your-secret-key',\n * { expiresIn: '1h' }\n * );\n *\n * // Verify a token\n * const payload = await jwtVerify(token, 'your-secret-key');\n * console.log(payload.uid); // 'user-123'\n * ```\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** JWT payload type. */\nexport interface JwtPayload {\n /** Subject (user ID). */\n sub?: string;\n /** Issued at timestamp. */\n iat?: number;\n /** Expiration timestamp. */\n exp?: number;\n /** Not before timestamp. */\n nbf?: number;\n /** Issuer. */\n iss?: string;\n /** Audience. */\n aud?: string | string[];\n /** Additional claims. */\n [key: string]: unknown;\n}\n\n/** Supported algorithms for JWT signing/verification. */\nexport type JwtAlgorithm =\n | 'HS256'\n | 'HS384'\n | 'HS512'\n | 'RS256'\n | 'RS384'\n | 'RS512'\n | 'PS256'\n | 'PS384'\n | 'PS512'\n | 'ES256'\n | 'ES384';\n\n/**\n * JWT secret value: an HMAC secret, a CryptoKey (HMAC, or RSA/EC public or private key),\n * or a remote key set from {@link createJwks}.\n */\nexport type JwtSecret = string | BufferSource | CryptoKey | Jwks;\n\n/** Options for signing a JWT. */\nexport interface SignJwtOptions {\n /** Signing algorithm (default: 'HS256'). */\n algorithm?: JwtAlgorithm;\n /** Expiration time (e.g., '1h', '7d', '30m', or seconds as number). */\n expiresIn?: string | number;\n /** Not before time (e.g., '5m', or seconds as number). */\n notBefore?: string | number;\n /** Issuer claim. */\n issuer?: string;\n /** Audience claim. */\n audience?: string | string[];\n /** Subject claim. */\n subject?: string;\n /** Key ID header (`kid`), used by verifiers to pick a key from a JWKS. */\n keyId?: string;\n /** Custom issued-at timestamp (defaults to now). */\n issuedAt?: number | Date;\n}\n\n/** Options for verifying a JWT. */\nexport interface VerifyJwtOptions {\n /** Allowed algorithms (default: ['HS256'] for HMAC secrets, ['RS256'] otherwise). */\n algorithms?: JwtAlgorithm[];\n /** Required issuer (`iss`); any of the given values is accepted. */\n issuer?: string | string[];\n /** Required audience (`aud`); the token must contain at least one of the given values. */\n audience?: string | string[];\n /** Seconds of clock skew allowed when checking `exp` and `nbf` (default: 0). */\n clockTolerance?: number;\n}\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\n/** Web Crypto parameters for each JWT algorithm, used for key import, signing and verifying. */\ninterface AlgorithmSpec {\n name: 'HMAC' | 'RSASSA-PKCS1-v1_5' | 'RSA-PSS' | 'ECDSA';\n hash: string;\n saltLength?: number;\n namedCurve?: string;\n}\n\nconst ALGORITHMS: Record<JwtAlgorithm, AlgorithmSpec> = {\n HS256: { name: 'HMAC', hash: 'SHA-256' },\n HS384: { name: 'HMAC', hash: 'SHA-384' },\n HS512: { name: 'HMAC', hash: 'SHA-512' },\n RS256: { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n RS384: { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-384' },\n RS512: { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-512' },\n PS256: { name: 'RSA-PSS', hash: 'SHA-256', saltLength: 32 },\n PS384: { name: 'RSA-PSS', hash: 'SHA-384', saltLength: 48 },\n PS512: { name: 'RSA-PSS', hash: 'SHA-512', saltLength: 64 },\n ES256: { name: 'ECDSA', hash: 'SHA-256', namedCurve: 'P-256' },\n ES384: { name: 'ECDSA', hash: 'SHA-384', namedCurve: 'P-384' },\n};\n\n/** Checks that a CryptoKey was created for exactly this algorithm. */\nfunction keyMatches(key: CryptoKey, spec: AlgorithmSpec): boolean {\n const alg = key.algorithm as KeyAlgorithm & {\n hash?: { name: string };\n namedCurve?: string;\n modulusLength?: number;\n };\n if (alg.name !== spec.name) return false;\n if (spec.name === 'ECDSA') return alg.namedCurve === spec.namedCurve;\n // Web Crypto accepts RSA keys too weak for signatures (RFC 7518 §3.3 requires 2048 bits).\n if (spec.name !== 'HMAC' && (alg.modulusLength ?? 0) < 2048) return false;\n return alg.hash?.name === spec.hash;\n}\n\n/** Checks that a JWK may be used to verify this algorithm. */\nfunction jwkMatches(jwk: JsonWebKey & { kid?: string }, alg: JwtAlgorithm): boolean {\n const spec = ALGORITHMS[alg];\n if (jwk.alg !== undefined && jwk.alg !== alg) return false;\n if (jwk.use !== undefined && jwk.use !== 'sig') return false;\n if (spec.name === 'ECDSA') return jwk.kty === 'EC' && jwk.crv === spec.namedCurve;\n return spec.name !== 'HMAC' && jwk.kty === 'RSA';\n}\n\n/** Resolves the CryptoKey for a token, rejecting keys that don't fit its algorithm. */\nasync function resolveKey(\n secret: JwtSecret,\n alg: JwtAlgorithm,\n kid: string | undefined,\n usage: 'sign' | 'verify',\n): Promise<CryptoKey> {\n const spec = ALGORITHMS[alg];\n let key: CryptoKey;\n if (secret instanceof Jwks) {\n if (spec.name === 'HMAC') throw new Error(`Key does not match algorithm: ${alg}`);\n key = await secret.getKey(alg, kid);\n } else if (secret instanceof CryptoKey) {\n key = secret;\n } else {\n if (spec.name !== 'HMAC') throw new Error(`Key does not match algorithm: ${alg}`);\n const keyData = typeof secret === 'string' ? new TextEncoder().encode(secret) : secret;\n key = await crypto.subtle.importKey('raw', keyData, spec, false, [usage]);\n }\n if (!keyMatches(key, spec)) throw new Error(`Key does not match algorithm: ${alg}`);\n return key;\n}\n\n/** Decodes a base64url string to bytes. */\nfunction base64urlDecode(data: string): Uint8Array<ArrayBuffer> {\n // Strict alphabet so each token has one encoding; atob alone tolerates padding and whitespace.\n if (!/^[A-Za-z0-9_-]*$/.test(data)) throw new Error('Invalid JWT format');\n return Uint8Array.from(atob(data.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));\n}\n\n/** Decodes a base64url JSON segment, without echoing its content in errors. */\nfunction decodeSegment(data: string): any {\n let value: unknown;\n try {\n value = JSON.parse(new TextDecoder().decode(base64urlDecode(data)));\n } catch {}\n if (typeof value !== 'object' || value === null) throw new Error('Invalid JWT format');\n return value;\n}\n\n/** Returns true if `actual` (a claim value) contains any of `expected`. */\nfunction claimMatches(actual: unknown, expected: string | string[]): boolean {\n const values = Array.isArray(actual) ? actual : [actual];\n return [expected].flat().some((e) => values.includes(e));\n}\n\n/**\n * Parses a duration string (e.g., '1h', '7d', '30m') to seconds.\n */\nfunction parseDuration(duration: string | number): number {\n if (typeof duration === 'number') return duration;\n\n const match = duration.match(/^(\\d+)\\s*(s|m|h|d|w)$/i);\n if (!match) {\n throw new Error(\n `Invalid duration format: ${duration}. Use formats like '1h', '7d', '30m', '60s'.`,\n );\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2].toLowerCase();\n\n switch (unit) {\n case 's':\n return value;\n case 'm':\n return value * 60;\n case 'h':\n return value * 60 * 60;\n case 'd':\n return value * 60 * 60 * 24;\n case 'w':\n return value * 60 * 60 * 24 * 7;\n default:\n throw new Error(`Unknown duration unit: ${unit}`);\n }\n}\n\n/**\n * Base64url encodes a string or Uint8Array.\n */\nfunction base64urlEncode(data: string | Uint8Array): string {\n const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;\n const binary = String.fromCharCode(...bytes);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n// =============================================================================\n// Public API\n// =============================================================================\n\n/**\n * Signs a JWT token using Web Crypto API.\n *\n * Uses only Web Crypto API, compatible with Cloudflare Workers, Deno, and browsers.\n *\n * @param payload - The JWT payload (custom claims)\n * @param secret - The signing secret (string, bytes, or private/HMAC CryptoKey)\n * @param options - Optional signing options (algorithm, expiresIn, issuer, etc.)\n * @returns The signed JWT token string\n *\n * @example\n * ```typescript\n * // Basic usage (HS256 by default)\n * const token = await jwtSign(\n * { uid: 'user-123', role: 'admin' },\n * 'your-secret-key',\n * { expiresIn: '1h' }\n * );\n *\n * // With HS512 algorithm\n * const token = await jwtSign(\n * { uid: 'user-123' },\n * process.env.JWT_SECRET,\n * { algorithm: 'HS512', expiresIn: '7d' }\n * );\n * ```\n */\nexport async function jwtSign(\n payload: Record<string, unknown>,\n secret: JwtSecret,\n options: SignJwtOptions = {},\n): Promise<string> {\n const algorithm = options.algorithm || 'HS256';\n const spec = ALGORITHMS[algorithm];\n if (!spec) throw new Error(`Unknown algorithm: ${algorithm}`);\n\n const now = options.issuedAt\n ? options.issuedAt instanceof Date\n ? Math.floor(options.issuedAt.getTime() / 1000)\n : options.issuedAt\n : Math.floor(Date.now() / 1000);\n\n // Build the final payload with registered claims.\n const finalPayload: JwtPayload = {\n ...payload,\n iat: now,\n };\n\n if (options.expiresIn !== undefined) {\n finalPayload.exp = now + parseDuration(options.expiresIn);\n }\n\n if (options.notBefore !== undefined) {\n finalPayload.nbf = now + parseDuration(options.notBefore);\n }\n\n if (options.issuer !== undefined) {\n finalPayload.iss = options.issuer;\n }\n\n if (options.audience !== undefined) {\n finalPayload.aud = options.audience;\n }\n\n if (options.subject !== undefined) {\n finalPayload.sub = options.subject;\n }\n\n // Create header.\n const header = {\n alg: algorithm,\n typ: 'JWT',\n ...(options.keyId !== undefined && { kid: options.keyId }),\n };\n\n // Encode header and payload.\n const headerB64 = base64urlEncode(JSON.stringify(header));\n const payloadB64 = base64urlEncode(JSON.stringify(finalPayload));\n const data = `${headerB64}.${payloadB64}`;\n\n if (secret instanceof Jwks) throw new Error('Cannot sign with a JWKS');\n const key = await resolveKey(secret, algorithm, undefined, 'sign');\n const signatureBuffer = await crypto.subtle.sign(spec, key, new TextEncoder().encode(data));\n\n const signatureB64 = base64urlEncode(new Uint8Array(signatureBuffer));\n\n return `${data}.${signatureB64}`;\n}\n\n/**\n * Verifies a JWT token using Web Crypto API.\n *\n * Uses only Web Crypto API, compatible with Cloudflare Workers, Deno, and browsers.\n *\n * @param token - The JWT token string to verify\n * @param secret - The verification key (string, bytes, CryptoKey, or JWKS)\n * @param options - Optional verification options\n * @returns The decoded JWT payload\n * @throws Error if the token is invalid, expired, or not yet valid\n *\n * @example\n * ```typescript\n * import { jwtVerify } from '@fresho/router/auth';\n *\n * try {\n * const payload = await jwtVerify(token, 'your-secret-key');\n * console.log(payload.sub); // user ID\n * } catch (error) {\n * console.error('Invalid token:', error.message);\n * }\n *\n * // With specific algorithms\n * const payload = await jwtVerify(token, secret, { algorithms: ['HS512'] });\n * ```\n */\nexport async function jwtVerify(\n token: string,\n secret: JwtSecret,\n options: VerifyJwtOptions = {},\n): Promise<JwtPayload> {\n const symmetric =\n !(secret instanceof Jwks) && !(secret instanceof CryptoKey && secret.algorithm.name !== 'HMAC');\n const algorithms = options.algorithms || [symmetric ? 'HS256' : 'RS256'];\n\n const parts = token.split('.');\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format');\n }\n\n const [headerB64, payloadB64, signatureB64] = parts;\n\n // Decode header; the allowed list, not the token, decides which algorithms are acceptable.\n const header = decodeSegment(headerB64);\n if (!algorithms.includes(header.alg) || !Object.hasOwn(ALGORITHMS, header.alg)) {\n throw new Error(`Unsupported algorithm: ${header.alg}`);\n }\n const alg = header.alg as JwtAlgorithm;\n if (header.kid !== undefined && typeof header.kid !== 'string') {\n throw new Error('Invalid JWT format');\n }\n // No extensions are supported, so any critical one must be rejected (RFC 7515 §4.1.11).\n if (header.crit !== undefined) {\n throw new Error('Unsupported critical header');\n }\n const kid: string | undefined = header.kid;\n\n const key = await resolveKey(secret, alg, kid, 'verify');\n const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\n const valid = await crypto.subtle.verify(\n ALGORITHMS[alg],\n key,\n base64urlDecode(signatureB64),\n data,\n );\n\n if (!valid) {\n throw new Error('Invalid signature');\n }\n\n const payload = decodeSegment(payloadB64);\n\n const now = Date.now() / 1000;\n const tolerance = options.clockTolerance ?? 0;\n if (\n payload.exp !== undefined &&\n !(typeof payload.exp === 'number' && payload.exp + tolerance >= now)\n ) {\n throw new Error('Token expired');\n }\n if (\n payload.nbf !== undefined &&\n !(typeof payload.nbf === 'number' && payload.nbf - tolerance <= now)\n ) {\n throw new Error('Token not yet valid');\n }\n\n if (\n options.issuer !== undefined &&\n (typeof payload.iss !== 'string' || !claimMatches(payload.iss, options.issuer))\n ) {\n throw new Error('Invalid issuer');\n }\n\n if (options.audience !== undefined && !claimMatches(payload.aud, options.audience)) {\n throw new Error('Invalid audience');\n }\n\n return payload as JwtPayload;\n}\n\n// =============================================================================\n// Remote JWKS\n// =============================================================================\n\n/** How long fetched keys are trusted before the set is reloaded. */\nconst JWKS_MAX_AGE_MS = 10 * 60 * 1000;\n\n/** Minimum time between fetch attempts, successful or not. */\nconst JWKS_COOLDOWN_MS = 30 * 1000;\n\n/** Abort a JWKS fetch that takes longer than this. */\nconst JWKS_TIMEOUT_MS = 5 * 1000;\n\n/** A remote JSON Web Key Set, fetched lazily and cached. Create with {@link createJwks}. */\nexport class Jwks {\n private keys: (JsonWebKey & { kid?: string })[] = [];\n private fetchedAt = Number.NEGATIVE_INFINITY;\n private attemptedAt = Number.NEGATIVE_INFINITY;\n private pending?: Promise<void>;\n\n constructor(readonly url: string) {}\n\n /** Returns the verification key for `alg` and `kid`, reloading when stale or the kid is unknown. */\n async getKey(alg: JwtAlgorithm, kid: string | undefined): Promise<CryptoKey> {\n if (Date.now() - this.fetchedAt > JWKS_MAX_AGE_MS) await this.refresh();\n let candidates = this.match(alg, kid);\n if (candidates.length === 0) {\n await this.refresh();\n candidates = this.match(alg, kid);\n }\n // Without a kid the choice must be unambiguous.\n if (candidates.length !== 1) throw new Error('No matching key in JWKS');\n return crypto.subtle.importKey('jwk', candidates[0], ALGORITHMS[alg], false, ['verify']);\n }\n\n private match(alg: JwtAlgorithm, kid: string | undefined) {\n return this.keys.filter((k) => (kid === undefined || k.kid === kid) && jwkMatches(k, alg));\n }\n\n /** Joins an in-flight load or starts one at most once per cooldown; keeps old keys on failure. */\n private async refresh(): Promise<void> {\n if (!this.pending) {\n if (Date.now() - this.attemptedAt < JWKS_COOLDOWN_MS) return;\n this.attemptedAt = Date.now();\n this.pending = this.load().finally(() => {\n this.pending = undefined;\n });\n }\n try {\n await this.pending;\n } catch (error) {\n if (this.keys.length === 0) throw error;\n }\n }\n\n private async load(): Promise<void> {\n const response = await fetch(this.url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(JWKS_TIMEOUT_MS),\n });\n if (!response.ok) throw new Error(`JWKS fetch failed: ${response.status}`);\n const body = (await response.json().catch(() => ({}))) as { keys?: unknown };\n const keys = Array.isArray(body.keys)\n ? body.keys.filter((k) => typeof k === 'object' && k !== null)\n : [];\n if (keys.length === 0) throw new Error('JWKS response has no keys');\n this.keys = keys;\n this.fetchedAt = Date.now();\n }\n}\n\n/**\n * Creates a cached remote key set for verifying tokens from an OAuth/OIDC provider.\n *\n * Create it once (e.g. at module scope) and pass it as the secret. Keys are selected by the\n * token's `kid` and must match its algorithm; an unknown `kid` triggers a rate-limited reload.\n *\n * @example\n * ```typescript\n * const jwks = createJwks('https://example.auth0.com/.well-known/jwks.json');\n * const payload = await jwtVerify(token, jwks, {\n * algorithms: ['RS256'],\n * issuer: 'https://example.auth0.com/',\n * audience: 'https://api.example.com',\n * });\n * ```\n */\nexport function createJwks(url: string | URL): Jwks {\n return new Jwks(String(url));\n}\n"],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import { h as RouterRoutes, p as Router, t as Middleware } from "../middleware-zRMBpTLe.mjs";
2
+
3
+ //#region src/mcp/index.d.ts
4
+
5
+ /** OAuth 2.0 protected resource settings (RFC 9728). */
6
+ interface McpOAuthOptions {
7
+ /** Issuer URLs of the authorization servers that issue tokens for this server. */
8
+ authorizationServers: string[];
9
+ /** Scopes advertised to clients. */
10
+ scopes?: string[];
11
+ }
12
+ /** MCP server options. */
13
+ interface McpOptions {
14
+ /** Server name reported to clients. */
15
+ name: string;
16
+ /** Server version reported to clients. */
17
+ version: string;
18
+ /** Instructions for agents on how to use the tools. */
19
+ instructions?: string;
20
+ /** Advertise OAuth: 401s gain a `resource_metadata` challenge and metadata is served. */
21
+ oauth?: McpOAuthOptions;
22
+ /** Allow an endpoint without middleware: it is open, or already guarded by parent routers. */
23
+ public?: boolean;
24
+ }
25
+ /**
26
+ * Creates an MCP endpoint exposing routes as tools.
27
+ *
28
+ * Mount the result in a router (e.g. `mcp: mcp({ users }, options, auth)`). Tool names come from
29
+ * `route.operationId` or the method and path (`get_users`, `delete_users_by_id`), descriptions
30
+ * from `route.description`, and flat inputs from the route's path params and query/body schemas.
31
+ *
32
+ * Middleware passed after `options` guards the endpoint itself, and is required unless
33
+ * `options.public` is set; put auth there so unauthenticated
34
+ * clients get an HTTP 401 (with an OAuth challenge when `oauth` is set). Context it adds reaches
35
+ * handlers. Middleware on the given routers runs on every tool call, with the caller's headers
36
+ * forwarded; middleware on their parents elsewhere in the app does not.
37
+ *
38
+ * With `oauth`, metadata is served at `<endpoint>/.well-known/oauth-protected-resource` and
39
+ * referenced from the 401 challenge. Mount {@link oauthProtectedResource} at the origin root as
40
+ * well for clients that probe `/.well-known/oauth-protected-resource/<endpoint>` first.
41
+ *
42
+ * @param routes - A router, or a record of routers mounted under their keys
43
+ * @param options - Server info and optional OAuth settings
44
+ * @param middleware - Middleware for the MCP endpoint
45
+ */
46
+ declare function mcp(routes: Router<RouterRoutes> | Record<string, Router<RouterRoutes>>, options: McpOptions, ...middleware: Middleware[]): Router<RouterRoutes>;
47
+ /**
48
+ * Creates RFC 9728 protected resource metadata routes to mount as `'.well-known'` at the origin
49
+ * root, serving `/.well-known/oauth-protected-resource` and
50
+ * `/.well-known/oauth-protected-resource/<resource>` for single-segment resource paths like `/mcp`.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * router({
55
+ * mcp: mcp({ users }, { name: 'api', version: '1.0.0', oauth }, jwtAuth({ ... })),
56
+ * '.well-known': oauthProtectedResource(oauth),
57
+ * });
58
+ * ```
59
+ */
60
+ declare function oauthProtectedResource(oauth: McpOAuthOptions): Router<RouterRoutes>;
61
+ //#endregion
62
+ export { McpOAuthOptions, McpOptions, mcp, oauthProtectedResource };
63
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/mcp/index.ts"],"sourcesContent":[],"mappings":";;;;;UA4CiB,eAAA;;;;;;;UAQA,UAAA;;;;;;;;UAQP;;;;;;;;;;;;;;;;;;;;;;;;;iBAgRM,GAAA,SACN,OAAO,gBAAgB,eAAe,OAAO,yBAC5C,2BACM,eACd,OAAO;;;;;;;;;;;;;;iBAyDM,sBAAA,QAA8B,kBAAkB,OAAO"}