@bepalo/router 1.11.32 → 1.12.33

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.
Files changed (72) hide show
  1. package/dist/cjs/framework.d.ts +2 -4
  2. package/dist/cjs/framework.d.ts.map +1 -1
  3. package/dist/cjs/framework.js +4 -6
  4. package/dist/cjs/framework.js.map +1 -1
  5. package/dist/cjs/helpers.d.ts +2 -2
  6. package/dist/cjs/helpers.d.ts.map +1 -1
  7. package/dist/cjs/helpers.js +1 -1
  8. package/dist/cjs/helpers.js.map +1 -1
  9. package/dist/cjs/index.d.ts +5 -5
  10. package/dist/cjs/index.d.ts.map +1 -1
  11. package/dist/cjs/index.js +5 -5
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/middlewares.d.ts +2 -2
  14. package/dist/cjs/middlewares.d.ts.map +1 -1
  15. package/dist/cjs/middlewares.js +24 -24
  16. package/dist/cjs/middlewares.js.map +1 -1
  17. package/dist/cjs/router.d.ts +2 -2
  18. package/dist/cjs/router.d.ts.map +1 -1
  19. package/dist/cjs/router.js +8 -8
  20. package/dist/cjs/router.js.map +1 -1
  21. package/dist/cjs/types.d.ts +1 -1
  22. package/dist/cjs/types.d.ts.map +1 -1
  23. package/dist/cjs/upload-stream.d.ts +1 -1
  24. package/dist/cjs/upload-stream.d.ts.map +1 -1
  25. package/dist/cjs/upload-stream.js +7 -7
  26. package/dist/cjs/upload-stream.js.map +1 -1
  27. package/dist/framework.d.ts +2 -4
  28. package/dist/framework.d.ts.map +1 -1
  29. package/dist/framework.js +4 -6
  30. package/dist/framework.js.map +1 -1
  31. package/dist/helpers.d.ts +2 -2
  32. package/dist/helpers.d.ts.map +1 -1
  33. package/dist/helpers.js +1 -1
  34. package/dist/helpers.js.map +1 -1
  35. package/dist/index.d.ts +5 -5
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5 -5
  38. package/dist/index.js.map +1 -1
  39. package/dist/middlewares.d.ts +2 -2
  40. package/dist/middlewares.d.ts.map +1 -1
  41. package/dist/middlewares.js +24 -24
  42. package/dist/middlewares.js.map +1 -1
  43. package/dist/router.d.ts +2 -2
  44. package/dist/router.d.ts.map +1 -1
  45. package/dist/router.js +8 -8
  46. package/dist/router.js.map +1 -1
  47. package/dist/types.d.ts +1 -1
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/upload-stream.d.ts +1 -1
  50. package/dist/upload-stream.d.ts.map +1 -1
  51. package/dist/upload-stream.js +7 -7
  52. package/dist/upload-stream.js.map +1 -1
  53. package/package.json +8 -1
  54. package/src/framework.deno.ts +194 -0
  55. package/src/framework.ts +197 -0
  56. package/src/helpers.ts +829 -0
  57. package/src/index.ts +5 -0
  58. package/src/list.ts +462 -0
  59. package/src/middlewares.deno.ts +851 -0
  60. package/src/middlewares.ts +851 -0
  61. package/src/router.ts +993 -0
  62. package/src/tree.ts +139 -0
  63. package/src/types.ts +197 -0
  64. package/src/upload-stream.ts +661 -0
  65. package/dist/cjs/framework.deno.d.ts +0 -31
  66. package/dist/cjs/framework.deno.d.ts.map +0 -1
  67. package/dist/cjs/framework.deno.js +0 -245
  68. package/dist/cjs/framework.deno.js.map +0 -1
  69. package/dist/framework.deno.d.ts +0 -31
  70. package/dist/framework.deno.d.ts.map +0 -1
  71. package/dist/framework.deno.js +0 -245
  72. package/dist/framework.deno.js.map +0 -1
