@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.
- package/dist/cjs/framework.d.ts +2 -4
- package/dist/cjs/framework.d.ts.map +1 -1
- package/dist/cjs/framework.js +4 -6
- package/dist/cjs/framework.js.map +1 -1
- package/dist/cjs/helpers.d.ts +2 -2
- package/dist/cjs/helpers.d.ts.map +1 -1
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/helpers.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -5
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +5 -5
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/middlewares.d.ts +2 -2
- package/dist/cjs/middlewares.d.ts.map +1 -1
- package/dist/cjs/middlewares.js +24 -24
- package/dist/cjs/middlewares.js.map +1 -1
- package/dist/cjs/router.d.ts +2 -2
- package/dist/cjs/router.d.ts.map +1 -1
- package/dist/cjs/router.js +8 -8
- package/dist/cjs/router.js.map +1 -1
- package/dist/cjs/types.d.ts +1 -1
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/upload-stream.d.ts +1 -1
- package/dist/cjs/upload-stream.d.ts.map +1 -1
- package/dist/cjs/upload-stream.js +7 -7
- package/dist/cjs/upload-stream.js.map +1 -1
- package/dist/framework.d.ts +2 -4
- package/dist/framework.d.ts.map +1 -1
- package/dist/framework.js +4 -6
- package/dist/framework.js.map +1 -1
- package/dist/helpers.d.ts +2 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +1 -1
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/middlewares.d.ts +2 -2
- package/dist/middlewares.d.ts.map +1 -1
- package/dist/middlewares.js +24 -24
- package/dist/middlewares.js.map +1 -1
- package/dist/router.d.ts +2 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +8 -8
- package/dist/router.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/upload-stream.d.ts +1 -1
- package/dist/upload-stream.d.ts.map +1 -1
- package/dist/upload-stream.js +7 -7
- package/dist/upload-stream.js.map +1 -1
- package/package.json +8 -1
- package/src/framework.deno.ts +194 -0
- package/src/framework.ts +197 -0
- package/src/helpers.ts +829 -0
- package/src/index.ts +5 -0
- package/src/list.ts +462 -0
- package/src/middlewares.deno.ts +851 -0
- package/src/middlewares.ts +851 -0
- package/src/router.ts +993 -0
- package/src/tree.ts +139 -0
- package/src/types.ts +197 -0
- package/src/upload-stream.ts +661 -0
- package/dist/cjs/framework.deno.d.ts +0 -31
- package/dist/cjs/framework.deno.d.ts.map +0 -1
- package/dist/cjs/framework.deno.js +0 -245
- package/dist/cjs/framework.deno.js.map +0 -1
- package/dist/framework.deno.d.ts +0 -31
- package/dist/framework.deno.d.ts.map +0 -1
- package/dist/framework.deno.js +0 -245
- 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;
|