@morojs/moro 1.8.6 → 1.8.8

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 (68) hide show
  1. package/dist/core/http/http-server.d.ts +5 -3
  2. package/dist/core/http/http-server.js +42 -11
  3. package/dist/core/http/http-server.js.map +1 -1
  4. package/dist/core/http/http2-server.d.ts +1 -1
  5. package/dist/core/http/http2-server.js +27 -9
  6. package/dist/core/http/http2-server.js.map +1 -1
  7. package/dist/core/http/moro-engine-server.d.ts +24 -4
  8. package/dist/core/http/moro-engine-server.js +198 -42
  9. package/dist/core/http/moro-engine-server.js.map +1 -1
  10. package/dist/core/http/uws-http-server.js +4 -0
  11. package/dist/core/http/uws-http-server.js.map +1 -1
  12. package/dist/core/middleware/built-in/cookie/core.d.ts +26 -1
  13. package/dist/core/middleware/built-in/cookie/core.js +70 -2
  14. package/dist/core/middleware/built-in/cookie/core.js.map +1 -1
  15. package/dist/core/middleware/built-in/cookie/hook.d.ts +3 -1
  16. package/dist/core/middleware/built-in/cookie/hook.js +17 -5
  17. package/dist/core/middleware/built-in/cookie/hook.js.map +1 -1
  18. package/dist/core/middleware/built-in/cookie/index.d.ts +1 -1
  19. package/dist/core/middleware/built-in/cookie/index.js +1 -1
  20. package/dist/core/middleware/built-in/cookie/index.js.map +1 -1
  21. package/dist/core/middleware/built-in/cookie/middleware.d.ts +1 -1
  22. package/dist/core/middleware/built-in/cookie/middleware.js +6 -4
  23. package/dist/core/middleware/built-in/cookie/middleware.js.map +1 -1
  24. package/dist/core/middleware/built-in/index.d.ts +4 -12
  25. package/dist/core/middleware/built-in/rate-limit/core.d.ts +24 -0
  26. package/dist/core/middleware/built-in/rate-limit/core.js +47 -0
  27. package/dist/core/middleware/built-in/rate-limit/core.js.map +1 -1
  28. package/dist/core/middleware/built-in/rate-limit/hook.d.ts +24 -9
  29. package/dist/core/middleware/built-in/rate-limit/hook.js +34 -12
  30. package/dist/core/middleware/built-in/rate-limit/hook.js.map +1 -1
  31. package/dist/core/middleware/built-in/rate-limit/middleware.js +9 -1
  32. package/dist/core/middleware/built-in/rate-limit/middleware.js.map +1 -1
  33. package/dist/core/middleware/built-in/session/core.d.ts +8 -0
  34. package/dist/core/middleware/built-in/session/core.js +29 -2
  35. package/dist/core/middleware/built-in/session/core.js.map +1 -1
  36. package/dist/core/middleware/built-in/session/hook.js +2 -3
  37. package/dist/core/middleware/built-in/session/hook.js.map +1 -1
  38. package/dist/core/middleware/built-in/session/middleware.js +2 -3
  39. package/dist/core/middleware/built-in/session/middleware.js.map +1 -1
  40. package/dist/core/middleware/built-in/static/core.d.ts +56 -0
  41. package/dist/core/middleware/built-in/static/core.js +214 -13
  42. package/dist/core/middleware/built-in/static/core.js.map +1 -1
  43. package/dist/core/middleware/built-in/static/middleware.d.ts +5 -0
  44. package/dist/core/middleware/built-in/static/middleware.js +5 -0
  45. package/dist/core/middleware/built-in/static/middleware.js.map +1 -1
  46. package/dist/core/middleware/built-in/upload/core.d.ts +17 -0
  47. package/dist/core/middleware/built-in/upload/core.js +30 -1
  48. package/dist/core/middleware/built-in/upload/core.js.map +1 -1
  49. package/dist/core/middleware/built-in/upload/middleware.js +12 -1
  50. package/dist/core/middleware/built-in/upload/middleware.js.map +1 -1
  51. package/dist/core/pooling/object-pool-manager.js +14 -20
  52. package/dist/core/pooling/object-pool-manager.js.map +1 -1
  53. package/dist/core/routing/index.d.ts +1 -0
  54. package/dist/core/routing/index.js.map +1 -1
  55. package/dist/core/routing/radix-tree.d.ts +1 -0
  56. package/dist/core/routing/radix-tree.js +6 -1
  57. package/dist/core/routing/radix-tree.js.map +1 -1
  58. package/dist/core/routing/unified-router.js +28 -25
  59. package/dist/core/routing/unified-router.js.map +1 -1
  60. package/dist/core/utilities/hooks.d.ts +2 -0
  61. package/dist/core/utilities/hooks.js +28 -2
  62. package/dist/core/utilities/hooks.js.map +1 -1
  63. package/dist/index.d.ts +2 -6
  64. package/dist/moro.js +12 -0
  65. package/dist/moro.js.map +1 -1
  66. package/dist/types/http.d.ts +3 -0
  67. package/dist/types/http.js.map +1 -1
  68. package/package.json +1 -1
@@ -7,7 +7,22 @@ export interface CookieOptions {
7
7
  sameSite?: 'strict' | 'lax' | 'none';
8
8
  domain?: string;
9
9
  path?: string;
10
+ /**
11
+ * Sign the value so tampering can be detected. Requires a `secret` on the
12
+ * cookie middleware; signed cookies arrive on `req.signedCookies`, never on
13
+ * `req.cookies`.
14
+ */
15
+ signed?: boolean;
10
16
  }
17
+ /** `s:<value>.<signature>` — HMAC-SHA256 of the value, base64url encoded. */
18
+ export declare function signCookieValue(value: string, secret: string): string;
19
+ /**
20
+ * Verify a signed value, returning the payload or null when the value isn't
21
+ * signed or the signature doesn't match. Comparison is constant-time.
22
+ */
23
+ export declare function unsignCookieValue(signed: string, secret: string): string | null;
24
+ /** Whether a raw cookie value carries the signed marker. */
25
+ export declare function isSignedCookieValue(value: string): boolean;
11
26
  /**
12
27
  * Parse cookies from a Cookie header string
13
28
  */
