skyguard-js 1.2.1 → 1.2.3
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/README.md +11 -680
- package/dist/app.d.ts +14 -9
- package/dist/app.js +37 -30
- package/dist/http/context.d.ts +115 -0
- package/dist/http/context.js +147 -0
- package/dist/http/httpAdapter.d.ts +4 -4
- package/dist/http/index.d.ts +3 -1
- package/dist/http/index.js +5 -1
- package/dist/http/logger.d.ts +9 -2
- package/dist/http/logger.js +49 -1
- package/dist/http/nodeNativeHttp.d.ts +4 -4
- package/dist/http/nodeNativeHttp.js +11 -4
- package/dist/http/request.d.ts +4 -0
- package/dist/http/request.js +8 -0
- package/dist/http/response.d.ts +21 -2
- package/dist/http/response.js +30 -2
- package/dist/http/webHttp.d.ts +19 -0
- package/dist/http/webHttp.js +95 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middlewares/cors.d.ts +10 -4
- package/dist/middlewares/cors.js +35 -18
- package/dist/middlewares/csrf.js +33 -33
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/rateLimiter.d.ts +58 -6
- package/dist/middlewares/rateLimiter.js +149 -40
- package/dist/middlewares/session.js +4 -4
- package/dist/parsers/contentParserManager.d.ts +4 -2
- package/dist/parsers/contentParserManager.js +22 -3
- package/dist/routing/routeResolveFunc.d.ts +10 -0
- package/dist/routing/routeResolveFunc.js +21 -0
- package/dist/routing/router.d.ts +16 -10
- package/dist/routing/router.js +32 -25
- package/dist/routing/routerGroup.d.ts +10 -5
- package/dist/routing/routerGroup.js +11 -10
- package/dist/server/bunRuntimeServer.d.ts +7 -0
- package/dist/server/bunRuntimeServer.js +28 -0
- package/dist/server/createRuntimeServer.d.ts +4 -0
- package/dist/server/createRuntimeServer.js +23 -0
- package/dist/server/denoRuntimeServer.d.ts +7 -0
- package/dist/server/denoRuntimeServer.js +27 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +19 -0
- package/dist/server/modulePath.d.ts +6 -0
- package/dist/server/modulePath.js +18 -0
- package/dist/server/nodeRuntimeServer.d.ts +7 -0
- package/dist/server/nodeRuntimeServer.js +21 -0
- package/dist/server/runtimeDetector.d.ts +4 -0
- package/dist/server/runtimeDetector.js +15 -0
- package/dist/server/types.d.ts +11 -0
- package/dist/server/types.js +2 -0
- package/dist/sessions/index.d.ts +2 -2
- package/dist/storage/storage.d.ts +3 -3
- package/dist/storage/storage.js +7 -7
- package/dist/storage/types.d.ts +5 -5
- package/dist/storage/uploader.d.ts +9 -9
- package/dist/storage/uploader.js +62 -62
- package/dist/types/index.d.ts +11 -10
- package/dist/validators/index.d.ts +2 -2
- package/dist/validators/rules/index.d.ts +5 -5
- package/dist/validators/validationSchema.js +8 -8
- package/package.json +2 -2
- package/dist/helpers/http.d.ts +0 -95
- package/dist/helpers/http.js +0 -112
|
@@ -1,9 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Middleware } from "../types";
|
|
1
|
+
import { Context } from "../http";
|
|
2
|
+
import type { Middleware, RouteHandler } from "../types";
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
/**
|
|
5
|
+
* Mutable counter entry for a single rate-limit key.
|
|
6
|
+
*/
|
|
7
|
+
export interface RateLimitStoreEntry {
|
|
8
|
+
/** Requests performed in the current window. */
|
|
9
|
+
count: number;
|
|
10
|
+
/** Absolute timestamp (ms) when the current window expires. */
|
|
11
|
+
resetTime: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Pluggable store contract for rate limiting.
|
|
15
|
+
*
|
|
16
|
+
* Use a shared implementation (e.g. Redis) in multi-instance deployments.
|
|
17
|
+
*/
|
|
18
|
+
export interface RateLimitStore {
|
|
19
|
+
/**
|
|
20
|
+
* Increments the counter for `key` in the active window.
|
|
21
|
+
*
|
|
22
|
+
* @param key - Unique client key.
|
|
23
|
+
* @param windowMs - Active window size in milliseconds.
|
|
24
|
+
* @param now - Current unix timestamp in milliseconds.
|
|
25
|
+
* @returns Updated counter entry for the key.
|
|
26
|
+
*/
|
|
27
|
+
increment(key: string, windowMs: number, now: number): MaybePromise<RateLimitStoreEntry>;
|
|
28
|
+
/**
|
|
29
|
+
* Optional cleanup hook for removing expired entries.
|
|
30
|
+
*
|
|
31
|
+
* @param now - Current unix timestamp in milliseconds.
|
|
32
|
+
*/
|
|
33
|
+
cleanup?(now: number): MaybePromise<void>;
|
|
34
|
+
}
|
|
3
35
|
/**
|
|
4
36
|
* Rate limit middleware configuration.
|
|
5
37
|
*/
|
|
6
|
-
interface RateLimitOptions {
|
|
38
|
+
export interface RateLimitOptions {
|
|
7
39
|
/**
|
|
8
40
|
* Time window in milliseconds where requests are counted.
|
|
9
41
|
* @default 60000
|
|
@@ -26,11 +58,31 @@ interface RateLimitOptions {
|
|
|
26
58
|
/**
|
|
27
59
|
* Resolves the identity key used for counting requests.
|
|
28
60
|
*/
|
|
29
|
-
keyGenerator?: (
|
|
61
|
+
keyGenerator?: (context: Context) => string;
|
|
62
|
+
/**
|
|
63
|
+
* Trusts proxy-provided IP headers (`x-forwarded-for`, `x-real-ip`,
|
|
64
|
+
* `cf-connecting-ip`) in the default key generator.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
trustProxy?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Custom store implementation. Defaults to in-memory store.
|
|
70
|
+
*/
|
|
71
|
+
store?: RateLimitStore;
|
|
72
|
+
/**
|
|
73
|
+
* Periodic cleanup interval for stores that support `cleanup`.
|
|
74
|
+
* @default windowMs
|
|
75
|
+
*/
|
|
76
|
+
cleanupIntervalMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Max number of keys retained by the built-in in-memory store.
|
|
79
|
+
* @default 50000
|
|
80
|
+
*/
|
|
81
|
+
maxKeys?: number;
|
|
30
82
|
/**
|
|
31
83
|
* Optional predicate to skip rate limiting for selected requests.
|
|
32
84
|
*/
|
|
33
|
-
skip?: (
|
|
85
|
+
skip?: (context: Context) => boolean | Promise<boolean>;
|
|
34
86
|
/**
|
|
35
87
|
* Includes `RateLimit-*` response headers.
|
|
36
88
|
* @default true
|
|
@@ -44,7 +96,7 @@ interface RateLimitOptions {
|
|
|
44
96
|
/**
|
|
45
97
|
* Optional custom handler executed when the limit is exceeded.
|
|
46
98
|
*/
|
|
47
|
-
handler?:
|
|
99
|
+
handler?: RouteHandler;
|
|
48
100
|
}
|
|
49
101
|
/**
|
|
50
102
|
* Creates a configurable request rate-limiting middleware.
|
|
@@ -3,35 +3,146 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.rateLimit = void 0;
|
|
4
4
|
const http_1 = require("../http");
|
|
5
5
|
const httpExceptions_1 = require("../exceptions/httpExceptions");
|
|
6
|
+
const node_net_1 = require("node:net");
|
|
6
7
|
/**
|
|
7
|
-
* Default
|
|
8
|
+
* Default in-memory implementation of {@link RateLimitStore}.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
* Use a shared external store (e.g. Redis) for distributed environments.
|
|
11
|
+
*/
|
|
12
|
+
class MemoryRateLimitStore {
|
|
13
|
+
maxKeys;
|
|
14
|
+
store = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* @param maxKeys - Maximum number of retained keys in memory.
|
|
17
|
+
*/
|
|
18
|
+
constructor(maxKeys) {
|
|
19
|
+
this.maxKeys = maxKeys;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Increments an in-memory counter for the provided key.
|
|
23
|
+
*
|
|
24
|
+
* If the window is expired (or absent), a new window starts at `now`.
|
|
25
|
+
*/
|
|
26
|
+
increment(key, windowMs, now) {
|
|
27
|
+
let entry = this.store.get(key);
|
|
28
|
+
if (!entry || now > entry.resetTime) {
|
|
29
|
+
entry = { count: 1, resetTime: now + windowMs };
|
|
30
|
+
this.store.set(key, entry);
|
|
31
|
+
this.trimIfNeeded(now);
|
|
32
|
+
return entry;
|
|
33
|
+
}
|
|
34
|
+
entry.count += 1;
|
|
35
|
+
return entry;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Removes expired entries from the internal map.
|
|
39
|
+
*/
|
|
40
|
+
cleanup(now) {
|
|
41
|
+
for (const [key, value] of this.store.entries()) {
|
|
42
|
+
if (now > value.resetTime)
|
|
43
|
+
this.store.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Enforces `maxKeys` and avoids unbounded memory growth.
|
|
48
|
+
*/
|
|
49
|
+
trimIfNeeded(now) {
|
|
50
|
+
if (this.store.size <= this.maxKeys)
|
|
51
|
+
return;
|
|
52
|
+
this.cleanup(now);
|
|
53
|
+
while (this.store.size > this.maxKeys) {
|
|
54
|
+
const oldestKey = this.store.keys().next().value;
|
|
55
|
+
if (!oldestKey)
|
|
56
|
+
break;
|
|
57
|
+
this.store.delete(oldestKey);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Normalizes a candidate IP address into a canonical value.
|
|
16
63
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
64
|
+
* Supported inputs:
|
|
65
|
+
* - Plain IPv4/IPv6
|
|
66
|
+
* - IPv4-mapped IPv6 (`::ffff:x.x.x.x`)
|
|
67
|
+
* - Bracketed IPv6 (`[2001:db8::1]`)
|
|
68
|
+
* - IPv4 with port (`x.x.x.x:port`)
|
|
19
69
|
*
|
|
20
|
-
* @param
|
|
21
|
-
* @returns
|
|
70
|
+
* @param value - Raw address candidate.
|
|
71
|
+
* @returns Normalized IP, or `null` when the input is not a valid IP.
|
|
22
72
|
*/
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
73
|
+
const normalizeAddress = (value) => {
|
|
74
|
+
if (!value)
|
|
75
|
+
return null;
|
|
76
|
+
const candidate = value.trim();
|
|
77
|
+
if (!candidate)
|
|
78
|
+
return null;
|
|
79
|
+
if ((0, node_net_1.isIP)(candidate))
|
|
80
|
+
return candidate;
|
|
81
|
+
if (candidate.startsWith("::ffff:")) {
|
|
82
|
+
const mapped = candidate.slice("::ffff:".length);
|
|
83
|
+
if ((0, node_net_1.isIP)(mapped))
|
|
84
|
+
return mapped;
|
|
29
85
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
86
|
+
const withoutBrackets = candidate.startsWith("[") && candidate.endsWith("]")
|
|
87
|
+
? candidate.slice(1, -1)
|
|
88
|
+
: candidate;
|
|
89
|
+
if ((0, node_net_1.isIP)(withoutBrackets))
|
|
90
|
+
return withoutBrackets;
|
|
91
|
+
const ipv4WithPort = withoutBrackets.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/);
|
|
92
|
+
if (ipv4WithPort && (0, node_net_1.isIP)(ipv4WithPort[1]))
|
|
93
|
+
return ipv4WithPort[1];
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Returns the first value when a header can be `string | string[]`.
|
|
98
|
+
*/
|
|
99
|
+
const pickFirstHeaderValue = (value) => Array.isArray(value) ? value[0] : value;
|
|
100
|
+
/**
|
|
101
|
+
* Parses `x-forwarded-for` into an ordered list of candidates.
|
|
102
|
+
*
|
|
103
|
+
* @param value - Raw `x-forwarded-for` header value.
|
|
104
|
+
* @returns A trimmed list of forwarded addresses.
|
|
105
|
+
*/
|
|
106
|
+
const parseForwardedFor = (value) => {
|
|
107
|
+
const firstValue = pickFirstHeaderValue(value);
|
|
108
|
+
if (!firstValue)
|
|
109
|
+
return [];
|
|
110
|
+
return firstValue
|
|
111
|
+
.split(",")
|
|
112
|
+
.map(item => item.trim())
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Builds the default key generator used by the rate limiter.
|
|
117
|
+
*
|
|
118
|
+
* Behavior:
|
|
119
|
+
* - `trustProxy: false` -> uses only socket `remoteAddress` (plus host fallback).
|
|
120
|
+
* - `trustProxy: true` -> allows proxy headers in this priority:
|
|
121
|
+
* 1) `cf-connecting-ip`
|
|
122
|
+
* 2) `x-real-ip`
|
|
123
|
+
* 3) first valid item in `x-forwarded-for`
|
|
124
|
+
* 4) socket `remoteAddress` fallback
|
|
125
|
+
*
|
|
126
|
+
* @param trustProxy - Whether proxy headers are trusted.
|
|
127
|
+
* @returns A key generator function for rate-limit identity.
|
|
128
|
+
*/
|
|
129
|
+
const buildDefaultKeyGenerator = (trustProxy) => (context) => {
|
|
130
|
+
const remoteAddress = normalizeAddress(context.remoteAddress);
|
|
131
|
+
if (!trustProxy)
|
|
132
|
+
return remoteAddress ?? context.headers.host ?? "anonymous";
|
|
133
|
+
const cfIp = normalizeAddress(pickFirstHeaderValue(context.headers["cf-connecting-ip"]));
|
|
134
|
+
if (cfIp)
|
|
33
135
|
return cfIp;
|
|
34
|
-
|
|
136
|
+
const realIp = normalizeAddress(pickFirstHeaderValue(context.headers["x-real-ip"]));
|
|
137
|
+
if (realIp)
|
|
138
|
+
return realIp;
|
|
139
|
+
const forwardedCandidates = parseForwardedFor(context.headers["x-forwarded-for"]);
|
|
140
|
+
for (const candidate of forwardedCandidates) {
|
|
141
|
+
const trustedIp = normalizeAddress(candidate);
|
|
142
|
+
if (trustedIp)
|
|
143
|
+
return trustedIp;
|
|
144
|
+
}
|
|
145
|
+
return remoteAddress ?? context.headers.host ?? "anonymous";
|
|
35
146
|
};
|
|
36
147
|
/**
|
|
37
148
|
* Generates HTTP headers describing the current rate limit state.
|
|
@@ -112,46 +223,44 @@ const getRateLimitHeaders = (limit, current, resetTime, now, includeStandardHead
|
|
|
112
223
|
* ]);
|
|
113
224
|
*/
|
|
114
225
|
const rateLimit = (options = {}) => {
|
|
226
|
+
const trustProxy = options.trustProxy ?? false;
|
|
227
|
+
const memoryStoreMaxKeys = Math.max(options.maxKeys ?? 50_000, 1);
|
|
228
|
+
const store = options.store ?? new MemoryRateLimitStore(memoryStoreMaxKeys);
|
|
115
229
|
const config = {
|
|
116
230
|
windowMs: options.windowMs ?? 60_000,
|
|
117
231
|
max: options.max ?? 5,
|
|
118
232
|
message: options.message ?? "Too many requests, please try again later.",
|
|
119
233
|
statusCode: options.statusCode ?? 429,
|
|
120
|
-
keyGenerator: options.keyGenerator ??
|
|
234
|
+
keyGenerator: options.keyGenerator ?? buildDefaultKeyGenerator(trustProxy),
|
|
121
235
|
skip: options.skip,
|
|
122
236
|
standardHeaders: options.standardHeaders ?? true,
|
|
123
237
|
legacyHeaders: options.legacyHeaders ?? false,
|
|
124
238
|
handler: options.handler,
|
|
125
239
|
};
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
240
|
+
const cleanupIntervalMs = Math.max(options.cleanupIntervalMs ?? config.windowMs, 1000);
|
|
241
|
+
let nextCleanupAt = Date.now() + cleanupIntervalMs;
|
|
242
|
+
return async (context, next) => {
|
|
243
|
+
if (config.skip && (await config.skip(context))) {
|
|
244
|
+
return next(context);
|
|
130
245
|
}
|
|
131
246
|
const now = Date.now();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
store.set(key, {
|
|
136
|
-
count: 1,
|
|
137
|
-
resetTime: now + config.windowMs,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
current.count += 1;
|
|
247
|
+
if (now >= nextCleanupAt && store.cleanup) {
|
|
248
|
+
await store.cleanup(now);
|
|
249
|
+
nextCleanupAt = now + cleanupIntervalMs;
|
|
142
250
|
}
|
|
143
|
-
const
|
|
251
|
+
const key = config.keyGenerator(context) || "anonymous";
|
|
252
|
+
const entry = await store.increment(key, config.windowMs, now);
|
|
144
253
|
const headers = getRateLimitHeaders(config.max, entry.count, entry.resetTime, now, config.standardHeaders, config.legacyHeaders);
|
|
145
254
|
if (entry.count > config.max) {
|
|
146
255
|
const retryAfter = Math.max(Math.ceil((entry.resetTime - now) / 1000), 0);
|
|
147
256
|
headers["Retry-After"] = String(retryAfter);
|
|
148
257
|
const blockedResponse = config.handler
|
|
149
|
-
? await config.handler(
|
|
258
|
+
? await config.handler(context)
|
|
150
259
|
: http_1.Response.json(new httpExceptions_1.TooManyRequestsError(config.message).toJSON()).setStatusCode(config.statusCode);
|
|
151
260
|
blockedResponse.setHeaders(headers);
|
|
152
261
|
return blockedResponse;
|
|
153
262
|
}
|
|
154
|
-
const response = await next(
|
|
263
|
+
const response = await next(context);
|
|
155
264
|
response.setHeaders(headers);
|
|
156
265
|
return response;
|
|
157
266
|
};
|
|
@@ -68,17 +68,17 @@ const sessions = (StorageClass, options = {}) => {
|
|
|
68
68
|
path: options.cookie?.path ?? "/",
|
|
69
69
|
},
|
|
70
70
|
};
|
|
71
|
-
return async (
|
|
72
|
-
const cookies = (0, cookies_1.parseCookies)(
|
|
71
|
+
return async (context, next) => {
|
|
72
|
+
const cookies = (0, cookies_1.parseCookies)(context.headers.cookie);
|
|
73
73
|
const sessionIdFromCookie = cookies[config.name];
|
|
74
74
|
const storage = new StorageClass(config.cookie.maxAge);
|
|
75
75
|
await loadSessionFromCookie(storage, sessionIdFromCookie);
|
|
76
76
|
const session = new sessions_1.Session(storage);
|
|
77
|
-
|
|
77
|
+
context.req.setSession(session);
|
|
78
78
|
const sessionIdBefore = storage.id();
|
|
79
79
|
if (!sessionIdBefore && config.saveUninitialized)
|
|
80
80
|
await storage.start();
|
|
81
|
-
const response = await next(
|
|
81
|
+
const response = await next(context);
|
|
82
82
|
const sessionIdAfter = storage.id();
|
|
83
83
|
if (sessionIdAfter && config.rolling)
|
|
84
84
|
await storage.touch();
|
|
@@ -26,14 +26,16 @@ export declare class ContentParserManager {
|
|
|
26
26
|
* @param req - Native incoming HTTP request
|
|
27
27
|
* @returns Parsed body content or raw body
|
|
28
28
|
*/
|
|
29
|
-
parse(req: IncomingMessage): Promise<unknown>;
|
|
29
|
+
parse(req: IncomingMessage | globalThis.Request): Promise<unknown>;
|
|
30
30
|
/**
|
|
31
31
|
* Reads the raw request body.
|
|
32
32
|
*
|
|
33
33
|
* @param req - Native incoming HTTP request
|
|
34
34
|
* @returns A promise that resolves to the full body buffer
|
|
35
35
|
*/
|
|
36
|
-
private
|
|
36
|
+
private readNodeBody;
|
|
37
|
+
private readWebBody;
|
|
38
|
+
private isWebRequest;
|
|
37
39
|
/**
|
|
38
40
|
* Finds a parser capable of handling the given content type.
|
|
39
41
|
*
|
|
@@ -43,10 +43,15 @@ class ContentParserManager {
|
|
|
43
43
|
* @returns Parsed body content or raw body
|
|
44
44
|
*/
|
|
45
45
|
async parse(req) {
|
|
46
|
-
const
|
|
46
|
+
const isWebRequest = this.isWebRequest(req);
|
|
47
|
+
const body = isWebRequest
|
|
48
|
+
? await this.readWebBody(req)
|
|
49
|
+
: await this.readNodeBody(req);
|
|
47
50
|
if (body.length <= 0)
|
|
48
51
|
return {};
|
|
49
|
-
const contentType =
|
|
52
|
+
const contentType = isWebRequest
|
|
53
|
+
? req.headers.get("content-type") || "text/plain"
|
|
54
|
+
: req.headers["content-type"] || "text/plain";
|
|
50
55
|
const parser = this.findParser(contentType);
|
|
51
56
|
if (!parser)
|
|
52
57
|
return Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
@@ -58,7 +63,7 @@ class ContentParserManager {
|
|
|
58
63
|
* @param req - Native incoming HTTP request
|
|
59
64
|
* @returns A promise that resolves to the full body buffer
|
|
60
65
|
*/
|
|
61
|
-
|
|
66
|
+
readNodeBody(req) {
|
|
62
67
|
return new Promise((resolve, reject) => {
|
|
63
68
|
const chunks = [];
|
|
64
69
|
req.on("data", (chunk) => {
|
|
@@ -72,6 +77,20 @@ class ContentParserManager {
|
|
|
72
77
|
});
|
|
73
78
|
});
|
|
74
79
|
}
|
|
80
|
+
async readWebBody(req) {
|
|
81
|
+
try {
|
|
82
|
+
const arrayBuffer = await req.arrayBuffer();
|
|
83
|
+
return Buffer.from(arrayBuffer);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new httpExceptions_1.UnprocessableContentError("Failed to read request body");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
isWebRequest(req) {
|
|
90
|
+
const candidate = req;
|
|
91
|
+
return (typeof candidate.arrayBuffer === "function" &&
|
|
92
|
+
typeof candidate.headers?.get === "function");
|
|
93
|
+
}
|
|
75
94
|
/**
|
|
76
95
|
* Finds a parser capable of handling the given content type.
|
|
77
96
|
*
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HandlerOrMiddlewares, RouteHandler, Middleware } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes route registration args to support both signatures:
|
|
4
|
+
* - (path, action, middlewares?)
|
|
5
|
+
* - (path, middlewares, action)
|
|
6
|
+
*/
|
|
7
|
+
export declare const normalizeRouteArgs: (handlerOrMiddlewares: HandlerOrMiddlewares, handler?: RouteHandler) => {
|
|
8
|
+
action: RouteHandler;
|
|
9
|
+
middlewares: Middleware[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeRouteArgs = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes route registration args to support both signatures:
|
|
6
|
+
* - (path, action, middlewares?)
|
|
7
|
+
* - (path, middlewares, action)
|
|
8
|
+
*/
|
|
9
|
+
const normalizeRouteArgs = (handlerOrMiddlewares, handler) => {
|
|
10
|
+
if (Array.isArray(handlerOrMiddlewares)) {
|
|
11
|
+
return {
|
|
12
|
+
action: handler,
|
|
13
|
+
middlewares: handlerOrMiddlewares,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
action: handlerOrMiddlewares,
|
|
18
|
+
middlewares: Array.isArray(handler) ? handler : [],
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
exports.normalizeRouteArgs = normalizeRouteArgs;
|
package/dist/routing/router.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Middleware, RouteHandler } from "../types";
|
|
2
|
-
import {
|
|
2
|
+
import { Context, Response } from "../http";
|
|
3
3
|
import { Layer } from "./layer";
|
|
4
4
|
import { RouterGroup } from "./routerGroup";
|
|
5
5
|
/**
|
|
@@ -26,7 +26,7 @@ export declare class Router {
|
|
|
26
26
|
* @returns The matching {@link Layer}
|
|
27
27
|
* @throws {HttpNotFoundException} If no route matches
|
|
28
28
|
*/
|
|
29
|
-
resolveLayer(
|
|
29
|
+
resolveLayer(context: Context): Layer;
|
|
30
30
|
/**
|
|
31
31
|
* Resolves and executes a request.
|
|
32
32
|
*
|
|
@@ -39,14 +39,15 @@ export declare class Router {
|
|
|
39
39
|
* @param request - Request to process
|
|
40
40
|
* @returns The handler/middleware response (sync or async)
|
|
41
41
|
*/
|
|
42
|
-
resolve(request:
|
|
42
|
+
resolve(request: Context): Promise<Response> | Response;
|
|
43
|
+
private ensureContext;
|
|
43
44
|
/**
|
|
44
45
|
* Runs a middleware chain using the onion model.
|
|
45
46
|
*
|
|
46
|
-
* Each middleware receives `(
|
|
47
|
+
* Each middleware receives `(context, next)` and can run code
|
|
47
48
|
* before/after calling `next()`.
|
|
48
49
|
*
|
|
49
|
-
* @param
|
|
50
|
+
* @param context - Incoming context
|
|
50
51
|
* @param middlewares - Remaining middlewares to execute
|
|
51
52
|
* @param target - Final route handler
|
|
52
53
|
* @returns The response returned by a middleware or the final handler
|
|
@@ -87,13 +88,18 @@ export declare class Router {
|
|
|
87
88
|
*/
|
|
88
89
|
middlewares(middlewares: Middleware[]): this;
|
|
89
90
|
/** Registers a GET route. */
|
|
90
|
-
get(path: string, action: RouteHandler
|
|
91
|
+
get(path: string, action: RouteHandler): Layer;
|
|
92
|
+
get(path: string, middlewares: Middleware[], action: RouteHandler): Layer;
|
|
91
93
|
/** Registers a POST route. */
|
|
92
|
-
post(path: string, action: RouteHandler
|
|
94
|
+
post(path: string, action: RouteHandler): Layer;
|
|
95
|
+
post(path: string, middlewares: Middleware[], action: RouteHandler): Layer;
|
|
93
96
|
/** Registers a PATCH route. */
|
|
94
|
-
patch(path: string, action: RouteHandler
|
|
97
|
+
patch(path: string, action: RouteHandler): Layer;
|
|
98
|
+
patch(path: string, middlewares: Middleware[], action: RouteHandler): Layer;
|
|
95
99
|
/** Registers a PUT route. */
|
|
96
|
-
put(path: string, action: RouteHandler
|
|
100
|
+
put(path: string, action: RouteHandler): Layer;
|
|
101
|
+
put(path: string, middlewares: Middleware[], action: RouteHandler): Layer;
|
|
97
102
|
/** Registers a DELETE route. */
|
|
98
|
-
delete(path: string, action: RouteHandler
|
|
103
|
+
delete(path: string, action: RouteHandler): Layer;
|
|
104
|
+
delete(path: string, middlewares: Middleware[], action: RouteHandler): Layer;
|
|
99
105
|
}
|
package/dist/routing/router.js
CHANGED
|
@@ -6,6 +6,7 @@ const httpExceptions_1 = require("../exceptions/httpExceptions");
|
|
|
6
6
|
const layer_1 = require("./layer");
|
|
7
7
|
const routerGroup_1 = require("./routerGroup");
|
|
8
8
|
const buildFullPath_1 = require("./buildFullPath");
|
|
9
|
+
const routeResolveFunc_1 = require("./routeResolveFunc");
|
|
9
10
|
/**
|
|
10
11
|
* Central routing system of the framework.
|
|
11
12
|
*
|
|
@@ -34,10 +35,10 @@ class Router {
|
|
|
34
35
|
* @returns The matching {@link Layer}
|
|
35
36
|
* @throws {HttpNotFoundException} If no route matches
|
|
36
37
|
*/
|
|
37
|
-
resolveLayer(
|
|
38
|
-
const routes = this.routes[
|
|
38
|
+
resolveLayer(context) {
|
|
39
|
+
const routes = this.routes[context.req.method];
|
|
39
40
|
for (const route of routes) {
|
|
40
|
-
if (route.matches(
|
|
41
|
+
if (route.matches(context.req.url))
|
|
41
42
|
return route;
|
|
42
43
|
}
|
|
43
44
|
throw new httpExceptions_1.NotFoundError("Route not found");
|
|
@@ -55,38 +56,44 @@ class Router {
|
|
|
55
56
|
* @returns The handler/middleware response (sync or async)
|
|
56
57
|
*/
|
|
57
58
|
resolve(request) {
|
|
58
|
-
const
|
|
59
|
-
|
|
59
|
+
const context = this.ensureContext(request);
|
|
60
|
+
const executeLayer = (ctx) => {
|
|
61
|
+
const layer = this.resolveLayer(ctx);
|
|
60
62
|
if (layer.hasParameters()) {
|
|
61
|
-
req.setParams(layer.parseParameters(req.url));
|
|
63
|
+
ctx.req.setParams(layer.parseParameters(ctx.req.url));
|
|
62
64
|
}
|
|
63
65
|
const action = layer.getAction;
|
|
64
66
|
const routeMiddlewares = layer.getMiddlewares;
|
|
65
67
|
if (routeMiddlewares.length > 0) {
|
|
66
|
-
return this.runMiddlewares(
|
|
68
|
+
return this.runMiddlewares(ctx, routeMiddlewares, action);
|
|
67
69
|
}
|
|
68
|
-
return action(
|
|
70
|
+
return action(ctx);
|
|
69
71
|
};
|
|
70
72
|
if (this.globalMiddlewares.length > 0) {
|
|
71
|
-
return this.runMiddlewares(
|
|
73
|
+
return this.runMiddlewares(context, this.globalMiddlewares, executeLayer);
|
|
72
74
|
}
|
|
73
|
-
return executeLayer(
|
|
75
|
+
return executeLayer(context);
|
|
76
|
+
}
|
|
77
|
+
ensureContext(request) {
|
|
78
|
+
if (request instanceof http_1.Context)
|
|
79
|
+
return request;
|
|
80
|
+
return new http_1.Context(request);
|
|
74
81
|
}
|
|
75
82
|
/**
|
|
76
83
|
* Runs a middleware chain using the onion model.
|
|
77
84
|
*
|
|
78
|
-
* Each middleware receives `(
|
|
85
|
+
* Each middleware receives `(context, next)` and can run code
|
|
79
86
|
* before/after calling `next()`.
|
|
80
87
|
*
|
|
81
|
-
* @param
|
|
88
|
+
* @param context - Incoming context
|
|
82
89
|
* @param middlewares - Remaining middlewares to execute
|
|
83
90
|
* @param target - Final route handler
|
|
84
91
|
* @returns The response returned by a middleware or the final handler
|
|
85
92
|
*/
|
|
86
|
-
runMiddlewares(
|
|
93
|
+
runMiddlewares(context, middlewares, target) {
|
|
87
94
|
if (middlewares.length === 0)
|
|
88
|
-
return target(
|
|
89
|
-
return middlewares[0](
|
|
95
|
+
return target(context);
|
|
96
|
+
return middlewares[0](context, nextContext => this.runMiddlewares(this.ensureContext(nextContext), middlewares.slice(1), target));
|
|
90
97
|
}
|
|
91
98
|
/**
|
|
92
99
|
* Registers a route and returns the created {@link Layer}.
|
|
@@ -138,24 +145,24 @@ class Router {
|
|
|
138
145
|
this.globalMiddlewares.push(...middlewares);
|
|
139
146
|
return this;
|
|
140
147
|
}
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
get(path, handlerOrMiddlewares, handler) {
|
|
149
|
+
const { action, middlewares } = (0, routeResolveFunc_1.normalizeRouteArgs)(handlerOrMiddlewares, handler);
|
|
143
150
|
return this.registerRoute(http_1.HttpMethods.get, path, action, middlewares);
|
|
144
151
|
}
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
post(path, handlerOrMiddlewares, handler) {
|
|
153
|
+
const { action, middlewares } = (0, routeResolveFunc_1.normalizeRouteArgs)(handlerOrMiddlewares, handler);
|
|
147
154
|
return this.registerRoute(http_1.HttpMethods.post, path, action, middlewares);
|
|
148
155
|
}
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
patch(path, handlerOrMiddlewares, handler) {
|
|
157
|
+
const { action, middlewares } = (0, routeResolveFunc_1.normalizeRouteArgs)(handlerOrMiddlewares, handler);
|
|
151
158
|
return this.registerRoute(http_1.HttpMethods.patch, path, action, middlewares);
|
|
152
159
|
}
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
put(path, handlerOrMiddlewares, handler) {
|
|
161
|
+
const { action, middlewares } = (0, routeResolveFunc_1.normalizeRouteArgs)(handlerOrMiddlewares, handler);
|
|
155
162
|
return this.registerRoute(http_1.HttpMethods.put, path, action, middlewares);
|
|
156
163
|
}
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
delete(path, handlerOrMiddlewares, handler) {
|
|
165
|
+
const { action, middlewares } = (0, routeResolveFunc_1.normalizeRouteArgs)(handlerOrMiddlewares, handler);
|
|
159
166
|
return this.registerRoute(http_1.HttpMethods.delete, path, action, middlewares);
|
|
160
167
|
}
|
|
161
168
|
}
|
|
@@ -51,13 +51,18 @@ export declare class RouterGroup {
|
|
|
51
51
|
*/
|
|
52
52
|
private addRoute;
|
|
53
53
|
/** Registers a GET route within the group. */
|
|
54
|
-
get(path: string, action: RouteHandler
|
|
54
|
+
get(path: string, action: RouteHandler): void;
|
|
55
|
+
get(path: string, middlewares: Middleware[], action: RouteHandler): void;
|
|
55
56
|
/** Registers a POST route within the group. */
|
|
56
|
-
post(path: string, action: RouteHandler
|
|
57
|
+
post(path: string, action: RouteHandler): void;
|
|
58
|
+
post(path: string, middlewares: Middleware[], action: RouteHandler): void;
|
|
57
59
|
/** Registers a PUT route within the group. */
|
|
58
|
-
put(path: string, action: RouteHandler
|
|
60
|
+
put(path: string, action: RouteHandler): void;
|
|
61
|
+
put(path: string, middlewares: Middleware[], action: RouteHandler): void;
|
|
59
62
|
/** Registers a PATCH route within the group. */
|
|
60
|
-
patch(path: string, action: RouteHandler
|
|
63
|
+
patch(path: string, action: RouteHandler): void;
|
|
64
|
+
patch(path: string, middlewares: Middleware[], action: RouteHandler): void;
|
|
61
65
|
/** Registers a DELETE route within the group. */
|
|
62
|
-
delete(path: string, action: RouteHandler
|
|
66
|
+
delete(path: string, action: RouteHandler): void;
|
|
67
|
+
delete(path: string, middlewares: Middleware[], action: RouteHandler): void;
|
|
63
68
|
}
|