@fresho/router 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,675 @@
1
+ import { t as compose } from "../middleware-BpFh0XrR.mjs";
2
+
3
+ //#region src/middleware/auth.ts
4
+ /**
5
+ * Creates a Basic HTTP authentication middleware.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const adminRouter = router('/admin', routes,
10
+ * basicAuth({
11
+ * verify: async (username, password) => {
12
+ * if (username === 'admin' && password === 'secret') {
13
+ * return { user: { name: username } };
14
+ * }
15
+ * return null;
16
+ * }
17
+ * })
18
+ * );
19
+ * ```
20
+ */
21
+ function basicAuth(options) {
22
+ const realm = options.realm || "Secure Area";
23
+ return async (context, next) => {
24
+ const { request } = context;
25
+ const authHeader = request.headers.get("Authorization");
26
+ if (!authHeader?.startsWith("Basic ")) return new Response("Unauthorized", {
27
+ status: 401,
28
+ headers: { "WWW-Authenticate": `Basic realm="${realm}", charset="UTF-8"` }
29
+ });
30
+ try {
31
+ const credentials = atob(authHeader.slice(6));
32
+ const colonIndex = credentials.indexOf(":");
33
+ if (colonIndex === -1) throw new Error("Invalid credentials format");
34
+ const username = credentials.substring(0, colonIndex);
35
+ const password = credentials.substring(colonIndex + 1);
36
+ const claims = await options.verify(username, password, context);
37
+ if (claims === null) return new Response("Invalid credentials", {
38
+ status: 401,
39
+ headers: { "WWW-Authenticate": `Basic realm="${realm}", charset="UTF-8"` }
40
+ });
41
+ Object.assign(context, claims);
42
+ return next();
43
+ } catch (_error) {
44
+ return new Response("Invalid credentials", {
45
+ status: 401,
46
+ headers: { "WWW-Authenticate": `Basic realm="${realm}", charset="UTF-8"` }
47
+ });
48
+ }
49
+ };
50
+ }
51
+ /** Maps JWT algorithm names to Web Crypto hash names. */
52
+ const ALGORITHM_MAP = {
53
+ HS256: "SHA-256",
54
+ HS384: "SHA-384",
55
+ HS512: "SHA-512"
56
+ };
57
+ /**
58
+ * Verifies a JWT token using Web Crypto API.
59
+ *
60
+ * Uses only Web Crypto API, compatible with Cloudflare Workers, Deno, and browsers.
61
+ *
62
+ * @param token - The JWT token string to verify
63
+ * @param secret - The secret used to sign the token (string, ArrayBuffer, or CryptoKey)
64
+ * @param options - Optional verification options
65
+ * @returns The decoded JWT payload
66
+ * @throws Error if the token is invalid, expired, or not yet valid
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * import { jwtVerify } from '@fresho/router/middleware';
71
+ *
72
+ * try {
73
+ * const payload = await jwtVerify(token, 'your-secret-key');
74
+ * console.log(payload.sub); // user ID
75
+ * } catch (error) {
76
+ * console.error('Invalid token:', error.message);
77
+ * }
78
+ *
79
+ * // With specific algorithms
80
+ * const payload = await jwtVerify(token, secret, { algorithms: ['HS512'] });
81
+ * ```
82
+ */
83
+ async function jwtVerify(token, secret, options = {}) {
84
+ const algorithms = options.algorithms || ["HS256"];
85
+ const parts = token.split(".");
86
+ if (parts.length !== 3) throw new Error("Invalid JWT format");
87
+ const [headerB64, payloadB64, signatureB64] = parts;
88
+ const header = JSON.parse(atob(headerB64.replace(/-/g, "+").replace(/_/g, "/")));
89
+ if (!algorithms.includes(header.alg)) throw new Error(`Unsupported algorithm: ${header.alg}`);
90
+ const hashAlg = ALGORITHM_MAP[header.alg];
91
+ if (!hashAlg) throw new Error(`Unknown algorithm: ${header.alg}`);
92
+ const payload = JSON.parse(atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/")));
93
+ const data = `${headerB64}.${payloadB64}`;
94
+ const signature = Uint8Array.from(atob(signatureB64.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
95
+ let key;
96
+ if (secret instanceof CryptoKey) key = secret;
97
+ else {
98
+ const keyData = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
99
+ key = await crypto.subtle.importKey("raw", keyData, {
100
+ name: "HMAC",
101
+ hash: hashAlg
102
+ }, false, ["verify"]);
103
+ }
104
+ if (!await crypto.subtle.verify("HMAC", key, signature, new TextEncoder().encode(data))) throw new Error("Invalid signature");
105
+ if (payload.exp && payload.exp < Date.now() / 1e3) throw new Error("Token expired");
106
+ if (payload.nbf && payload.nbf > Date.now() / 1e3) throw new Error("Token not yet valid");
107
+ return payload;
108
+ }
109
+ /**
110
+ * Parses a duration string (e.g., '1h', '7d', '30m') to seconds.
111
+ */
112
+ function parseDuration(duration) {
113
+ if (typeof duration === "number") return duration;
114
+ const match = duration.match(/^(\d+)\s*(s|m|h|d|w)$/i);
115
+ if (!match) throw new Error(`Invalid duration format: ${duration}. Use formats like '1h', '7d', '30m', '60s'.`);
116
+ const value = parseInt(match[1], 10);
117
+ const unit = match[2].toLowerCase();
118
+ switch (unit) {
119
+ case "s": return value;
120
+ case "m": return value * 60;
121
+ case "h": return value * 60 * 60;
122
+ case "d": return value * 60 * 60 * 24;
123
+ case "w": return value * 60 * 60 * 24 * 7;
124
+ default: throw new Error(`Unknown duration unit: ${unit}`);
125
+ }
126
+ }
127
+ /**
128
+ * Base64url encodes a string or Uint8Array.
129
+ */
130
+ function base64urlEncode(data) {
131
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
132
+ const binary = String.fromCharCode(...bytes);
133
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
134
+ }
135
+ /**
136
+ * Signs a JWT token using Web Crypto API.
137
+ *
138
+ * Uses only Web Crypto API, compatible with Cloudflare Workers, Deno, and browsers.
139
+ *
140
+ * @param payload - The JWT payload (custom claims)
141
+ * @param secret - The signing secret (string, ArrayBuffer, or CryptoKey)
142
+ * @param options - Optional signing options (algorithm, expiresIn, issuer, etc.)
143
+ * @returns The signed JWT token string
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * // Basic usage (HS256 by default)
148
+ * const token = await jwtSign(
149
+ * { uid: 'user-123', role: 'admin' },
150
+ * 'your-secret-key',
151
+ * { expiresIn: '1h' }
152
+ * );
153
+ *
154
+ * // With HS512 algorithm
155
+ * const token = await jwtSign(
156
+ * { uid: 'user-123' },
157
+ * process.env.JWT_SECRET,
158
+ * { algorithm: 'HS512', expiresIn: '7d' }
159
+ * );
160
+ * ```
161
+ */
162
+ async function jwtSign(payload, secret, options = {}) {
163
+ const algorithm = options.algorithm || "HS256";
164
+ const hashAlg = ALGORITHM_MAP[algorithm];
165
+ const now = options.issuedAt ? options.issuedAt instanceof Date ? Math.floor(options.issuedAt.getTime() / 1e3) : options.issuedAt : Math.floor(Date.now() / 1e3);
166
+ const finalPayload = {
167
+ ...payload,
168
+ iat: now
169
+ };
170
+ if (options.expiresIn !== void 0) finalPayload.exp = now + parseDuration(options.expiresIn);
171
+ if (options.notBefore !== void 0) finalPayload.nbf = now + parseDuration(options.notBefore);
172
+ if (options.issuer !== void 0) finalPayload.iss = options.issuer;
173
+ if (options.audience !== void 0) finalPayload.aud = options.audience;
174
+ if (options.subject !== void 0) finalPayload.sub = options.subject;
175
+ const header = {
176
+ alg: algorithm,
177
+ typ: "JWT"
178
+ };
179
+ const data = `${base64urlEncode(JSON.stringify(header))}.${base64urlEncode(JSON.stringify(finalPayload))}`;
180
+ let key;
181
+ if (secret instanceof CryptoKey) key = secret;
182
+ else {
183
+ const keyData = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
184
+ key = await crypto.subtle.importKey("raw", keyData, {
185
+ name: "HMAC",
186
+ hash: hashAlg
187
+ }, false, ["sign"]);
188
+ }
189
+ const signatureBuffer = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
190
+ return `${data}.${base64urlEncode(new Uint8Array(signatureBuffer))}`;
191
+ }
192
+ /**
193
+ * Creates a JWT authentication middleware.
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * interface AppContext {
198
+ * env: { JWT_SECRET: string };
199
+ * user: { id: string; email: string };
200
+ * }
201
+ *
202
+ * jwtAuth<AppContext>({
203
+ * secret: (ctx) => ctx.env.JWT_SECRET, // ctx.env is typed
204
+ * claims: (payload) => ({
205
+ * user: { id: payload.sub, email: payload.email },
206
+ * }),
207
+ * })
208
+ *
209
+ * // In your route handler:
210
+ * route.ctx<AppContext>()({
211
+ * handler: async (c) => {
212
+ * return { email: c.user.email }; // Fully typed
213
+ * },
214
+ * })
215
+ * ```
216
+ */
217
+ function jwtAuth(options) {
218
+ const getToken = options.getToken || ((req) => {
219
+ const auth = req.headers.get("Authorization");
220
+ if (auth?.startsWith("Bearer ")) return auth.slice(7);
221
+ const cookie = req.headers.get("Cookie");
222
+ if (cookie) {
223
+ const match = cookie.match(/(?:^|;\s*)token=([^;]*)/);
224
+ if (match) return match[1];
225
+ }
226
+ return null;
227
+ });
228
+ return async (context, next) => {
229
+ const { request } = context;
230
+ const token = getToken(request);
231
+ if (!token) return new Response("Missing token", {
232
+ status: 401,
233
+ headers: { "WWW-Authenticate": "Bearer" }
234
+ });
235
+ try {
236
+ const payload = await jwtVerify(token, typeof options.secret === "function" ? options.secret(context) : options.secret, { algorithms: options.algorithms });
237
+ const claims = options.claims(payload);
238
+ if (claims === null) throw new Error("Token validation failed");
239
+ Object.assign(context, claims);
240
+ return next();
241
+ } catch (error) {
242
+ const message = error instanceof Error ? error.message : "Invalid token";
243
+ return new Response(message, {
244
+ status: 401,
245
+ headers: { "WWW-Authenticate": `Bearer error="invalid_token", error_description="${message}"` }
246
+ });
247
+ }
248
+ };
249
+ }
250
+ /**
251
+ * Creates a simple bearer token authentication middleware.
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * const apiRouter = router('/api', routes,
256
+ * bearerAuth({
257
+ * verify: async (token) => {
258
+ * if (token === process.env.API_TOKEN) {
259
+ * return { apiClient: 'trusted' };
260
+ * }
261
+ * return null;
262
+ * }
263
+ * })
264
+ * );
265
+ * ```
266
+ */
267
+ function bearerAuth(options) {
268
+ return async (context, next) => {
269
+ const { request } = context;
270
+ const authHeader = request.headers.get("Authorization");
271
+ if (!authHeader?.startsWith("Bearer ")) return new Response("Missing token", {
272
+ status: 401,
273
+ headers: { "WWW-Authenticate": "Bearer" }
274
+ });
275
+ const token = authHeader.slice(7);
276
+ const claims = await options.verify(token, context);
277
+ if (claims === null) return new Response("Invalid token", {
278
+ status: 401,
279
+ headers: { "WWW-Authenticate": "Bearer error=\"invalid_token\"" }
280
+ });
281
+ Object.assign(context, claims);
282
+ return next();
283
+ };
284
+ }
285
+
286
+ //#endregion
287
+ //#region src/middleware/common.ts
288
+ /**
289
+ * HTTP error with status code for use with errorHandler middleware.
290
+ *
291
+ * Throw this in route handlers to return a specific HTTP status code.
292
+ * The errorHandler middleware will catch it and format the response.
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * import { HttpError } from '@fresho/router/middleware';
297
+ *
298
+ * const getUser = route({
299
+ * method: 'get',
300
+ * path: '/users/:id',
301
+ * handler: async (c) => {
302
+ * const user = await db.findUser(c.path.id);
303
+ * if (!user) throw new HttpError('User not found', 404);
304
+ * return user;
305
+ * },
306
+ * });
307
+ * ```
308
+ */
309
+ var HttpError = class extends Error {
310
+ constructor(message, status) {
311
+ super(message);
312
+ this.status = status;
313
+ this.name = "HttpError";
314
+ }
315
+ };
316
+ /**
317
+ * Extracts HTTP status code from an error object.
318
+ *
319
+ * Supports errors with `status` or `statusCode` properties (common patterns
320
+ * in frameworks like Express, Koa, Hono, and custom HTTP error classes).
321
+ */
322
+ function getErrorStatus(error) {
323
+ if (typeof error !== "object" || error === null) return 500;
324
+ if ("status" in error && typeof error.status === "number") return error.status;
325
+ if ("statusCode" in error && typeof error.statusCode === "number") return error.statusCode;
326
+ return 500;
327
+ }
328
+ /**
329
+ * Creates an error handling middleware that catches and formats errors.
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * const apiRouter = router('/api', routes, errorHandler({ expose: false }));
334
+ * ```
335
+ */
336
+ function errorHandler(options = {}) {
337
+ return async (context, next) => {
338
+ try {
339
+ return await next();
340
+ } catch (error) {
341
+ const err = error;
342
+ if (options.log) await options.log(err, context);
343
+ else console.error(`Error in ${context.request.method} ${context.request.url}:`, err);
344
+ if (options.formatter) return await options.formatter(err, context);
345
+ const message = options.expose ? err.message : "Internal Server Error";
346
+ const status = getErrorStatus(error);
347
+ const errorBody = {
348
+ error: message,
349
+ ...options.expose && {
350
+ stack: err.stack,
351
+ name: err.name
352
+ }
353
+ };
354
+ return new Response(JSON.stringify(errorBody), {
355
+ status,
356
+ headers: { "Content-Type": "application/json" }
357
+ });
358
+ }
359
+ };
360
+ }
361
+ /**
362
+ * Creates a request/response logging middleware.
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * const apiRouter = router('/api', routes, logger({ includeHeaders: true }));
367
+ * ```
368
+ */
369
+ function logger(options = {}) {
370
+ const log = options.log || console.log;
371
+ return async (context, next) => {
372
+ const { request } = context;
373
+ const start = Date.now();
374
+ const url = new URL(request.url);
375
+ const info = {
376
+ method: request.method,
377
+ url: `${url.pathname}${url.search}`,
378
+ status: 0,
379
+ duration: 0
380
+ };
381
+ if (options.includeHeaders) info.headers = Object.fromEntries(request.headers.entries());
382
+ if (options.includeBody && request.body) try {
383
+ const text = await request.clone().text();
384
+ try {
385
+ info.body = JSON.parse(text);
386
+ } catch {
387
+ info.body = text;
388
+ }
389
+ } catch {
390
+ info.body = "[Body not readable]";
391
+ }
392
+ log(options.formatter ? options.formatter({
393
+ ...info,
394
+ status: 0,
395
+ duration: 0
396
+ }) : `→ ${info.method} ${info.url}`);
397
+ try {
398
+ const response = await next();
399
+ info.status = response.status;
400
+ info.duration = Date.now() - start;
401
+ log(options.formatter ? options.formatter(info) : `← ${info.status} ${response.statusText || "OK"} (${info.duration}ms)`);
402
+ return response;
403
+ } catch (error) {
404
+ info.status = 500;
405
+ info.duration = Date.now() - start;
406
+ info.error = error;
407
+ log(options.formatter ? options.formatter(info) : `✗ ${info.status} Error: ${error.message} (${info.duration}ms)`);
408
+ throw error;
409
+ }
410
+ };
411
+ }
412
+ /** In-memory rate limit store with periodic cleanup of expired entries. */
413
+ var MemoryRateLimitStore = class {
414
+ counts = /* @__PURE__ */ new Map();
415
+ lastCleanup = Date.now();
416
+ /** Interval between cleanups (default: 60 seconds). */
417
+ cleanupIntervalMs;
418
+ constructor(windowMs, options) {
419
+ this.windowMs = windowMs;
420
+ this.cleanupIntervalMs = options?.cleanupIntervalMs ?? Math.max(windowMs, 6e4);
421
+ }
422
+ async increment(key) {
423
+ const now = Date.now();
424
+ if (now - this.lastCleanup > this.cleanupIntervalMs) this.cleanup(now);
425
+ const record = this.counts.get(key);
426
+ if (!record || now > record.resetAt) {
427
+ this.counts.set(key, {
428
+ count: 1,
429
+ resetAt: now + this.windowMs
430
+ });
431
+ return 1;
432
+ }
433
+ record.count++;
434
+ return record.count;
435
+ }
436
+ async decrement(key) {
437
+ const record = this.counts.get(key);
438
+ if (record && record.count > 0) record.count--;
439
+ }
440
+ async reset(key) {
441
+ this.counts.delete(key);
442
+ }
443
+ /** Removes all expired entries from the store. */
444
+ cleanup(now) {
445
+ for (const [key, record] of this.counts) if (now > record.resetAt) this.counts.delete(key);
446
+ this.lastCleanup = now;
447
+ }
448
+ /** Returns the current number of tracked keys (useful for testing). */
449
+ get size() {
450
+ return this.counts.size;
451
+ }
452
+ };
453
+ /**
454
+ * Creates a rate limiting middleware.
455
+ *
456
+ * @example
457
+ * ```typescript
458
+ * const apiRouter = router('/api', routes, rateLimit({ max: 100, windowMs: 60000 }));
459
+ * ```
460
+ */
461
+ function rateLimit(options = {}) {
462
+ const config = {
463
+ windowMs: 60 * 1e3,
464
+ max: 100,
465
+ keyGenerator: (ctx) => {
466
+ const cfIp = ctx.request.headers.get("CF-Connecting-IP");
467
+ const xForwardedFor = ctx.request.headers.get("X-Forwarded-For");
468
+ const xRealIp = ctx.request.headers.get("X-Real-IP");
469
+ return cfIp || xForwardedFor?.split(",")[0] || xRealIp || "unknown";
470
+ },
471
+ ...options
472
+ };
473
+ const store = config.store || new MemoryRateLimitStore(config.windowMs);
474
+ return async (context, next) => {
475
+ const key = config.keyGenerator(context);
476
+ const current = await store.increment(key);
477
+ if (current > config.max) {
478
+ if (config.handler) return await config.handler(context);
479
+ return new Response("Too many requests", {
480
+ status: 429,
481
+ headers: {
482
+ "Retry-After": String(Math.ceil(config.windowMs / 1e3)),
483
+ "X-RateLimit-Limit": String(config.max),
484
+ "X-RateLimit-Remaining": "0",
485
+ "X-RateLimit-Reset": new Date(Date.now() + config.windowMs).toISOString()
486
+ }
487
+ });
488
+ }
489
+ const response = await next();
490
+ if (config.skipSuccessfulRequests && response.status < 400 || config.skipFailedRequests && response.status >= 400) await store.decrement(key);
491
+ const headers = new Headers(response.headers);
492
+ headers.set("X-RateLimit-Limit", String(config.max));
493
+ headers.set("X-RateLimit-Remaining", String(Math.max(0, config.max - current)));
494
+ headers.set("X-RateLimit-Reset", new Date(Date.now() + config.windowMs).toISOString());
495
+ return new Response(response.body, {
496
+ status: response.status,
497
+ statusText: response.statusText,
498
+ headers
499
+ });
500
+ };
501
+ }
502
+ /**
503
+ * Creates a middleware that adds a unique request ID to each request.
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * const apiRouter = router('/api', routes, requestId({ headerName: 'X-Request-ID' }));
508
+ * ```
509
+ */
510
+ function requestId(options = {}) {
511
+ const headerName = options.headerName || "X-Request-ID";
512
+ const generator = options.generator || (() => crypto.randomUUID());
513
+ return async (context, next) => {
514
+ let id = context.request.headers.get(headerName);
515
+ if (!id) id = generator();
516
+ context.requestId = id;
517
+ const response = await next();
518
+ const headers = new Headers(response.headers);
519
+ headers.set(headerName, id);
520
+ return new Response(response.body, {
521
+ status: response.status,
522
+ statusText: response.statusText,
523
+ headers
524
+ });
525
+ };
526
+ }
527
+ /**
528
+ * Creates a middleware that times out requests after a specified duration.
529
+ *
530
+ * @example
531
+ * ```typescript
532
+ * const apiRouter = router('/api', routes, timeout({ timeout: 5000 }));
533
+ * ```
534
+ */
535
+ function timeout(options) {
536
+ return async (context, next) => {
537
+ const controller = new AbortController();
538
+ const timeoutId = setTimeout(() => controller.abort(), options.timeout);
539
+ try {
540
+ const responsePromise = next();
541
+ const timeoutPromise = new Promise((_, reject) => {
542
+ controller.signal.addEventListener("abort", () => {
543
+ reject(/* @__PURE__ */ new Error("Request timeout"));
544
+ });
545
+ });
546
+ const response = await Promise.race([responsePromise, timeoutPromise]);
547
+ clearTimeout(timeoutId);
548
+ return response;
549
+ } catch (error) {
550
+ clearTimeout(timeoutId);
551
+ if (error.message === "Request timeout") return new Response(options.message || "Request timeout", { status: 408 });
552
+ throw error;
553
+ }
554
+ };
555
+ }
556
+ /**
557
+ * Creates a middleware that validates request content type.
558
+ *
559
+ * @example
560
+ * ```typescript
561
+ * const apiRouter = router('/api', routes, contentType({ types: ['application/json'] }));
562
+ * ```
563
+ */
564
+ function contentType(options) {
565
+ const skipMethods = new Set(options.skipMethods?.map((m) => m.toUpperCase()) || [
566
+ "GET",
567
+ "HEAD",
568
+ "OPTIONS"
569
+ ]);
570
+ return async (context, next) => {
571
+ if (skipMethods.has(context.request.method.toUpperCase())) return next();
572
+ const contentType$1 = context.request.headers.get("Content-Type");
573
+ if (!contentType$1) return new Response(options.message || "Missing Content-Type header", { status: 415 });
574
+ if (!options.types.some((type) => contentType$1.includes(type))) return new Response(options.message || `Unsupported Media Type. Expected: ${options.types.join(", ")}`, { status: 415 });
575
+ return next();
576
+ };
577
+ }
578
+
579
+ //#endregion
580
+ //#region src/middleware/cors.ts
581
+ /**
582
+ * Determines if an origin is allowed based on the configuration.
583
+ */
584
+ function getAllowedOrigin(requestOrigin, configOrigin) {
585
+ if (!requestOrigin) return "*";
586
+ if (!configOrigin || configOrigin === "*") return "*";
587
+ if (typeof configOrigin === "string") return configOrigin === requestOrigin ? configOrigin : "false";
588
+ if (Array.isArray(configOrigin)) return configOrigin.includes(requestOrigin) ? requestOrigin : "false";
589
+ if (configOrigin instanceof RegExp) return configOrigin.test(requestOrigin) ? requestOrigin : "false";
590
+ if (typeof configOrigin === "function") return configOrigin(requestOrigin) ? requestOrigin : "false";
591
+ return "false";
592
+ }
593
+ /**
594
+ * Creates a CORS middleware with the specified options.
595
+ *
596
+ * @example
597
+ * ```typescript
598
+ * const apiRouter = router('/api', routes, cors({ origin: 'https://example.com', credentials: true }));
599
+ * ```
600
+ */
601
+ function cors(options = {}) {
602
+ const config = {
603
+ origin: "*",
604
+ methods: [
605
+ "GET",
606
+ "POST",
607
+ "PUT",
608
+ "PATCH",
609
+ "DELETE",
610
+ "OPTIONS",
611
+ "HEAD"
612
+ ],
613
+ allowedHeaders: ["Content-Type", "Authorization"],
614
+ exposedHeaders: [],
615
+ credentials: false,
616
+ maxAge: 86400,
617
+ preflightContinue: false,
618
+ ...options
619
+ };
620
+ return async (context, next) => {
621
+ const { request } = context;
622
+ const allowedOrigin = getAllowedOrigin(request.headers.get("Origin"), config.origin);
623
+ if (request.method === "OPTIONS") {
624
+ const headers$1 = {
625
+ "Access-Control-Allow-Origin": allowedOrigin,
626
+ "Access-Control-Allow-Methods": config.methods.join(", "),
627
+ Vary: "Origin"
628
+ };
629
+ if (config.allowedHeaders.length > 0) {
630
+ const requestHeaders = request.headers.get("Access-Control-Request-Headers");
631
+ if (requestHeaders) {
632
+ const requested = requestHeaders.split(",").map((h) => h.trim().toLowerCase());
633
+ const allowed = config.allowedHeaders.map((h) => h.toLowerCase());
634
+ const toAllow = requested.filter((h) => allowed.includes(h));
635
+ if (toAllow.length > 0) headers$1["Access-Control-Allow-Headers"] = toAllow.join(", ");
636
+ } else headers$1["Access-Control-Allow-Headers"] = config.allowedHeaders.join(", ");
637
+ }
638
+ if (config.exposedHeaders.length > 0) headers$1["Access-Control-Expose-Headers"] = config.exposedHeaders.join(", ");
639
+ if (config.credentials) headers$1["Access-Control-Allow-Credentials"] = "true";
640
+ if (config.maxAge) headers$1["Access-Control-Max-Age"] = String(config.maxAge);
641
+ const response$1 = new Response(null, {
642
+ status: 204,
643
+ headers: headers$1
644
+ });
645
+ if (config.preflightContinue) {
646
+ const nextResponse = await next();
647
+ const mergedHeaders = new Headers(nextResponse.headers);
648
+ Object.entries(headers$1).forEach(([key, value]) => {
649
+ mergedHeaders.set(key, value);
650
+ });
651
+ return new Response(nextResponse.body, {
652
+ status: nextResponse.status,
653
+ statusText: nextResponse.statusText,
654
+ headers: mergedHeaders
655
+ });
656
+ }
657
+ return response$1;
658
+ }
659
+ const response = await next();
660
+ const headers = new Headers(response.headers);
661
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
662
+ headers.set("Vary", "Origin");
663
+ if (config.credentials && allowedOrigin !== "*") headers.set("Access-Control-Allow-Credentials", "true");
664
+ if (config.exposedHeaders.length > 0) headers.set("Access-Control-Expose-Headers", config.exposedHeaders.join(", "));
665
+ return new Response(response.body, {
666
+ status: response.status,
667
+ statusText: response.statusText,
668
+ headers
669
+ });
670
+ };
671
+ }
672
+
673
+ //#endregion
674
+ export { HttpError, MemoryRateLimitStore, basicAuth, bearerAuth, compose, contentType, cors, errorHandler, jwtAuth, jwtSign, jwtVerify, logger, rateLimit, requestId, timeout };
675
+ //# sourceMappingURL=index.mjs.map