@@ -21,11 +36,21 @@ export declare function buildCookieString(name: string, value: string, options?:
21
36
  * Used directly by the router for route-based cookie handling
22
37
  */
23
38
  export declare class CookieCore {
24
- constructor();
39
+ private secret;
40
+ constructor(secret?: string);
25
41
  /**
26
42
  * Parse cookies from request header
27
43
  */
28
44
  parseCookies(cookieHeader: string | undefined): Record<string, string>;
45
+ /**
46
+ * Split parsed cookies into plain values and verified signed ones. A signed
47
+ * cookie whose signature doesn't verify is dropped from both — a tampered
48
+ * value must never look like a valid one.
49
+ */
50
+ splitSignedCookies(cookies: Record<string, string>): {
51
+ cookies: Record<string, string>;
52
+ signedCookies: Record<string, string>;
53
+ };
29
54
  /**
30
55
  * Set a cookie on the response
31
56
  */
@@ -1,3 +1,38 @@
1
+ // Cookie Core - Reusable cookie parsing and setting logic
2
+ import * as crypto from 'crypto';
3
+ /** Prefix marking a signed value, matching the cookie-parser convention. */
4
+ const SIGNED_PREFIX = 's:';
5
+ /** `s:<value>.<signature>` — HMAC-SHA256 of the value, base64url encoded. */
6
+ export function signCookieValue(value, secret) {
7
+ const signature = crypto
8
+ .createHmac('sha256', secret)
9
+ .update(value)
10
+ .digest('base64')
11
+ .replace(/=+$/, '');
12
+ return `${SIGNED_PREFIX}${value}.${signature}`;
13
+ }
14
+ /**
15
+ * Verify a signed value, returning the payload or null when the value isn't
16
+ * signed or the signature doesn't match. Comparison is constant-time.
17
+ */
18
+ export function unsignCookieValue(signed, secret) {
19
+ if (!signed.startsWith(SIGNED_PREFIX))
20
+ return null;
21
+ const body = signed.slice(SIGNED_PREFIX.length);
22
+ const separator = body.lastIndexOf('.');
23
+ if (separator <= 0)
24
+ return null;
25
+ const value = body.slice(0, separator);
26
+ const provided = Buffer.from(body.slice(separator + 1));
27
+ const expected = Buffer.from(crypto.createHmac('sha256', secret).update(value).digest('base64').replace(/=+$/, ''));
28
+ if (provided.length !== expected.length)
29
+ return null;
30
+ return crypto.timingSafeEqual(provided, expected) ? value : null;
31
+ }
32
+ /** Whether a raw cookie value carries the signed marker. */
33
+ export function isSignedCookieValue(value) {
34
+ return value.startsWith(SIGNED_PREFIX);
35
+ }
1
36
  // ===== Core Logic =====
2
37
  /**
3
38
  * Parse cookies from a Cookie header string
@@ -67,18 +102,51 @@ export function buildCookieString(name, value, options = {}) {
67
102
  * Used directly by the router for route-based cookie handling
68
103
  */
69
104
  export class CookieCore {
70
- constructor() { }
105
+ secret;
106
+ constructor(secret) {
107
+ this.secret = secret;
108
+ }
71
109
  /**
72
110
  * Parse cookies from request header
73
111
  */
74
112
  parseCookies(cookieHeader) {
75
113
  return parseCookies(cookieHeader || '');
76
114
  }
115
+ /**
116
+ * Split parsed cookies into plain values and verified signed ones. A signed
117
+ * cookie whose signature doesn't verify is dropped from both — a tampered
118
+ * value must never look like a valid one.
119
+ */
120
+ splitSignedCookies(cookies) {
121
+ if (!this.secret)
122
+ return { cookies, signedCookies: {} };
123
+ const plain = {};
124
+ const signed = {};
125
+ for (const name of Object.keys(cookies)) {
126
+ const value = cookies[name];
127
+ if (!isSignedCookieValue(value)) {
128
+ plain[name] = value;
129
+ continue;
130
+ }
131
+ const verified = unsignCookieValue(value, this.secret);
132
+ if (verified !== null)
133
+ signed[name] = verified;
134
+ }
135
+ return { cookies: plain, signedCookies: signed };
136
+ }
77
137
  /**
78
138
  * Set a cookie on the response
79
139
  */
80
140
  setCookie(res, name, value, options = {}) {
81
- const cookieString = buildCookieString(name, value, options);
141
+ let outgoing = value;
142
+ if (options.signed) {
143
+ if (!this.secret) {
144
+ throw new Error(`Cookie '${name}' was set with signed: true but no secret is configured. ` +
145
+ `Pass one to the middleware: middleware.cookie({ secret: '...' }).`);
146
+ }
147
+ outgoing = signCookieValue(value, this.secret);
148
+ }
149
+ const cookieString = buildCookieString(name, outgoing, options);
82
150
  const existingCookies = res.getHeader('Set-Cookie') || [];
83
151
  // Avoid spread operator - direct array manipulation
84
152
  const cookies = Array.isArray(existingCookies) ? existingCookies : [existingCookies];
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/cookie/core.ts"],"names":[],"mappings":"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","sourcesContent":["// Cookie Core - Reusable cookie parsing and setting logic\nimport { HttpResponse } from '../../../../types/http.js';\n\n// ===== Types =====\n\nexport interface CookieOptions {\n maxAge?: number;\n expires?: Date;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'strict' | 'lax' | 'none';\n domain?: string;\n path?: string;\n}\n\n// ===== Core Logic =====\n\n/**\n * Parse cookies from a Cookie header string\n */\nexport function parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n\n if (!cookieHeader) {\n return cookies;\n }\n\n // Avoid split/forEach, use single pass with indexOf\n let start = 0;\n const len = cookieHeader.length;\n\n for (let i = 0; i <= len; i++) {\n if (i === len || cookieHeader[i] === ';') {\n if (i > start) {\n const cookie = cookieHeader.substring(start, i).trim();\n const equalIndex = cookie.indexOf('=');\n if (equalIndex > 0) {\n const name = cookie.substring(0, equalIndex);\n const value = cookie.substring(equalIndex + 1);\n if (name && value) {\n try {\n cookies[name] = decodeURIComponent(value);\n } catch {\n cookies[name] = value;\n }\n }\n }\n }\n start = i + 1;\n }\n }\n\n return cookies;\n}\n\n/**\n * Build a Set-Cookie header string from name, value, and options\n */\nexport function buildCookieString(\n name: string,\n value: string,\n options: CookieOptions = {}\n): string {\n const cookieValue = encodeURIComponent(value);\n let cookieString = `${name}=${cookieValue}`;\n\n if (options.maxAge !== undefined) {\n cookieString += `; Max-Age=${options.maxAge}`;\n }\n\n if (options.expires) {\n cookieString += `; Expires=${options.expires.toUTCString()}`;\n }\n\n if (options.httpOnly) {\n cookieString += '; HttpOnly';\n }\n\n if (options.secure) {\n cookieString += '; Secure';\n }\n\n if (options.sameSite) {\n cookieString += `; SameSite=${options.sameSite}`;\n }\n\n if (options.domain) {\n cookieString += `; Domain=${options.domain}`;\n }\n\n if (options.path) {\n cookieString += `; Path=${options.path}`;\n }\n\n return cookieString;\n}\n\n/**\n * CookieCore - Core cookie management logic\n * Used directly by the router for route-based cookie handling\n */\nexport class CookieCore {\n constructor() {}\n\n /**\n * Parse cookies from request header\n */\n parseCookies(cookieHeader: string | undefined): Record<string, string> {\n return parseCookies(cookieHeader || '');\n }\n\n /**\n * Set a cookie on the response\n */\n setCookie(res: HttpResponse, name: string, value: string, options: CookieOptions = {}): void {\n const cookieString = buildCookieString(name, value, options);\n\n const existingCookies = res.getHeader('Set-Cookie') || [];\n // Avoid spread operator - direct array manipulation\n const cookies = Array.isArray(existingCookies) ? existingCookies : [existingCookies as string];\n cookies.push(cookieString);\n res.setHeader('Set-Cookie', cookies);\n }\n\n /**\n * Clear a cookie by setting its expiration to the past\n */\n clearCookie(res: HttpResponse, name: string, options: CookieOptions = {}): void {\n // Avoid spread operator - manually set properties\n const clearOptions: CookieOptions = {\n expires: new Date(0),\n maxAge: 0,\n };\n // Copy other options manually\n if (options.path !== undefined) clearOptions.path = options.path;\n if (options.domain !== undefined) clearOptions.domain = options.domain;\n if (options.httpOnly !== undefined) clearOptions.httpOnly = options.httpOnly;\n if (options.secure !== undefined) clearOptions.secure = options.secure;\n if (options.sameSite !== undefined) clearOptions.sameSite = options.sameSite;\n this.setCookie(res, name, '', clearOptions);\n }\n}\n"]}
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/cookie/core.ts"],"names":[],"mappings":"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","sourcesContent":["// Cookie Core - Reusable cookie parsing and setting logic\nimport * as crypto from 'crypto';\nimport { HttpResponse } from '../../../../types/http.js';\n\n// ===== Types =====\n\nexport interface CookieOptions {\n maxAge?: number;\n expires?: Date;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'strict' | 'lax' | 'none';\n domain?: string;\n path?: string;\n /**\n * Sign the value so tampering can be detected. Requires a `secret` on the\n * cookie middleware; signed cookies arrive on `req.signedCookies`, never on\n * `req.cookies`.\n */\n signed?: boolean;\n}\n\n/** Prefix marking a signed value, matching the cookie-parser convention. */\nconst SIGNED_PREFIX = 's:';\n\n/** `s:<value>.<signature>` — HMAC-SHA256 of the value, base64url encoded. */\nexport function signCookieValue(value: string, secret: string): string {\n const signature = crypto\n .createHmac('sha256', secret)\n .update(value)\n .digest('base64')\n .replace(/=+$/, '');\n return `${SIGNED_PREFIX}${value}.${signature}`;\n}\n\n/**\n * Verify a signed value, returning the payload or null when the value isn't\n * signed or the signature doesn't match. Comparison is constant-time.\n */\nexport function unsignCookieValue(signed: string, secret: string): string | null {\n if (!signed.startsWith(SIGNED_PREFIX)) return null;\n\n const body = signed.slice(SIGNED_PREFIX.length);\n const separator = body.lastIndexOf('.');\n if (separator <= 0) return null;\n\n const value = body.slice(0, separator);\n const provided = Buffer.from(body.slice(separator + 1));\n const expected = Buffer.from(\n crypto.createHmac('sha256', secret).update(value).digest('base64').replace(/=+$/, '')\n );\n\n if (provided.length !== expected.length) return null;\n return crypto.timingSafeEqual(provided, expected) ? value : null;\n}\n\n/** Whether a raw cookie value carries the signed marker. */\nexport function isSignedCookieValue(value: string): boolean {\n return value.startsWith(SIGNED_PREFIX);\n}\n\n// ===== Core Logic =====\n\n/**\n * Parse cookies from a Cookie header string\n */\nexport function parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n\n if (!cookieHeader) {\n return cookies;\n }\n\n // Avoid split/forEach, use single pass with indexOf\n let start = 0;\n const len = cookieHeader.length;\n\n for (let i = 0; i <= len; i++) {\n if (i === len || cookieHeader[i] === ';') {\n if (i > start) {\n const cookie = cookieHeader.substring(start, i).trim();\n const equalIndex = cookie.indexOf('=');\n if (equalIndex > 0) {\n const name = cookie.substring(0, equalIndex);\n const value = cookie.substring(equalIndex + 1);\n if (name && value) {\n try {\n cookies[name] = decodeURIComponent(value);\n } catch {\n cookies[name] = value;\n }\n }\n }\n }\n start = i + 1;\n }\n }\n\n return cookies;\n}\n\n/**\n * Build a Set-Cookie header string from name, value, and options\n */\nexport function buildCookieString(\n name: string,\n value: string,\n options: CookieOptions = {}\n): string {\n const cookieValue = encodeURIComponent(value);\n let cookieString = `${name}=${cookieValue}`;\n\n if (options.maxAge !== undefined) {\n cookieString += `; Max-Age=${options.maxAge}`;\n }\n\n if (options.expires) {\n cookieString += `; Expires=${options.expires.toUTCString()}`;\n }\n\n if (options.httpOnly) {\n cookieString += '; HttpOnly';\n }\n\n if (options.secure) {\n cookieString += '; Secure';\n }\n\n if (options.sameSite) {\n cookieString += `; SameSite=${options.sameSite}`;\n }\n\n if (options.domain) {\n cookieString += `; Domain=${options.domain}`;\n }\n\n if (options.path) {\n cookieString += `; Path=${options.path}`;\n }\n\n return cookieString;\n}\n\n/**\n * CookieCore - Core cookie management logic\n * Used directly by the router for route-based cookie handling\n */\nexport class CookieCore {\n private secret: string | undefined;\n\n constructor(secret?: string) {\n this.secret = secret;\n }\n\n /**\n * Parse cookies from request header\n */\n parseCookies(cookieHeader: string | undefined): Record<string, string> {\n return parseCookies(cookieHeader || '');\n }\n\n /**\n * Split parsed cookies into plain values and verified signed ones. A signed\n * cookie whose signature doesn't verify is dropped from both — a tampered\n * value must never look like a valid one.\n */\n splitSignedCookies(cookies: Record<string, string>): {\n cookies: Record<string, string>;\n signedCookies: Record<string, string>;\n } {\n if (!this.secret) return { cookies, signedCookies: {} };\n\n const plain: Record<string, string> = {};\n const signed: Record<string, string> = {};\n\n for (const name of Object.keys(cookies)) {\n const value = cookies[name] as string;\n if (!isSignedCookieValue(value)) {\n plain[name] = value;\n continue;\n }\n const verified = unsignCookieValue(value, this.secret);\n if (verified !== null) signed[name] = verified;\n }\n\n return { cookies: plain, signedCookies: signed };\n }\n\n /**\n * Set a cookie on the response\n */\n setCookie(res: HttpResponse, name: string, value: string, options: CookieOptions = {}): void {\n let outgoing = value;\n if (options.signed) {\n if (!this.secret) {\n throw new Error(\n `Cookie '${name}' was set with signed: true but no secret is configured. ` +\n `Pass one to the middleware: middleware.cookie({ secret: '...' }).`\n );\n }\n outgoing = signCookieValue(value, this.secret);\n }\n\n const cookieString = buildCookieString(name, outgoing, options);\n\n const existingCookies = res.getHeader('Set-Cookie') || [];\n // Avoid spread operator - direct array manipulation\n const cookies = Array.isArray(existingCookies) ? existingCookies : [existingCookies as string];\n cookies.push(cookieString);\n res.setHeader('Set-Cookie', cookies);\n }\n\n /**\n * Clear a cookie by setting its expiration to the past\n */\n clearCookie(res: HttpResponse, name: string, options: CookieOptions = {}): void {\n // Avoid spread operator - manually set properties\n const clearOptions: CookieOptions = {\n expires: new Date(0),\n maxAge: 0,\n };\n // Copy other options manually\n if (options.path !== undefined) clearOptions.path = options.path;\n if (options.domain !== undefined) clearOptions.domain = options.domain;\n if (options.httpOnly !== undefined) clearOptions.httpOnly = options.httpOnly;\n if (options.secure !== undefined) clearOptions.secure = options.secure;\n if (options.sameSite !== undefined) clearOptions.sameSite = options.sameSite;\n this.setCookie(res, name, '', clearOptions);\n }\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  import { MiddlewareInterface } from '../../../../types/hooks.js';
2
2
  export interface CookieConfig {
3
+ /** Signing key. Required before any cookie can be set with `signed: true`. */
3
4
  secret?: string;
5
+ /** Sign every cookie by default; individual calls can still opt out. */
4
6
  signed?: boolean;
5
7
  }
6
8
  /**
@@ -17,4 +19,4 @@ export interface CookieConfig {
17
19
  * }));
18
20
  * ```
19
21
  */
20
- export declare const cookie: (_config?: CookieConfig) => MiddlewareInterface;
22
+ export declare const cookie: (config?: CookieConfig) => MiddlewareInterface;
@@ -15,7 +15,7 @@ const logger = createFrameworkLogger('CookieMiddleware');
15
15
  * }));
16
16
  * ```
17
17
  */
18
- export const cookie = (_config = {}) => ({
18
+ export const cookie = (config = {}) => ({
19
19
  name: 'cookie',
20
20
  version: '1.0.0',
21
21
  metadata: {
@@ -26,15 +26,27 @@ export const cookie = (_config = {}) => ({
26
26
  },
27
27
  install: async (hooks, _middlewareOptions = {}) => {
28
28
  logger.debug('Installing cookie middleware', 'Installation');
29
- const cookieCore = new CookieCore();
29
+ // Fail at registration rather than silently handing out unsigned cookies
30
+ if (config.signed && !config.secret) {
31
+ throw new Error('middleware.cookie({ signed: true }) requires a secret to sign with. ' +
32
+ "Pass one: middleware.cookie({ secret: '...', signed: true }).");
33
+ }
34
+ const cookieCore = new CookieCore(config.secret);
35
+ const signByDefault = config.signed === true;
30
36
  hooks.before('request', async (context) => {
31
37
  const req = context.request;
32
38
  const res = context.response;
33
- // Parse cookies from request
34
- req.cookies = cookieCore.parseCookies(req.headers.cookie);
39
+ // Parse cookies from request; signed ones are verified and kept apart
40
+ const parsed = cookieCore.parseCookies(req.headers.cookie);
41
+ const split = cookieCore.splitSignedCookies(parsed);
42
+ req.cookies = split.cookies;
43
+ req.signedCookies = split.signedCookies;
35
44
  // Add cookie methods to response
36
45
  res.cookie = (name, value, options = {}) => {
37
- cookieCore.setCookie(res, name, value, options);
46
+ cookieCore.setCookie(res, name, value, {
47
+ ...options,
48
+ signed: options.signed ?? signByDefault,
49
+ });
38
50
  return res;
39
51
  };
40
52
  res.clearCookie = (name, options = {}) => {
@@ -1 +1 @@
1
- {"version":3,"file":"hook.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/cookie/hook.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,UAAU,EAAsB,MAAM,WAAW,CAAC;AAE3D,MAAM,MAAM,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;AAOzD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,UAAwB,EAAE,EAAuB,EAAE,CAAC,CAAC;IAC1E,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,OAAO;IAChB,QAAQ,EAAE;QACR,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,8DAA8D;QAC3E,MAAM,EAAE,aAAa;KACtB;IAED,OAAO,EAAE,KAAK,EAAE,KAAU,EAAE,qBAA0B,EAAE,EAAE,EAAE;QAC1D,MAAM,CAAC,KAAK,CAAC,8BAA8B,EAAE,cAAc,CAAC,CAAC;QAE7D,MAAM,UAAU,GAAG,IAAI,UAAU,EAAE,CAAC;QAEpC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,OAAoB,EAAE,EAAE;YACrD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAc,CAAC;YACnC,MAAM,GAAG,GAAG,OAAO,CAAC,QAAe,CAAC;YAEpC,6BAA6B;YAC7B,GAAG,CAAC,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAE1D,iCAAiC;YACjC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,UAAyB,EAAE,EAAE,EAAE;gBACxE,UAAU,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;gBAChD,OAAO,GAAG,CAAC;YACb,CAAC,CAAC;YAEF,GAAG,CAAC,WAAW,GAAG,CAAC,IAAY,EAAE,UAAyB,EAAE,EAAE,EAAE;gBAC9D,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;gBAC3C,OAAO,GAAG,CAAC;YACb,CAAC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC,CAAC","sourcesContent":["// Cookie Hook - MiddlewareInterface for global registration\nimport { MiddlewareInterface, HookContext } from '../../../../types/hooks.js';\nimport { createFrameworkLogger } from '../../../logger/index.js';\nimport { CookieCore, type CookieOptions } from './core.js';\n\nconst logger = createFrameworkLogger('CookieMiddleware');\n\nexport interface CookieConfig {\n secret?: string;\n signed?: boolean;\n}\n\n/**\n * Cookie hook for global usage\n * Registers with the hooks system for application-wide cookie handling\n *\n * @example\n * ```ts\n * import { cookie } from '@/middleware/built-in/cookie';\n *\n * app.use(cookie({\n * secret: 'my-secret-key',\n * signed: true\n * }));\n * ```\n */\nexport const cookie = (_config: CookieConfig = {}): MiddlewareInterface => ({\n name: 'cookie',\n version: '1.0.0',\n metadata: {\n name: 'cookie',\n version: '1.0.0',\n description: 'Cookie parsing and setting middleware with security features',\n author: 'MoroJS Team',\n },\n\n install: async (hooks: any, _middlewareOptions: any = {}) => {\n logger.debug('Installing cookie middleware', 'Installation');\n\n const cookieCore = new CookieCore();\n\n hooks.before('request', async (context: HookContext) => {\n const req = context.request as any;\n const res = context.response as any;\n\n // Parse cookies from request\n req.cookies = cookieCore.parseCookies(req.headers.cookie);\n\n // Add cookie methods to response\n res.cookie = (name: string, value: string, options: CookieOptions = {}) => {\n cookieCore.setCookie(res, name, value, options);\n return res;\n };\n\n res.clearCookie = (name: string, options: CookieOptions = {}) => {\n cookieCore.clearCookie(res, name, options);\n return res;\n };\n });\n },\n});\n"]}
1
+ {"version":3,"file":"hook.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/cookie/hook.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,UAAU,EAAsB,MAAM,WAAW,CAAC;AAE3D,MAAM,MAAM,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;AASzD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,SAAuB,EAAE,EAAuB,EAAE,CAAC,CAAC;IACzE,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,OAAO;IAChB,QAAQ,EAAE;QACR,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,8DAA8D;QAC3E,MAAM,EAAE,aAAa;KACtB;IAED,OAAO,EAAE,KAAK,EAAE,KAAU,EAAE,qBAA0B,EAAE,EAAE,EAAE;QAC1D,MAAM,CAAC,KAAK,CAAC,8BAA8B,EAAE,cAAc,CAAC,CAAC;QAE7D,yEAAyE;QACzE,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,sEAAsE;gBACpE,+DAA+D,CAClE,CAAC;QACJ,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACjD,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC;QAE7C,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,OAAoB,EAAE,EAAE;YACrD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAc,CAAC;YACnC,MAAM,GAAG,GAAG,OAAO,CAAC,QAAe,CAAC;YAEpC,sEAAsE;YACtE,MAAM,MAAM,GAAG,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAC3D,MAAM,KAAK,GAAG,UAAU,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;YACpD,GAAG,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;YAC5B,GAAG,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC;YAExC,iCAAiC;YACjC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,UAAyB,EAAE,EAAE,EAAE;gBACxE,UAAU,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE;oBACrC,GAAG,OAAO;oBACV,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,aAAa;iBACxC,CAAC,CAAC;gBACH,OAAO,GAAG,CAAC;YACb,CAAC,CAAC;YAEF,GAAG,CAAC,WAAW,GAAG,CAAC,IAAY,EAAE,UAAyB,EAAE,EAAE,EAAE;gBAC9D,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;gBAC3C,OAAO,GAAG,CAAC;YACb,CAAC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC,CAAC","sourcesContent":["// Cookie Hook - MiddlewareInterface for global registration\nimport { MiddlewareInterface, HookContext } from '../../../../types/hooks.js';\nimport { createFrameworkLogger } from '../../../logger/index.js';\nimport { CookieCore, type CookieOptions } from './core.js';\n\nconst logger = createFrameworkLogger('CookieMiddleware');\n\nexport interface CookieConfig {\n /** Signing key. Required before any cookie can be set with `signed: true`. */\n secret?: string;\n /** Sign every cookie by default; individual calls can still opt out. */\n signed?: boolean;\n}\n\n/**\n * Cookie hook for global usage\n * Registers with the hooks system for application-wide cookie handling\n *\n * @example\n * ```ts\n * import { cookie } from '@/middleware/built-in/cookie';\n *\n * app.use(cookie({\n * secret: 'my-secret-key',\n * signed: true\n * }));\n * ```\n */\nexport const cookie = (config: CookieConfig = {}): MiddlewareInterface => ({\n name: 'cookie',\n version: '1.0.0',\n metadata: {\n name: 'cookie',\n version: '1.0.0',\n description: 'Cookie parsing and setting middleware with security features',\n author: 'MoroJS Team',\n },\n\n install: async (hooks: any, _middlewareOptions: any = {}) => {\n logger.debug('Installing cookie middleware', 'Installation');\n\n // Fail at registration rather than silently handing out unsigned cookies\n if (config.signed && !config.secret) {\n throw new Error(\n 'middleware.cookie({ signed: true }) requires a secret to sign with. ' +\n \"Pass one: middleware.cookie({ secret: '...', signed: true }).\"\n );\n }\n\n const cookieCore = new CookieCore(config.secret);\n const signByDefault = config.signed === true;\n\n hooks.before('request', async (context: HookContext) => {\n const req = context.request as any;\n const res = context.response as any;\n\n // Parse cookies from request; signed ones are verified and kept apart\n const parsed = cookieCore.parseCookies(req.headers.cookie);\n const split = cookieCore.splitSignedCookies(parsed);\n req.cookies = split.cookies;\n req.signedCookies = split.signedCookies;\n\n // Add cookie methods to response\n res.cookie = (name: string, value: string, options: CookieOptions = {}) => {\n cookieCore.setCookie(res, name, value, {\n ...options,\n signed: options.signed ?? signByDefault,\n });\n return res;\n };\n\n res.clearCookie = (name: string, options: CookieOptions = {}) => {\n cookieCore.clearCookie(res, name, options);\n return res;\n };\n });\n },\n});\n"]}
@@ -1,3 +1,3 @@
1
- export { CookieCore, parseCookies, buildCookieString, type CookieOptions } from './core.js';
1
+ export { CookieCore, parseCookies, buildCookieString, signCookieValue, unsignCookieValue, type CookieOptions, } from './core.js';
2
2
  export { createCookieMiddleware } from './middleware.js';
3
3
  export { cookie, type CookieConfig } from './hook.js';
@@ -1,7 +1,7 @@
1
1
  // Cookie - Main entry point
2
2
  // Re-exports all public APIs for the cookie built-in
3
3
  // Core (for direct use by router and custom implementations)
4
- export { CookieCore, parseCookies, buildCookieString } from './core.js';
4
+ export { CookieCore, parseCookies, buildCookieString, signCookieValue, unsignCookieValue, } from './core.js';
5
5
  // Middleware (for middleware chains)
6
6
  export { createCookieMiddleware } from './middleware.js';
7
7
  // Hook (for global registration)
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/cookie/index.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,qDAAqD;AAErD,6DAA6D;AAC7D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAsB,MAAM,WAAW,CAAC;AAE5F,qCAAqC;AACrC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,iCAAiC;AACjC,OAAO,EAAE,MAAM,EAAqB,MAAM,WAAW,CAAC","sourcesContent":["// Cookie - Main entry point\n// Re-exports all public APIs for the cookie built-in\n\n// Core (for direct use by router and custom implementations)\nexport { CookieCore, parseCookies, buildCookieString, type CookieOptions } from './core.js';\n\n// Middleware (for middleware chains)\nexport { createCookieMiddleware } from './middleware.js';\n\n// Hook (for global registration)\nexport { cookie, type CookieConfig } from './hook.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/cookie/index.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,qDAAqD;AAErD,6DAA6D;AAC7D,OAAO,EACL,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,iBAAiB,GAElB,MAAM,WAAW,CAAC;AAEnB,qCAAqC;AACrC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,iCAAiC;AACjC,OAAO,EAAE,MAAM,EAAqB,MAAM,WAAW,CAAC","sourcesContent":["// Cookie - Main entry point\n// Re-exports all public APIs for the cookie built-in\n\n// Core (for direct use by router and custom implementations)\nexport {\n CookieCore,\n parseCookies,\n buildCookieString,\n signCookieValue,\n unsignCookieValue,\n type CookieOptions,\n} from './core.js';\n\n// Middleware (for middleware chains)\nexport { createCookieMiddleware } from './middleware.js';\n\n// Hook (for global registration)\nexport { cookie, type CookieConfig } from './hook.js';\n"]}
@@ -14,4 +14,4 @@ import { StandardMiddleware } from '../../../../types/hooks.js';
14
14
  * res.clearCookie('user')
15
15
  * ```
16
16
  */
17
- export declare function createCookieMiddleware(): StandardMiddleware;
17
+ export declare function createCookieMiddleware(secret?: string): StandardMiddleware;
@@ -14,13 +14,15 @@ import { CookieCore } from './core.js';
14
14
  * res.clearCookie('user')
15
15
  * ```
16
16
  */
17
- export function createCookieMiddleware() {
18
- const cookieCore = new CookieCore();
17
+ export function createCookieMiddleware(secret) {
18
+ const cookieCore = new CookieCore(secret);
19
19
  return async (req, res, next) => {
20
20
  const reqAny = req;
21
21
  const resAny = res;
22
- // Parse cookies from request
23
- reqAny.cookies = cookieCore.parseCookies(req.headers.cookie);
22
+ // Parse cookies from request; signed values are verified and kept apart
23
+ const split = cookieCore.splitSignedCookies(cookieCore.parseCookies(req.headers.cookie));
24
+ reqAny.cookies = split.cookies;
25
+ reqAny.signedCookies = split.signedCookies;
24
26
  // Add cookie methods to response
25
27
  resAny.cookie = (name, value, options = {}) => {
26
28
  cookieCore.setCookie(res, name, value, options);
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/cookie/middleware.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAsB,MAAM,WAAW,CAAC;AAE3D;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,sBAAsB;IACpC,MAAM,UAAU,GAAG,IAAI,UAAU,EAAE,CAAC;IAEpC,OAAO,KAAK,EAAE,GAAgB,EAAE,GAAiB,EAAE,IAAyB,EAAE,EAAE;QAC9E,MAAM,MAAM,GAAG,GAAU,CAAC;QAC1B,MAAM,MAAM,GAAG,GAAU,CAAC;QAE1B,6BAA6B;QAC7B,MAAM,CAAC,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAE7D,iCAAiC;QACjC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,UAAyB,EAAE,EAAE,EAAE;YAC3E,UAAU,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAChD,OAAO,GAAG,CAAC;QACb,CAAC,CAAC;QAEF,MAAM,CAAC,WAAW,GAAG,CAAC,IAAY,EAAE,UAAyB,EAAE,EAAE,EAAE;YACjE,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;YAC3C,OAAO,GAAG,CAAC;QACb,CAAC,CAAC;QAEF,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Cookie Middleware - Standard (req, res, next) middleware function\nimport { StandardMiddleware } from '../../../../types/hooks.js';\nimport { HttpRequest, HttpResponse } from '../../../../types/http.js';\nimport { CookieCore, type CookieOptions } from './core.js';\n\n/**\n * Create cookie middleware for use in middleware chains\n * Adds cookie parsing and response methods to req/res\n *\n * @example\n * ```ts\n * const cookieMw = createCookieMiddleware();\n * app.use(cookieMw);\n *\n * // In routes:\n * req.cookies // { sessionId: '123' }\n * res.cookie('user', 'john', { httpOnly: true })\n * res.clearCookie('user')\n * ```\n */\nexport function createCookieMiddleware(): StandardMiddleware {\n const cookieCore = new CookieCore();\n\n return async (req: HttpRequest, res: HttpResponse, next: () => Promise<void>) => {\n const reqAny = req as any;\n const resAny = res as any;\n\n // Parse cookies from request\n reqAny.cookies = cookieCore.parseCookies(req.headers.cookie);\n\n // Add cookie methods to response\n resAny.cookie = (name: string, value: string, options: CookieOptions = {}) => {\n cookieCore.setCookie(res, name, value, options);\n return res;\n };\n\n resAny.clearCookie = (name: string, options: CookieOptions = {}) => {\n cookieCore.clearCookie(res, name, options);\n return res;\n };\n\n await next();\n };\n}\n"]}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/cookie/middleware.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAsB,MAAM,WAAW,CAAC;AAE3D;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAe;IACpD,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IAE1C,OAAO,KAAK,EAAE,GAAgB,EAAE,GAAiB,EAAE,IAAyB,EAAE,EAAE;QAC9E,MAAM,MAAM,GAAG,GAAU,CAAC;QAC1B,MAAM,MAAM,GAAG,GAAU,CAAC;QAE1B,wEAAwE;QACxE,MAAM,KAAK,GAAG,UAAU,CAAC,kBAAkB,CAAC,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACzF,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC/B,MAAM,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC;QAE3C,iCAAiC;QACjC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,UAAyB,EAAE,EAAE,EAAE;YAC3E,UAAU,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAChD,OAAO,GAAG,CAAC;QACb,CAAC,CAAC;QAEF,MAAM,CAAC,WAAW,GAAG,CAAC,IAAY,EAAE,UAAyB,EAAE,EAAE,EAAE;YACjE,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;YAC3C,OAAO,GAAG,CAAC;QACb,CAAC,CAAC;QAEF,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Cookie Middleware - Standard (req, res, next) middleware function\nimport { StandardMiddleware } from '../../../../types/hooks.js';\nimport { HttpRequest, HttpResponse } from '../../../../types/http.js';\nimport { CookieCore, type CookieOptions } from './core.js';\n\n/**\n * Create cookie middleware for use in middleware chains\n * Adds cookie parsing and response methods to req/res\n *\n * @example\n * ```ts\n * const cookieMw = createCookieMiddleware();\n * app.use(cookieMw);\n *\n * // In routes:\n * req.cookies // { sessionId: '123' }\n * res.cookie('user', 'john', { httpOnly: true })\n * res.clearCookie('user')\n * ```\n */\nexport function createCookieMiddleware(secret?: string): StandardMiddleware {\n const cookieCore = new CookieCore(secret);\n\n return async (req: HttpRequest, res: HttpResponse, next: () => Promise<void>) => {\n const reqAny = req as any;\n const resAny = res as any;\n\n // Parse cookies from request; signed values are verified and kept apart\n const split = cookieCore.splitSignedCookies(cookieCore.parseCookies(req.headers.cookie));\n reqAny.cookies = split.cookies;\n reqAny.signedCookies = split.signedCookies;\n\n // Add cookie methods to response\n resAny.cookie = (name: string, value: string, options: CookieOptions = {}) => {\n cookieCore.setCookie(res, name, value, options);\n return res;\n };\n\n resAny.clearCookie = (name: string, options: CookieOptions = {}) => {\n cookieCore.clearCookie(res, name, options);\n return res;\n };\n\n await next();\n };\n}\n"]}
@@ -28,11 +28,7 @@ export { safeVerifyJWT, extractJWTFromHeader, createAuthErrorResponse, type JWTV
28
28
  export { extendedProviders, enterpriseProviders, createCustomOAuthProvider, createCustomOIDCProvider, } from './auth/providers.js';
29
29
  export declare const builtInMiddleware: {
30
30
  auth: (options: import("./auth/index.js").AuthOptions) => import("../index.js").MiddlewareInterface;
31
- rateLimit: (options?: {
32
- windowMs?: number;
33
- max?: number;
34
- message?: string;
35
- }) => import("../index.js").MiddlewareInterface;
31
+ rateLimit: (options?: import("./rate-limit/hook.js").RateLimitHookOptions) => import("../index.js").MiddlewareInterface;
36
32
  cors: (options?: import("./cors/core.js").CORSOptions) => import("../index.js").MiddlewareInterface;
37
33
  validation: () => import("../index.js").MiddlewareInterface;
38
34
  requestLogger: typeof import("./request-logger/middleware.js").createRequestLoggerMiddleware;
@@ -40,7 +36,7 @@ export declare const builtInMiddleware: {
40
36
  errorTracker: typeof import("./error-tracker/middleware.js").createErrorTrackerMiddleware;
41
37
  prometheus: typeof import("./prometheus/middleware.js").createPrometheusMiddleware;
42
38
  cloudWatch: typeof import("./cloudwatch/middleware.js").createCloudWatchMiddleware;
43
- cookie: (_config?: import("./cookie/hook.js").CookieConfig) => import("../index.js").MiddlewareInterface;
39
+ cookie: (config?: import("./cookie/hook.js").CookieConfig) => import("../index.js").MiddlewareInterface;
44
40
  csrf: (options?: import("./csrf/core.js").CSRFOptions) => import("../index.js").MiddlewareInterface;
45
41
  csp: (options?: import("./csp/core.js").CSPOptions) => import("../index.js").MiddlewareInterface;
46
42
  sse: (options?: import("./sse/core.js").SSEOptions) => import("../index.js").MiddlewareInterface;
@@ -61,11 +57,7 @@ export declare const builtInMiddleware: {
61
57
  };
62
58
  export declare const middleware: {
63
59
  auth: (options: import("./auth/index.js").AuthOptions) => import("../index.js").MiddlewareInterface;
64
- rateLimit: (options?: {
65
- windowMs?: number;
66
- max?: number;
67
- message?: string;
68
- }) => import("../index.js").MiddlewareInterface;
60
+ rateLimit: (options?: import("./rate-limit/hook.js").RateLimitHookOptions) => import("../index.js").MiddlewareInterface;
69
61
  cors: (options?: import("./cors/core.js").CORSOptions) => import("../index.js").MiddlewareInterface;
70
62
  validation: () => import("../index.js").MiddlewareInterface;
71
63
  requestLogger: typeof import("./request-logger/middleware.js").createRequestLoggerMiddleware;
@@ -73,7 +65,7 @@ export declare const middleware: {
73
65
  errorTracker: typeof import("./error-tracker/middleware.js").createErrorTrackerMiddleware;
74
66
  prometheus: typeof import("./prometheus/middleware.js").createPrometheusMiddleware;
75
67
  cloudWatch: typeof import("./cloudwatch/middleware.js").createCloudWatchMiddleware;
76
- cookie: (_config?: import("./cookie/hook.js").CookieConfig) => import("../index.js").MiddlewareInterface;
68
+ cookie: (config?: import("./cookie/hook.js").CookieConfig) => import("../index.js").MiddlewareInterface;
77
69
  csrf: (options?: import("./csrf/core.js").CSRFOptions) => import("../index.js").MiddlewareInterface;
78
70
  csp: (options?: import("./csp/core.js").CSPOptions) => import("../index.js").MiddlewareInterface;
79
71
  sse: (options?: import("./sse/core.js").SSEOptions) => import("../index.js").MiddlewareInterface;
@@ -2,8 +2,26 @@ import { HttpRequest, HttpResponse } from '../../../../types/http.js';
2
2
  export interface RateLimitConfig {
3
3
  requests: number;
4
4
  window: number;
5
+ /**
6
+ * Don't count requests that completed successfully (status < 400) against the
7
+ * limit — the request is counted up front and refunded once the response
8
+ * finishes, so a burst still can't exceed the limit mid-flight.
9
+ */
5
10
  skipSuccessfulRequests?: boolean;
11
+ /** The mirror of the above: don't count requests that failed (status >= 400). */
12
+ skipFailedRequests?: boolean;
6
13
  }
14
+ /** Whether a finished response should give its counted request back. */
15
+ export declare function shouldRefund(statusCode: number, config: {
16
+ skipSuccessfulRequests?: boolean;
17
+ skipFailedRequests?: boolean;
18
+ }): boolean;
19
+ /**
20
+ * Run `cb` once the response has been fully sent. Prefers the 'finish' event
21
+ * (node, engine and uWS responses all emit it) and falls back to wrapping
22
+ * end() for the HTTP/2 response, which is a plain object with no emitter.
23
+ */
24
+ export declare function onResponseFinished(res: HttpResponse, cb: () => void): void;
7
25
  /**
8
26
  * RateLimitCore - Core rate limiting logic
9
27
  * Used directly by the router for route-based rate limiting
@@ -33,6 +51,12 @@ export declare class RateLimitCore {
33
51
  * Returns true if request is allowed, false if rate limit exceeded
34
52
  */
35
53
  check(clientId: string, routeKey: string, requests: number, window: number): boolean;
54
+ /**
55
+ * Give a counted request back — used by skipSuccessfulRequests once a
56
+ * response finishes successfully. Never drops below zero, and ignores
57
+ * entries whose window has already rolled over.
58
+ */
59
+ refund(clientId: string, routeKey: string): void;
36
60
  /**
37
61
  * Get retry-after time in seconds for a rate-limited client
38
62
  */
@@ -1,6 +1,31 @@
1
1
  // Rate Limit Core - Reusable rate limiting logic
2
2
  import { createFrameworkLogger } from '../../../logger/index.js';
3
3
  const logger = createFrameworkLogger('RateLimitCore');
4
+ /** Whether a finished response should give its counted request back. */
5
+ export function shouldRefund(statusCode, config) {
6
+ const failed = statusCode >= 400;
7
+ return failed ? !!config.skipFailedRequests : !!config.skipSuccessfulRequests;
8
+ }
9
+ /**
10
+ * Run `cb` once the response has been fully sent. Prefers the 'finish' event
11
+ * (node, engine and uWS responses all emit it) and falls back to wrapping
12
+ * end() for the HTTP/2 response, which is a plain object with no emitter.
13
+ */
14
+ export function onResponseFinished(res, cb) {
15
+ const emitter = res;
16
+ if (typeof emitter.once === 'function') {
17
+ emitter.once('finish', cb);
18
+ return;
19
+ }
20
+ const originalEnd = emitter.end;
21
+ if (typeof originalEnd !== 'function')
22
+ return;
23
+ emitter.end = function patchedEnd(...args) {
24
+ const result = originalEnd.apply(this, args);
25
+ cb();
26
+ return result;
27
+ };
28
+ }
4
29
  // ===== Core Logic =====
5
30
  /**
6
31
  * RateLimitCore - Core rate limiting logic
@@ -52,6 +77,7 @@ export class RateLimitCore {
52
77
  const allowed = this.check(clientId, routeKey, config.requests, config.window);
53
78
  if (!allowed) {
54
79
  const retryAfter = this.getRetryAfter(clientId, routeKey);
80
+ res.setHeader('Retry-After', String(retryAfter));
55
81
  res.status(429).json({
56
82
  success: false,
57
83
  error: 'Rate limit exceeded',
@@ -59,6 +85,12 @@ export class RateLimitCore {
59
85
  });
60
86
  return;
61
87
  }
88
+ if (config.skipSuccessfulRequests || config.skipFailedRequests) {
89
+ onResponseFinished(res, () => {
90
+ if (shouldRefund(res.statusCode || 200, config))
91
+ this.refund(clientId, routeKey);
92
+ });
93
+ }
62
94
  }
63
95
  /**
64
96
  * Low-level check method
@@ -101,6 +133,21 @@ export class RateLimitCore {
101
133
  limitData.count++;
102
134
  return true;
103
135
  }
136
+ /**
137
+ * Give a counted request back — used by skipSuccessfulRequests once a
138
+ * response finishes successfully. Never drops below zero, and ignores
139
+ * entries whose window has already rolled over.
140
+ */
141
+ refund(clientId, routeKey) {
142
+ const key = `${routeKey}:${clientId}`;
143
+ const limitData = this.store.get(key);
144
+ if (!limitData)
145
+ return;
146
+ if (RateLimitCore.getTime() > limitData.resetTime)
147
+ return;
148
+ if (limitData.count > 0)
149
+ limitData.count--;
150
+ }
104
151
  /**
105
152
  * Get retry-after time in seconds for a rate-limited client
106
153
  */
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/rate-limit/core.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,MAAM,MAAM,GAAG,qBAAqB,CAAC,eAAe,CAAC,CAAC;AAetD,yBAAyB;AAEzB;;;;GAIG;AACH,MAAM,OAAO,aAAa;IAChB,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC1C,MAAM,CAAU,cAAc,GAAG,MAAM,CAAC;IACxC,YAAY,GAA0C,IAAI,CAAC;IAEnE,mCAAmC;IAC3B,MAAM,CAAU,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAE/C;QACE,yEAAyE;QACzE,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,CAAC,CAAC;QAClE,gDAAgD;QAChD,IAAI,IAAI,CAAC,YAAY,IAAI,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YACvE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IAED;;OAEG;IACK,MAAM,CAAC,OAAO;QACpB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC;IAC9C,CAAC;IAED;;OAEG;IACK,YAAY;QAClB,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,EAAE,CAAC;QACpC,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACrC,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;gBACzB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,GAAgB,EAAE,GAAiB,EAAE,MAAuB;QAC3E,8CAA8C;QAC9C,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,GAAG,CAAC,EAAE,IAAK,GAAG,CAAC,UAAkB,EAAE,aAAa,IAAI,SAAS,CAAC;QAC/E,MAAM,QAAQ,GAAG,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QAE7C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAE/E,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAC1D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,qBAAqB;gBAC5B,UAAU;aACX,CAAC,CAAC;YACH,OAAO;QACT,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAgB,EAAE,QAAgB,EAAE,QAAgB,EAAE,MAAc;QACxE,MAAM,GAAG,GAAG,GAAG,QAAQ,IAAI,QAAQ,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,EAAE,CAAC;QAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,6DAA6D;YAC7D,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,aAAa,CAAC,cAAc,EAAE,CAAC;gBACpD,IAAI,CAAC,YAAY,EAAE,CAAC;gBACpB,0DAA0D;gBAC1D,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,aAAa,CAAC,cAAc,EAAE,CAAC;oBACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;oBAChD,IAAI,QAAQ;wBAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAC5C,CAAC;YACH,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,GAAG,MAAM,EAAE,CAAC,CAAC;YAC3D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,qCAAqC;QACrC,IAAI,GAAG,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC;YAC9B,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC;YACpB,SAAS,CAAC,SAAS,GAAG,GAAG,GAAG,MAAM,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,kCAAkC;QAClC,IAAI,SAAS,CAAC,KAAK,IAAI,QAAQ,EAAE,CAAC;YAChC,MAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE,WAAW,EAAE;gBAC9C,QAAQ;gBACR,KAAK,EAAE,QAAQ;gBACf,KAAK,EAAE,SAAS,CAAC,KAAK;gBACtB,KAAK,EAAE,QAAQ;aAChB,CAAC,CAAC;YACH,OAAO,KAAK,CAAC;QACf,CAAC;QAED,SAAS,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,QAAgB,EAAE,QAAgB;QAC9C,MAAM,GAAG,GAAG,GAAG,QAAQ,IAAI,QAAQ,EAAE,CAAC;QACtC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,SAAS,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;QACvD,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACjC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;IACH,CAAC;;AAGH,gDAAgD;AAChD,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,aAAa,EAAE,CAAC","sourcesContent":["// Rate Limit Core - Reusable rate limiting logic\nimport { createFrameworkLogger } from '../../../logger/index.js';\nimport { HttpRequest, HttpResponse } from '../../../../types/http.js';\n\nconst logger = createFrameworkLogger('RateLimitCore');\n\n// ===== Types =====\n\nexport interface RateLimitConfig {\n requests: number;\n window: number;\n skipSuccessfulRequests?: boolean;\n}\n\ninterface RateLimitStore {\n count: number;\n resetTime: number;\n}\n\n// ===== Core Logic =====\n\n/**\n * RateLimitCore - Core rate limiting logic\n * Used directly by the router for route-based rate limiting\n * Can be instantiated for use in middleware or hooks\n */\nexport class RateLimitCore {\n private store = new Map<string, RateLimitStore>();\n private static readonly MAX_STORE_SIZE = 100000;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n // Monotonic timestamp optimization\n private static readonly startTime = Date.now();\n\n constructor() {\n // Periodic cleanup of expired entries to prevent unbounded memory growth\n this.cleanupTimer = setInterval(() => this.evictExpired(), 60000);\n // Allow the timer to not keep the process alive\n if (this.cleanupTimer && typeof this.cleanupTimer.unref === 'function') {\n this.cleanupTimer.unref();\n }\n }\n\n /**\n * Get monotonic timestamp (smaller integers for better JIT optimization)\n */\n private static getTime(): number {\n return Date.now() - RateLimitCore.startTime;\n }\n\n /**\n * Evict expired entries from the store\n */\n private evictExpired(): void {\n const now = RateLimitCore.getTime();\n for (const [key, data] of this.store) {\n if (now > data.resetTime) {\n this.store.delete(key);\n }\n }\n }\n\n /**\n * High-level check for router use: checkLimit(req, res, config)\n * Sends response if rate limit exceeded\n */\n async checkLimit(req: HttpRequest, res: HttpResponse, config: RateLimitConfig): Promise<void> {\n // Don't send response if headers already sent\n if (res.headersSent) {\n return;\n }\n\n const clientId = req.ip || (req.connection as any)?.remoteAddress || 'unknown';\n const routeKey = `${req.method}:${req.path}`;\n\n const allowed = this.check(clientId, routeKey, config.requests, config.window);\n\n if (!allowed) {\n const retryAfter = this.getRetryAfter(clientId, routeKey);\n res.status(429).json({\n success: false,\n error: 'Rate limit exceeded',\n retryAfter,\n });\n return;\n }\n }\n\n /**\n * Low-level check method\n * Returns true if request is allowed, false if rate limit exceeded\n */\n check(clientId: string, routeKey: string, requests: number, window: number): boolean {\n const key = `${routeKey}:${clientId}`;\n const now = RateLimitCore.getTime();\n\n const limitData = this.store.get(key);\n if (!limitData) {\n // Safety cap: evict oldest entries if store exceeds max size\n if (this.store.size >= RateLimitCore.MAX_STORE_SIZE) {\n this.evictExpired();\n // If still over limit after eviction, remove oldest entry\n if (this.store.size >= RateLimitCore.MAX_STORE_SIZE) {\n const firstKey = this.store.keys().next().value;\n if (firstKey) this.store.delete(firstKey);\n }\n }\n this.store.set(key, { count: 1, resetTime: now + window });\n return true;\n }\n\n // Fast path: check if window expired\n if (now > limitData.resetTime) {\n limitData.count = 1;\n limitData.resetTime = now + window;\n return true;\n }\n\n // Check limit before incrementing\n if (limitData.count >= requests) {\n logger.warn('Rate limit exceeded', 'RateLimit', {\n clientId,\n route: routeKey,\n count: limitData.count,\n limit: requests,\n });\n return false;\n }\n\n limitData.count++;\n return true;\n }\n\n /**\n * Get retry-after time in seconds for a rate-limited client\n */\n getRetryAfter(clientId: string, routeKey: string): number {\n const key = `${routeKey}:${clientId}`;\n const limitData = this.store.get(key);\n if (limitData) {\n const now = RateLimitCore.getTime();\n return Math.ceil((limitData.resetTime - now) / 1000);\n }\n return 0;\n }\n\n /**\n * Clear all rate limit data and stop cleanup timer\n */\n clear(): void {\n this.store.clear();\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n\n// Shared instance for route-based rate limiting\nexport const sharedRateLimitCore = new RateLimitCore();\n"]}
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/rate-limit/core.ts"],"names":[],"mappings":"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","sourcesContent":["// Rate Limit Core - Reusable rate limiting logic\nimport { createFrameworkLogger } from '../../../logger/index.js';\nimport { HttpRequest, HttpResponse } from '../../../../types/http.js';\n\nconst logger = createFrameworkLogger('RateLimitCore');\n\n// ===== Types =====\n\nexport interface RateLimitConfig {\n requests: number;\n window: number;\n /**\n * Don't count requests that completed successfully (status < 400) against the\n * limit — the request is counted up front and refunded once the response\n * finishes, so a burst still can't exceed the limit mid-flight.\n */\n skipSuccessfulRequests?: boolean;\n /** The mirror of the above: don't count requests that failed (status >= 400). */\n skipFailedRequests?: boolean;\n}\n\n/** Whether a finished response should give its counted request back. */\nexport function shouldRefund(\n statusCode: number,\n config: { skipSuccessfulRequests?: boolean; skipFailedRequests?: boolean }\n): boolean {\n const failed = statusCode >= 400;\n return failed ? !!config.skipFailedRequests : !!config.skipSuccessfulRequests;\n}\n\n/**\n * Run `cb` once the response has been fully sent. Prefers the 'finish' event\n * (node, engine and uWS responses all emit it) and falls back to wrapping\n * end() for the HTTP/2 response, which is a plain object with no emitter.\n */\nexport function onResponseFinished(res: HttpResponse, cb: () => void): void {\n const emitter = res as any;\n if (typeof emitter.once === 'function') {\n emitter.once('finish', cb);\n return;\n }\n\n const originalEnd = emitter.end;\n if (typeof originalEnd !== 'function') return;\n emitter.end = function patchedEnd(this: unknown, ...args: unknown[]) {\n const result = originalEnd.apply(this, args);\n cb();\n return result;\n };\n}\n\ninterface RateLimitStore {\n count: number;\n resetTime: number;\n}\n\n// ===== Core Logic =====\n\n/**\n * RateLimitCore - Core rate limiting logic\n * Used directly by the router for route-based rate limiting\n * Can be instantiated for use in middleware or hooks\n */\nexport class RateLimitCore {\n private store = new Map<string, RateLimitStore>();\n private static readonly MAX_STORE_SIZE = 100000;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n // Monotonic timestamp optimization\n private static readonly startTime = Date.now();\n\n constructor() {\n // Periodic cleanup of expired entries to prevent unbounded memory growth\n this.cleanupTimer = setInterval(() => this.evictExpired(), 60000);\n // Allow the timer to not keep the process alive\n if (this.cleanupTimer && typeof this.cleanupTimer.unref === 'function') {\n this.cleanupTimer.unref();\n }\n }\n\n /**\n * Get monotonic timestamp (smaller integers for better JIT optimization)\n */\n private static getTime(): number {\n return Date.now() - RateLimitCore.startTime;\n }\n\n /**\n * Evict expired entries from the store\n */\n private evictExpired(): void {\n const now = RateLimitCore.getTime();\n for (const [key, data] of this.store) {\n if (now > data.resetTime) {\n this.store.delete(key);\n }\n }\n }\n\n /**\n * High-level check for router use: checkLimit(req, res, config)\n * Sends response if rate limit exceeded\n */\n async checkLimit(req: HttpRequest, res: HttpResponse, config: RateLimitConfig): Promise<void> {\n // Don't send response if headers already sent\n if (res.headersSent) {\n return;\n }\n\n const clientId = req.ip || (req.connection as any)?.remoteAddress || 'unknown';\n const routeKey = `${req.method}:${req.path}`;\n\n const allowed = this.check(clientId, routeKey, config.requests, config.window);\n\n if (!allowed) {\n const retryAfter = this.getRetryAfter(clientId, routeKey);\n res.setHeader('Retry-After', String(retryAfter));\n res.status(429).json({\n success: false,\n error: 'Rate limit exceeded',\n retryAfter,\n });\n return;\n }\n\n if (config.skipSuccessfulRequests || config.skipFailedRequests) {\n onResponseFinished(res, () => {\n if (shouldRefund(res.statusCode || 200, config)) this.refund(clientId, routeKey);\n });\n }\n }\n\n /**\n * Low-level check method\n * Returns true if request is allowed, false if rate limit exceeded\n */\n check(clientId: string, routeKey: string, requests: number, window: number): boolean {\n const key = `${routeKey}:${clientId}`;\n const now = RateLimitCore.getTime();\n\n const limitData = this.store.get(key);\n if (!limitData) {\n // Safety cap: evict oldest entries if store exceeds max size\n if (this.store.size >= RateLimitCore.MAX_STORE_SIZE) {\n this.evictExpired();\n // If still over limit after eviction, remove oldest entry\n if (this.store.size >= RateLimitCore.MAX_STORE_SIZE) {\n const firstKey = this.store.keys().next().value;\n if (firstKey) this.store.delete(firstKey);\n }\n }\n this.store.set(key, { count: 1, resetTime: now + window });\n return true;\n }\n\n // Fast path: check if window expired\n if (now > limitData.resetTime) {\n limitData.count = 1;\n limitData.resetTime = now + window;\n return true;\n }\n\n // Check limit before incrementing\n if (limitData.count >= requests) {\n logger.warn('Rate limit exceeded', 'RateLimit', {\n clientId,\n route: routeKey,\n count: limitData.count,\n limit: requests,\n });\n return false;\n }\n\n limitData.count++;\n return true;\n }\n\n /**\n * Give a counted request back — used by skipSuccessfulRequests once a\n * response finishes successfully. Never drops below zero, and ignores\n * entries whose window has already rolled over.\n */\n refund(clientId: string, routeKey: string): void {\n const key = `${routeKey}:${clientId}`;\n const limitData = this.store.get(key);\n if (!limitData) return;\n if (RateLimitCore.getTime() > limitData.resetTime) return;\n if (limitData.count > 0) limitData.count--;\n }\n\n /**\n * Get retry-after time in seconds for a rate-limited client\n */\n getRetryAfter(clientId: string, routeKey: string): number {\n const key = `${routeKey}:${clientId}`;\n const limitData = this.store.get(key);\n if (limitData) {\n const now = RateLimitCore.getTime();\n return Math.ceil((limitData.resetTime - now) / 1000);\n }\n return 0;\n }\n\n /**\n * Clear all rate limit data and stop cleanup timer\n */\n clear(): void {\n this.store.clear();\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n\n// Shared instance for route-based rate limiting\nexport const sharedRateLimitCore = new RateLimitCore();\n"]}
@@ -1,20 +1,35 @@
1
1
  import { MiddlewareInterface } from '../../../../types/hooks.js';
2
+ export interface RateLimitHookOptions {
3
+ /** Requests allowed per window. `max` is the express-rate-limit spelling. */
4
+ requests?: number;
5
+ max?: number;
6
+ /** Window length in milliseconds. `windowMs` is the express-rate-limit spelling. */
7
+ window?: number;
8
+ windowMs?: number;
9
+ /** Body message for a rejected request. Default: 'Too many requests'. */
10
+ message?: string;
11
+ /** Status for a rejected request. Default: 429. */
12
+ statusCode?: number;
13
+ /** Only count requests that did not complete successfully. */
14
+ skipSuccessfulRequests?: boolean;
15
+ /** Only count requests that did complete successfully. */
16
+ skipFailedRequests?: boolean;
17
+ }
2
18
  /**
3
19
  * Rate limit hook for global usage
4
20
  * Registers with the hooks system for application-wide rate limiting
5
21
  *
22
+ * Rejected requests are answered directly with the configured status (429 by
23
+ * default) and a `Retry-After` header — the request never reaches your routes.
24
+ *
6
25
  * @example
7
26
  * ```ts
8
- * import { rateLimit } from '@/middleware/built-in/rate-limit';
27
+ * import { middleware } from '@morojs/moro';
9
28
  *
10
- * app.use(rateLimit({
11
- * windowMs: 60000, // 1 minute
12
- * max: 100 // 100 requests per window
29
+ * app.use(middleware.rateLimit({
30
+ * requests: 100, // or max: 100
31
+ * window: 60000, // or windowMs: 60000
13
32
  * }));
14
33
  * ```
15
34
  */
16
- export declare const rateLimit: (options?: {
17
- windowMs?: number;
18
- max?: number;
19
- message?: string;
20
- }) => MiddlewareInterface;
35
+ export declare const rateLimit: (options?: RateLimitHookOptions) => MiddlewareInterface;