@bepalo/router 1.11.32 → 1.12.34

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