@@ -0,0 +1,851 @@
1
+ import { parseCookieFromRequest, status } from "./helpers.ts";
2
+ import type { RouterContext } from "./router.ts";
3
+ import type { FreeHandler, HttpMethod, CTXAddress } from "./types.ts";
4
+ import { Cache, CacheConfig } from "jsr:@bepalo/cache/mod.ts";
5
+ import { JWT, JwtPayload, JwtVerifyOptions } from "jsr:@bepalo/jwt/mod.ts";
6
+ import { Time } from "jsr:@bepalo/time/mod.ts";
7
+
8
+ /**
9
+ * Context object containing parsed query.
10
+ * @typedef {Object} CTXQuery
11
+ * @property {Record<string, string>} query - Parsed query from the request url
12
+ */
13
+ export type CTXQuery = {
14
+ query: Record<string, string>;
15
+ };
16
+
17
+ /**
18
+ * Creates middleware that parses queries from the request url and adds them to the context.
19
+ * @returns {Function} A middleware function that adds parsed queries to context.query
20
+ */
21
+ export const parseQuery = <XContext = {}>(): FreeHandler<
22
+ XContext & CTXQuery
23
+ > => {
24
+ return (req: Request, ctx: RouterContext<XContext & CTXQuery>) => {
25
+ const query = Object.fromEntries(ctx.url.searchParams.entries());
26
+ ctx.query = query;
27
+ };
28
+ };
29
+
30
+ /**
31
+ * Context object containing parsed cookies.
32
+ * @typedef {Object} CTXCookie
33
+ * @property {Record<string, string>} cookie - Parsed cookies from the request
34
+ */
35
+ export type CTXCookie = {
36
+ cookie: Record<string, string>;
37
+ };
38
+
39
+ /**
40
+ * Creates middleware that parses cookies from the request and adds them to the context.
41
+ * @returns {Function} A middleware function that adds parsed cookies to context.cookie
42
+ */
43
+ export const parseCookie = <XContext = {}>(): FreeHandler<
44
+ XContext & CTXCookie
45
+ > => {
46
+ return (req: Request, ctx: RouterContext<XContext & CTXCookie>) => {
47
+ const cookie = parseCookieFromRequest(req) ?? {};
48
+ ctx.cookie = cookie;
49
+ };
50
+ };
51
+
52
+ /**
53
+ * Parsed body object types.
54
+ * @typedef {Object} ParsedBody
55
+ */
56
+ export type ParsedBody =
57
+ | { value: string | number | boolean | null }
58
+ | { values: unknown[] }
59
+ | Record<string, unknown>;
60
+
61
+ /**
62
+ * Context object containing parsed request body.
63
+ * @typedef {Object} CTXBody
64
+ * @property {ParsedBody} body - Parsed request body data
65
+ */
66
+ export type CTXBody = {
67
+ body: ParsedBody;
68
+ };
69
+
70
+ /**
71
+ * Supported media types for request body parsing.
72
+ * @typedef {"application/x-www-form-urlencoded"|"application/json"|"text/plain"} SupportedBodyMediaTypes
73
+ */
74
+ export type SupportedBodyMediaTypes =
75
+ | "application/x-www-form-urlencoded"
76
+ | "application/json"
77
+ | "text/plain";
78
+
79
+ /**
80
+ * Creates middleware that parses the request body based on Content-Type.
81
+ * Supports url-encoded forms, JSON, and plain text.
82
+ * @param {Object} [options] - Configuration options for body parsing
83
+ * @param {SupportedBodyMediaTypes|SupportedBodyMediaTypes[]} [options.accept] - Media types to accept (defaults to all supported)
84
+ * @param {number} [options.maxSize] - Maximum body size in bytes (defaults to 1MB)
85
+ * @param {number} [options.once] - Do not parse if parsed already. checks `ctx.body`
86
+ * @param {number} [options.clone] - Clone request before parsing it. Useful for forwarding.
87
+ * @returns {Function} A middleware function that adds parsed body to context.body
88
+ * @throws {Response} Returns a 415 response if content-type is not accepted
89
+ * @throws {Response} Returns a 413 response if body exceeds maxSize
90
+ * @throws {Response} Returns a 400 response if body is malformed
91
+ */
92
+ export const parseBody = <XContext = {}>(options?: {
93
+ accept?: SupportedBodyMediaTypes | SupportedBodyMediaTypes[]; // defaults to all
94
+ maxSize?: number; // in bytes
95
+ once?: boolean;
96
+ clone?: boolean;
97
+ }): FreeHandler<XContext & CTXBody> => {
98
+ const accept = options?.accept
99
+ ? Array.isArray(options.accept)
100
+ ? options.accept
101
+ : [options.accept]
102
+ : ([
103
+ "application/x-www-form-urlencoded",
104
+ "application/json",
105
+ "text/plain",
106
+ ] as string[]);
107
+ const maxSize = options?.maxSize ?? 1024 * 1024; // Default 1MB
108
+ const once = options?.once;
109
+ const clone = options?.clone;
110
+ return async (_req: Request, ctx: RouterContext<XContext & CTXBody>) => {
111
+ if (once && ctx.body) return;
112
+ const contentType = _req.headers.get("content-type")?.split(";", 2)[0];
113
+ if (!(contentType && accept.includes(contentType))) {
114
+ await _req.body?.cancel().catch(() => {});
115
+ return status(415);
116
+ }
117
+ const req = clone ? _req.clone() : _req;
118
+ try {
119
+ const contentLengthHeader = req.headers.get("content-length");
120
+ const contentLength = contentLengthHeader
121
+ ? parseInt(contentLengthHeader)
122
+ : undefined;
123
+ if (contentLength === 0) {
124
+ ctx.body = {};
125
+ return;
126
+ }
127
+ if (contentLength !== undefined && contentLength > maxSize) {
128
+ await _req.body?.cancel().catch(() => {});
129
+ return status(413);
130
+ }
131
+ switch (contentType) {
132
+ case "application/x-www-form-urlencoded": {
133
+ const body = await req.formData();
134
+ ctx.body = Object.fromEntries(body.entries());
135
+ break;
136
+ }
137
+ case "application/json": {
138
+ const body = await req.json();
139
+ if (Array.isArray(body)) {
140
+ ctx.body = { values: body };
141
+ } else if (body === undefined) {
142
+ ctx.body = {};
143
+ } else if (body === null) {
144
+ ctx.body = { value: null };
145
+ } else if (typeof body === "object") {
146
+ ctx.body = body;
147
+ } else {
148
+ ctx.body = { value: body };
149
+ }
150
+ break;
151
+ }
152
+ case "text/plain": {
153
+ const text = await req.text();
154
+ ctx.body = { text };
155
+ break;
156
+ }
157
+ default:
158
+ ctx.body = {};
159
+ break;
160
+ }
161
+ } catch (error) {
162
+ await _req.body?.cancel().catch(() => {});
163
+ return status(400, "Malformed Payload");
164
+ }
165
+ };
166
+ };
167
+
168
+ /**
169
+ * Creates a rate limiting middleware using token bucket algorithm.
170
+ * Supports both fixed interval refill and continuous rate-based refill.
171
+ *
172
+ * @template Context - Must extend RouterContext & CTXAddress
173
+ * @param {Object} config - Rate limiting configuration
174
+ * @param {Function} config.key - Function to generate cache key from request and context
175
+ * @param {number} [config.refillInterval] - Fixed interval in milliseconds for token refill
176
+ * @param {number} [config.refillRate] - Continuous refill rate in tokens per second (or custom denominator)
177
+ * @param {number} config.maxTokens - Maximum number of tokens in the bucket
178
+ * @param {number} [config.refillTimeSecondsDenominator=1000] - Denominator for time calculations (default: 1000 = seconds)
179
+ * @param {Function} [config.now=Date.now] - Function returning current timestamp in milliseconds
180
+ * @param {CacheConfig<string, any>} [config.cacheConfig] - Configuration for the underlying cache
181
+ * @param {boolean} [config.setXRateLimitHeaders=false] - Whether to set X-RateLimit headers in response
182
+ * @returns {Function} Middleware function that enforces rate limits
183
+ *
184
+ * @example
185
+ * // Fixed interval rate limiting (10 requests per minute)
186
+ * const rateLimiter = limitRate({
187
+ * key: (req, ctx) => ctx.address.address, // IP-based limiting
188
+ * refillInterval: 60 * 1000, // 1 minute
189
+ * refillRate: 10, // 10 tokens per interval
190
+ * maxTokens: 10,
191
+ * setXRateLimitHeaders: true
192
+ * });
193
+ *
194
+ * @example
195
+ * // Continuous rate limiting (100 requests per hour)
196
+ * const rateLimiter = limitRate({
197
+ * key: (req, ctx) => req.headers.get('x-user-id') || 'anonymous',
198
+ * refillRate: 100 / (60 * 60), // 100 tokens per hour
199
+ * maxTokens: 100,
200
+ * refillTimeSecondsDenominator: 1 // Use seconds as time unit
201
+ * });
202
+ *
203
+ * @throws {Error} If neither refillInterval nor refillRate is provided
204
+ */
205
+ export const limitRate = <XContext = {}>(config: {
206
+ key: (req: Request, ctx: RouterContext<XContext & CTXAddress>) => string;
207
+ refillInterval?: number;
208
+ refillRate?: number;
209
+ maxTokens: number;
210
+ refillTimeSecondsDenominator?: number;
211
+ now?: () => number;
212
+ cacheConfig?: CacheConfig<string, any>;
213
+ setXRateLimitHeaders?: boolean;
214
+ endHere?: boolean;
215
+ }): FreeHandler<XContext & CTXAddress> => {
216
+ const {
217
+ key,
218
+ refillInterval,
219
+ refillRate,
220
+ maxTokens,
221
+ refillTimeSecondsDenominator = 1000,
222
+ now = () => Date.now(),
223
+ cacheConfig = {
224
+ now: () => Date.now(),
225
+ // defaultMaxAgse: Time.for(10).seconds._ms,
226
+ defaultMaxAge: Time.for(1).hour._ms,
227
+ cleanupInterval: Time.every(10).minutes._ms,
228
+ onGetMiss: (cache: Cache<string, any>, key, reason) => {
229
+ cache.set(key, { tokens: maxTokens, lastRefill: now() });
230
+ return true;
231
+ },
232
+ },
233
+ setXRateLimitHeaders = false,
234
+ endHere = false,
235
+ } = config;
236
+ type CacheEntry = {
237
+ tokens: number;
238
+ lastRefill: number;
239
+ };
240
+ const rateLimits: Cache<string, CacheEntry> = new Cache(cacheConfig);
241
+ if (refillInterval) {
242
+ return function (req: Request, ctx: RouterContext<XContext & CTXAddress>) {
243
+ const id = key(req, ctx);
244
+ const entry = rateLimits.get(id)?.value as CacheEntry;
245
+ const timeElapsed = now() - entry.lastRefill;
246
+ if (timeElapsed >= refillInterval) {
247
+ if (refillRate) {
248
+ const newTokens =
249
+ entry.tokens +
250
+ refillRate * Math.floor(timeElapsed / refillInterval);
251
+ entry.tokens = Math.min(newTokens, maxTokens);
252
+ entry.lastRefill = now();
253
+ } else {
254
+ entry.tokens = maxTokens;
255
+ entry.lastRefill = now();
256
+ }
257
+ }
258
+ if (entry.tokens <= 0) {
259
+ ctx.headers.set(
260
+ "Retry-After",
261
+ Math.ceil(
262
+ (refillInterval - timeElapsed) / refillTimeSecondsDenominator,
263
+ ).toFixed(),
264
+ );
265
+ return status(429);
266
+ } else {
267
+ entry.tokens--;
268
+ }
269
+ if (setXRateLimitHeaders) {
270
+ ctx.headers.set("X-RateLimit-Limit", maxTokens.toFixed());
271
+ ctx.headers.set("X-RateLimit-Remaining", entry.tokens.toFixed());
272
+ }
273
+ if (endHere) return true;
274
+ };
275
+ } else if (refillRate) {
276
+ return function (req: Request, ctx: RouterContext<XContext & CTXAddress>) {
277
+ const id = key(req, ctx);
278
+ const entry = rateLimits.get(id)?.value as CacheEntry;
279
+ const timeElapsed = now() - entry.lastRefill;
280
+ const newTokens =
281
+ entry.tokens +
282
+ (refillRate * timeElapsed) / refillTimeSecondsDenominator;
283
+ entry.tokens = Math.min(newTokens, maxTokens);
284
+ entry.lastRefill = now();
285
+ if (entry.tokens <= 0) {
286
+ ctx.headers.set("Retry-After", Math.ceil(1 / refillRate).toFixed());
287
+ return status(429);
288
+ } else {
289
+ entry.tokens--;
290
+ }
291
+ if (setXRateLimitHeaders) {
292
+ ctx.headers.set("X-RateLimit-Limit", maxTokens.toFixed());
293
+ ctx.headers.set(
294
+ "X-RateLimit-Remaining",
295
+ Math.max(0, entry.tokens).toFixed(),
296
+ );
297
+ }
298
+ if (endHere) return true;
299
+ };
300
+ }
301
+ throw new Error(
302
+ "LIMIT-RATE: `refillInterval` or `refillRate` or both should be set",
303
+ );
304
+ };
305
+
306
+ /**
307
+ * Creates a CORS (Cross-Origin Resource Sharing) middleware.
308
+ * Supports preflight requests and configurable CORS headers.
309
+ *
310
+ * @template Context - Must extend RouterContext
311
+ * @param {Object} [config] - CORS configuration
312
+ * @param {string|string[]|"*"} [config.origins="*"] - Allowed origins (wildcard "*", single origin, or array)
313
+ * @param {HttpMethod[]} [config.methods=["GET","HEAD","PUT","PATCH","POST","DELETE"]] - Allowed HTTP methods
314
+ * @param {string[]} [config.allowedHeaders=["Content-Type","Authorization"]] - Allowed request headers
315
+ * @param {string[]} [config.exposedHeaders] - Headers exposed to the browser
316
+ * @param {boolean} [config.credentials=false] - Allow credentials (cookies, authorization headers)
317
+ * @param {number} [config.maxAge=86400] - Maximum age for preflight cache in seconds
318
+ * @param {boolean} [config.varyOrigin=false] - Add Vary: Origin header for caching
319
+ * @param {boolean} [options.endHere=false] - If true, stops only pipeline flow per handler type after success.
320
+ * @returns {Function} Middleware function that handles CORS headers
321
+ *
322
+ * @example
323
+ * // Basic CORS with all defaults
324
+ * const corsMiddleware = cors();
325
+ *
326
+ * @example
327
+ * // Specific origins with credentials
328
+ * const corsMiddleware = cors({
329
+ * origins: ["https://example.com", "https://api.example.com"],
330
+ * credentials: true,
331
+ * methods: ["GET", "POST", "PUT", "DELETE"],
332
+ * allowedHeaders: ["Content-Type", "Authorization", "X-Custom-Header"]
333
+ * });
334
+ *
335
+ * @throws {Error} If credentials is true with wildcard origin ("*")
336
+ */
337
+ export const cors = <XContext = {}>(config?: {
338
+ origins: "*" | string | string[];
339
+ methods?: HttpMethod[] | null;
340
+ allowedHeaders?: string[] | null;
341
+ exposedHeaders?: string[] | null;
342
+ credentials?: boolean | null;
343
+ maxAge?: number | null;
344
+ varyOrigin?: boolean;
345
+ endHere?: boolean;
346
+ }): FreeHandler<XContext> => {
347
+ const {
348
+ origins = "*",
349
+ methods = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
350
+ allowedHeaders = ["Content-Type", "Authorization"],
351
+ exposedHeaders,
352
+ credentials = false,
353
+ maxAge = 86400,
354
+ varyOrigin = false,
355
+ endHere = false,
356
+ } = config ?? {};
357
+ const globOrigin = origins === "*" ? "*" : null;
358
+ const originsSet = new Set(
359
+ typeof origins !== "string" ? origins : origins !== "*" ? [] : [origins],
360
+ );
361
+ return function (req: Request, ctx: RouterContext<XContext>) {
362
+ const origin = req.headers.get("origin");
363
+ let corsOrigin: string | null = null;
364
+ if (!origin) {
365
+ if (endHere) return true;
366
+ return;
367
+ }
368
+ if (globOrigin) {
369
+ corsOrigin = "*";
370
+ } else {
371
+ corsOrigin = originsSet.has(origin) ? origin : null;
372
+ }
373
+ if (!corsOrigin) {
374
+ if (varyOrigin) ctx.headers.append("Vary", "Origin");
375
+ if (endHere) return true;
376
+ return;
377
+ }
378
+ ctx.headers.set("Access-Control-Allow-Origin", corsOrigin);
379
+ if (credentials) {
380
+ if (corsOrigin === "*")
381
+ throw new Error("CORS: Cannot use credentials with wildcard origin");
382
+ ctx.headers.set("Access-Control-Allow-Credentials", "true");
383
+ }
384
+ if (exposedHeaders && exposedHeaders.length > 0) {
385
+ ctx.headers.set(
386
+ "Access-Control-Expose-Headers",
387
+ exposedHeaders.join(", "),
388
+ );
389
+ }
390
+ if (varyOrigin) {
391
+ ctx.headers.append("Vary", "Origin");
392
+ }
393
+ if (req.method === "OPTIONS") {
394
+ if (methods && methods.length > 0) {
395
+ ctx.headers.set("Access-Control-Allow-Methods", methods.join(", "));
396
+ }
397
+ if (allowedHeaders && allowedHeaders.length > 0) {
398
+ ctx.headers.set(
399
+ "Access-Control-Allow-Headers",
400
+ allowedHeaders.join(", "),
401
+ );
402
+ }
403
+ if (maxAge) {
404
+ ctx.headers.set("Access-Control-Max-Age", maxAge.toString());
405
+ }
406
+ return status(204, null);
407
+ }
408
+ if (endHere) return true;
409
+ };
410
+ };
411
+
412
+ /**
413
+ * Context type for Basic Authentication middleware.
414
+ * @template {string} [prop="basicAuth"] - Property name to store auth data in context
415
+ * @typedef {RouterContext & {[K in prop]?: {username: string; role: string} & Record<string, any>}} CTXBasicAuth
416
+ */
417
+ export type CTXBasicAuth<prop extends string = "basicAuth"> = RouterContext<{
418
+ [K in prop]?: {
419
+ username: string;
420
+ role: string;
421
+ } & Record<string, any>;
422
+ }>;
423
+
424
+ /**
425
+ * Represents an authenticated user.
426
+ */
427
+ export interface Auth {
428
+ /** Unique identifier for the user */
429
+ id: string;
430
+ /** Role assigned to the user (e.g., "admin", "user") */
431
+ role: string;
432
+ }
433
+
434
+ /**
435
+ * Context extension that includes authentication information.
436
+ */
437
+ export interface CTXAuth {
438
+ /** Authenticated user details */
439
+ auth: Auth;
440
+ }
441
+
442
+ /**
443
+ * auth context parser for authenticate middleware
444
+ */
445
+ export type ParseAuthFn<XContext = {}> = (
446
+ req: Request,
447
+ ctx: RouterContext<Partial<CTXAuth> & XContext>,
448
+ ) => Promise<Auth | Error | null | undefined> | Auth | Error | null | undefined;
449
+
450
+ /**
451
+ * Middleware to authenticate a request.
452
+ *
453
+ * @template XContext - Additional context type to merge with CTXAuth.
454
+ * @param {Object} [options] - Configuration options.
455
+ * @param {ParseAuthFn}[options.parseAuth] - Function to extract authentication info from the request.
456
+ * Should return an `Auth` object if valid, `Error` if invalid, or `null/undefined` if missing.
457
+ * @param {boolean} [options.endHere=false] - If true, stops only pipeline flow per handler type after success.
458
+ * @param {boolean} [options.checkOnly=false] - If true, only checks authentication without returning a response.
459
+ *
460
+ * @returns {FreeHandler<Partial<CTXAuth>&XContext>} A handler that sets `ctx.auth` if authentication succeeds,
461
+ * otherwise returns a `401 Unauthorized` or with error message if available response (unless `checkOnly` is true).
462
+ */
463
+ export const authenticate = <XContext = {}>({
464
+ parseAuth,
465
+ endHere = false,
466
+ checkOnly = false,
467
+ }: {
468
+ parseAuth: ParseAuthFn<XContext>;
469
+ endHere?: boolean;
470
+ checkOnly?: boolean;
471
+ }): FreeHandler<Partial<CTXAuth> & XContext> => {
472
+ return async function (req, ctx) {
473
+ let auth = parseAuth(req, ctx);
474
+ if (auth instanceof Promise) auth = await auth;
475
+ if (!auth) {
476
+ return checkOnly ? false : status(401);
477
+ } else if (auth instanceof Error) {
478
+ return checkOnly ? false : status(401, auth.message ?? undefined);
479
+ }
480
+ ctx.auth = auth;
481
+ if (endHere) return true;
482
+ };
483
+ };
484
+
485
+ /**
486
+ * Middleware to authorize a request based on role or permissions.
487
+ *
488
+ * @template XContext - Additional context type to merge with CTXAuth.
489
+ * @param {Object} [options] - Configuration options.
490
+ * @param {(role: string) => boolean}[options.allowRole] - Function to check if a role is allowed.
491
+ * @param {(role: string) => boolean}[options.forbidRole] - Function to check if a role is forbidden.
492
+ * @param {string[]}[options.forPermissions] - List of permissions required for access.
493
+ * @param {(permission: string,role: string) => boolean|null|undefined}[options.hasPermission] - Function to check if a role has a given permission.
494
+ * Required if `forPermissions` is provided.
495
+ * @param {boolean} [options.endHere=false] - If true, stops only pipeline flow per handler type after success.
496
+ *
497
+ * @returns {FreeHandler<Partial<CTXAuth>&XContext>} A handler that checks `ctx.auth` and enforces role/permission rules.
498
+ * Returns `401 Unauthorized` if no auth is present, or `403 Forbidden` if checks fail.
499
+ * Throws an error if `forPermissions` is set without `hasPermission`.
500
+ *
501
+ */
502
+ export const authorize = <XContext = {}>({
503
+ allowRole,
504
+ forbidRole,
505
+ forPermissions,
506
+ hasPermission,
507
+ endHere = false,
508
+ }: {
509
+ allowRole?: (role: string) => boolean;
510
+ forbidRole?: (role: string) => boolean;
511
+ forPermissions?: string[];
512
+ hasPermission?: (
513
+ permission: string,
514
+ role: string,
515
+ ) => boolean | null | undefined;
516
+ endHere?: boolean;
517
+ }): FreeHandler<Partial<CTXAuth> & XContext> => {
518
+ if (forPermissions && !hasPermission) {
519
+ throw new Error(
520
+ "authorize middleware 'forPermissions' require 'hasPermission'",
521
+ );
522
+ }
523
+ return (req, { auth }) => {
524
+ if (!auth) {
525
+ return status(401);
526
+ }
527
+ if (allowRole && !allowRole(auth.role)) {
528
+ return status(403);
529
+ }
530
+ if (forbidRole && forbidRole(auth.role)) {
531
+ return status(403);
532
+ }
533
+ if (forPermissions && hasPermission) {
534
+ const permitted = forPermissions.some((permission) =>
535
+ hasPermission(permission, auth.role),
536
+ );
537
+ if (!permitted) return status(403);
538
+ }
539
+ if (endHere) return true;
540
+ };
541
+ };
542
+
543
+ /**
544
+ *
545
+ * @template XContext - Additional context type to merge with CTXAuth.
546
+ * @param {Object} [options] - Configuration options.
547
+ * @param {Map<string,{ password: string; role: string } & Record<string, any>>} [options.credentials] - A map containing the entries of the credentials identified and indexed by username
548
+ * @param {"base64" | "raw"} [options.type="base64"] - The token format/type
549
+ * @param {":" | " "} [options.separator=":"] - The separator for username and password
550
+ * @param {string} [options.realm="Protected"] - The realm of the basic authentication
551
+ * @returns
552
+ */
553
+ export const basicAuth = <XContext = {}>({
554
+ credentials,
555
+ type = "base64",
556
+ separator = ":",
557
+ realm = "Protected",
558
+ }: {
559
+ credentials: Map<
560
+ string,
561
+ { password: string; role: string } & Record<string, any>
562
+ >;
563
+ type?: "base64" | "raw";
564
+ separator?: ":" | " ";
565
+ realm?: string;
566
+ }): {
567
+ (req: Request, ctx: RouterContext<CTXAuth & XContext>): Auth | Error;
568
+ } => {
569
+ return (req: Request, ctx: RouterContext<CTXAuth & XContext>) => {
570
+ const authorization = req.headers.get("authorization");
571
+ ctx.headers.set(
572
+ "WWW-Authenticate",
573
+ `Basic realm="${realm}", charset="UTF-8"`,
574
+ );
575
+ if (!authorization) return new Error("Missing authorization header");
576
+ const [scheme, creds] = authorization.split(" ", 2);
577
+ if (scheme.toLowerCase() !== "basic" || !creds)
578
+ return new Error("Invalid authorization scheme");
579
+ let xcreds = creds;
580
+ if (type === "base64") {
581
+ try {
582
+ xcreds = atob(creds);
583
+ } catch {
584
+ return new Error("Bad authorization token");
585
+ }
586
+ }
587
+ const [username, password] = xcreds.split(separator, 2);
588
+ if (!username || !password) return new Error("Bad authorization token");
589
+ const user = credentials.get(username);
590
+ if (!user || password !== user.password)
591
+ return new Error("Invalid credentials");
592
+ const auth = {
593
+ id: username,
594
+ role: user.role,
595
+ };
596
+ return auth;
597
+ };
598
+ };
599
+
600
+ /**
601
+ * Creates a Basic Authentication middleware.
602
+ * Supports RFC 7617 Basic Authentication scheme.
603
+ *
604
+ * @template Context - Must extend CTXBasicAuth
605
+ * @template {string} prop - Property name to store auth data in context
606
+ * @param {Object} config - Basic Authentication configuration
607
+ * @param {Map<string, {pass: string} & Record<string, any>>} config.credentials - Map of usernames to user data (must include 'pass')
608
+ * @param {"base64"|"raw"} [config.type="base64"] - Credential encoding type
609
+ * @param {":"|" "} [config.separator=":"] - Separator between username and password
610
+ * @param {string} [config.realm="Protected"] - Authentication realm
611
+ * @param {prop} [config.ctxProp="basicAuth"] - Context property name for auth data
612
+ * @returns {Function} Middleware function that validates Basic Authentication
613
+ *
614
+ * @example
615
+ * // Simple username/password authentication
616
+ * const users = new Map();
617
+ * users.set("admin", { pass: "secret123", role: "admin", permissions: ["read", "write"] });
618
+ * users.set("user", { pass: "password", role: "user", permissions: ["read"] });
619
+ *
620
+ * const basicAuth = parseAuthBasic({
621
+ * credentials: users,
622
+ * realm: "My API",
623
+ * ctxProp: "user" // Store in ctx.user instead of ctx.basicAuth
624
+ * });
625
+ */
626
+ export const parseAuthBasic = <
627
+ XContext = {},
628
+ prop extends string = "basicAuth",
629
+ >({
630
+ credentials,
631
+ type = "raw",
632
+ separator = ":",
633
+ realm = "Protected",
634
+ ctxProp = "basicAuth" as prop,
635
+ endHere = false,
636
+ }: {
637
+ credentials: Map<
638
+ string,
639
+ { password: string; role: string } & Record<string, any>
640
+ >;
641
+ type?: "raw" | "base64";
642
+ separator?: ":" | " ";
643
+ realm?: string;
644
+ ctxProp?: prop;
645
+ endHere?: boolean;
646
+ }): FreeHandler<XContext & CTXBasicAuth> => {
647
+ return (req: Request, ctx: RouterContext<XContext & CTXBasicAuth>) => {
648
+ const authorization = req.headers.get("authorization");
649
+ ctx.headers.set(
650
+ "WWW-Authenticate",
651
+ `Basic realm="${realm}", charset="UTF-8"`,
652
+ );
653
+ if (!authorization) return status(401);
654
+ const [scheme, creds] = authorization.split(" ", 2);
655
+ if (scheme.toLowerCase() !== "basic" || !creds) return status(401);
656
+ let xcreds = creds;
657
+ if (type === "base64") {
658
+ try {
659
+ xcreds = atob(creds);
660
+ } catch {
661
+ return status(401);
662
+ }
663
+ }
664
+ const [username, password] = xcreds.split(separator, 2);
665
+ if (!username || !password) return status(401);
666
+ const user = credentials.get(username);
667
+ if (!user || password !== user.password) return status(401);
668
+ (ctx as { [K in prop]: any })[ctxProp] = {
669
+ username,
670
+ role: user.role,
671
+ };
672
+ if (endHere) return true;
673
+ };
674
+ };
675
+
676
+ /**
677
+ * @deprecated use `parseAuthBasic`
678
+ */
679
+ export const authBasic = parseAuthBasic;
680
+ /**
681
+ * Context type for API Key Authentication middleware.
682
+ * @template {string} [prop="apiKeyAuth"] - Property name to store auth data in context
683
+ * @typedef {RouterContext & {[K in prop]?: {apiKey: string} & Record<string, any>}} CTXAPIKeyAuth
684
+ */
685
+ export type CTXAPIKeyAuth<prop extends string = "apiKeyAuth"> = RouterContext<{
686
+ [K in prop]?: {
687
+ apiKey: string;
688
+ } & Record<string, any>;
689
+ }>;
690
+
691
+ /**
692
+ * Creates an API Key Authentication middleware.
693
+ * Validates API keys from X-API-Key header.
694
+ *
695
+ * @template Context - Must extend CTXAPIKeyAuth
696
+ * @template {string} prop - Property name to store auth data in context
697
+ * @param {Object} config - API Key Authentication configuration
698
+ * @param {Function} config.verify - Function to verify API key (returns boolean)
699
+ * @param {prop} [config.ctxProp="apiKeyAuth"] - Context property name for auth data
700
+ * @returns {Function} Middleware function that validates API keys
701
+ *
702
+ * @example
703
+ * // API key validation with database lookup
704
+ * const apiKeys = new Set(["abc123", "def456", "ghi789"]);
705
+ *
706
+ * const apiKeyAuth = parseAuthAPIKey({
707
+ * verify: (apiKey) => apiKeys.has(apiKey),
708
+ * ctxProp: "apiClient" // Store in ctx.apiClient
709
+ * });
710
+ *
711
+ * @example
712
+ * // API key with additional validation
713
+ * const apiKeyAuth = parseAuthAPIKey({
714
+ * verify: (apiKey) => {
715
+ * // Validate format
716
+ * if (!apiKey.startsWith("sk_")) return false;
717
+ *
718
+ * // Check against database
719
+ * return db.apiKeys.isValid(apiKey);
720
+ * }
721
+ * });
722
+ */
723
+ export const parseAuthAPIKey = <
724
+ XContext = {},
725
+ prop extends string = "apiKeyAuth",
726
+ >({
727
+ verify,
728
+ ctxProp = "apiKeyAuth" as prop,
729
+ endHere = false,
730
+ }: {
731
+ verify: (apiKey: string) => boolean;
732
+ ctxProp?: prop;
733
+ endHere?: boolean;
734
+ }): FreeHandler<XContext & CTXAPIKeyAuth> => {
735
+ return (req: Request, ctx: RouterContext<XContext & CTXAPIKeyAuth>) => {
736
+ const apiKey = req.headers.get("X-API-Key");
737
+ if (!apiKey || !verify(apiKey)) return status(401);
738
+ (ctx as { [K in prop]: any })[ctxProp] = {
739
+ apiKey,
740
+ };
741
+ if (endHere) return true;
742
+ };
743
+ };
744
+
745
+ /**
746
+ * Context type for JWT Authentication middleware.
747
+ * @template {JwtPayload<{}>} Payload - JWT payload type
748
+ * @template {string} [prop="jwtAuth"] - Property name to store auth data in context
749
+ * @typedef {RouterContext & {[K in prop]?: {jwt: JWT<Payload>; token: string; payload: Payload} & Record<string, any>}} CTXJWTAuth
750
+ */
751
+ export type CTXJWTAuth<
752
+ Payload extends JwtPayload<{}>,
753
+ prop extends string = "jwtAuth",
754
+ > = RouterContext<{
755
+ [K in prop]?: {
756
+ jwt: JWT<Payload>;
757
+ token: string;
758
+ payload: Payload;
759
+ } & Record<string, any>;
760
+ }>;
761
+
762
+ /**
763
+ * @deprecated use `parseAuthAPIKey`
764
+ */
765
+ export const authAPIKey = parseAuthAPIKey;
766
+ /**
767
+ * Creates a JWT (JSON Web Token) Authentication middleware.
768
+ * Validates Bearer tokens from Authorization header.
769
+ *
770
+ * @template {JwtPayload<{}>} Payload - JWT payload type
771
+ * @template Context - Must extend CTXJWTAuth<Payload, prop>
772
+ * @template {string} prop - Property name to store auth data in context
773
+ * @param {Object} config - JWT Authentication configuration
774
+ * @param {JWT<Payload>} config.jwt - JWT instance for verification
775
+ * @param {Function} [config.validate] - Additional payload validation function
776
+ * @param {JwtVerifyOptions} [config.verifyOptions] - JWT verification options
777
+ * @param {prop} [config.ctxProp="jwtAuth"] - Context property name for auth data
778
+ * @returns {Function} Middleware function that validates JWT tokens
779
+ *
780
+ * @example
781
+ * // Simple JWT authentication
782
+ * const jwt = new JWT({ secret: process.env.JWT_SECRET });
783
+ *
784
+ * const jwtAuth = parseAuthJWT({
785
+ * jwt,
786
+ * validate: (payload) => payload.exp > Date.now() / 1000, // Check expiration
787
+ * ctxProp: "auth" // Store in ctx.auth
788
+ * });
789
+ *
790
+ * @example
791
+ * // JWT with custom payload validation
792
+ * interface MyPayload extends JwtPayload<{}> {
793
+ * userId: string;
794
+ * role: string;
795
+ * permissions: string[];
796
+ * }
797
+ *
798
+ * const jwt = new JWT<MyPayload>({ secret: process.env.JWT_SECRET });
799
+ *
800
+ * const jwtAuth = parseAuthJWT<MyPayload>({
801
+ * jwt,
802
+ * validate: (payload) => {
803
+ * // Custom business logic
804
+ * if (!payload.userId) return false;
805
+ * if (payload.role !== "admin") return false;
806
+ * return true;
807
+ * },
808
+ * verifyOptions: {
809
+ * algorithms: ["HS256"],
810
+ * maxAge: "2h"
811
+ * }
812
+ * });
813
+ */
814
+ export const parseAuthJWT = <
815
+ Payload extends JwtPayload<{}>,
816
+ XContext extends CTXJWTAuth<Payload, prop>,
817
+ prop extends string = "jwtAuth",
818
+ >({
819
+ jwt,
820
+ validate,
821
+ verifyOptions,
822
+ ctxProp = "jwtAuth" as prop,
823
+ endHere = false,
824
+ }: {
825
+ jwt: JWT<Payload>;
826
+ validate?: (payload: Payload) => boolean;
827
+ verifyOptions?: JwtVerifyOptions;
828
+ ctxProp?: prop;
829
+ endHere?: boolean;
830
+ }): FreeHandler<XContext & CTXJWTAuth<Payload>> => {
831
+ return (req: Request, ctx: RouterContext<XContext & CTXJWTAuth<Payload>>) => {
832
+ const authorization = req.headers.get("authorization");
833
+ if (!authorization) return status(401);
834
+ const [scheme, token] = authorization.split(" ", 2);
835
+ if (scheme.toLowerCase() !== "bearer" || !token) return status(401);
836
+ const result = jwt.verifySync(token, verifyOptions);
837
+ if (!result.payload) return status(401, result.error?.message);
838
+ if (validate && !validate(result.payload)) return status(401);
839
+ (ctx as { [K in prop]: any })[ctxProp] = {
840
+ jwt,
841
+ token,
842
+ payload: result.payload,
843
+ };
844
+ if (endHere) return true;
845
+ };
846
+ };
847
+
848
+ /**
849
+ * @deprecated use `parseAuthJWT`
850
+ */
851
+ export const authJWT = parseAuthJWT;