qhttpx 1.8.0

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 (197) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/.github/workflows/npm-publish.yml +37 -0
  4. package/.github/workflows/release.yml +21 -0
  5. package/.prettierrc +7 -0
  6. package/CHANGELOG.md +145 -0
  7. package/LICENSE +21 -0
  8. package/README.md +343 -0
  9. package/dist/package.json +61 -0
  10. package/dist/src/benchmarks/compare-frameworks.js +119 -0
  11. package/dist/src/benchmarks/quantam-users.js +56 -0
  12. package/dist/src/benchmarks/simple-json.js +58 -0
  13. package/dist/src/benchmarks/ultra-mode.js +122 -0
  14. package/dist/src/cli/index.js +200 -0
  15. package/dist/src/client/index.js +72 -0
  16. package/dist/src/core/batch.js +97 -0
  17. package/dist/src/core/body-parser.js +121 -0
  18. package/dist/src/core/buffer-pool.js +70 -0
  19. package/dist/src/core/config.js +50 -0
  20. package/dist/src/core/fusion.js +183 -0
  21. package/dist/src/core/logger.js +49 -0
  22. package/dist/src/core/metrics.js +111 -0
  23. package/dist/src/core/resources.js +25 -0
  24. package/dist/src/core/scheduler.js +85 -0
  25. package/dist/src/core/scope.js +68 -0
  26. package/dist/src/core/serializer.js +44 -0
  27. package/dist/src/core/server.js +905 -0
  28. package/dist/src/core/stream.js +71 -0
  29. package/dist/src/core/tasks.js +87 -0
  30. package/dist/src/core/types.js +19 -0
  31. package/dist/src/core/websocket.js +86 -0
  32. package/dist/src/core/worker-queue.js +73 -0
  33. package/dist/src/database/adapters/memory.js +90 -0
  34. package/dist/src/database/adapters/mongo.js +141 -0
  35. package/dist/src/database/adapters/postgres.js +111 -0
  36. package/dist/src/database/adapters/sqlite.js +42 -0
  37. package/dist/src/database/coalescer.js +134 -0
  38. package/dist/src/database/manager.js +87 -0
  39. package/dist/src/database/types.js +2 -0
  40. package/dist/src/index.js +61 -0
  41. package/dist/src/middleware/compression.js +133 -0
  42. package/dist/src/middleware/cors.js +66 -0
  43. package/dist/src/middleware/presets.js +33 -0
  44. package/dist/src/middleware/rate-limit.js +77 -0
  45. package/dist/src/middleware/security.js +69 -0
  46. package/dist/src/middleware/static.js +191 -0
  47. package/dist/src/openapi/generator.js +149 -0
  48. package/dist/src/router/radix-router.js +89 -0
  49. package/dist/src/router/radix-tree.js +81 -0
  50. package/dist/src/router/router.js +146 -0
  51. package/dist/src/testing/index.js +84 -0
  52. package/dist/src/utils/cookies.js +59 -0
  53. package/dist/src/utils/logger.js +45 -0
  54. package/dist/src/utils/signals.js +31 -0
  55. package/dist/src/utils/sse.js +32 -0
  56. package/dist/src/validation/index.js +19 -0
  57. package/dist/src/validation/simple.js +102 -0
  58. package/dist/src/validation/types.js +12 -0
  59. package/dist/src/validation/zod.js +18 -0
  60. package/dist/src/views/index.js +17 -0
  61. package/dist/src/views/types.js +2 -0
  62. package/dist/tests/adapters.test.js +106 -0
  63. package/dist/tests/batch.test.js +117 -0
  64. package/dist/tests/body-parser.test.js +52 -0
  65. package/dist/tests/compression-sse.test.js +87 -0
  66. package/dist/tests/cookies.test.js +63 -0
  67. package/dist/tests/cors.test.js +55 -0
  68. package/dist/tests/database.test.js +80 -0
  69. package/dist/tests/dx.test.js +64 -0
  70. package/dist/tests/ecosystem.test.js +133 -0
  71. package/dist/tests/features.test.js +47 -0
  72. package/dist/tests/fusion.test.js +92 -0
  73. package/dist/tests/http-basic.test.js +124 -0
  74. package/dist/tests/logger.test.js +33 -0
  75. package/dist/tests/middleware.test.js +109 -0
  76. package/dist/tests/observability.test.js +59 -0
  77. package/dist/tests/openapi.test.js +64 -0
  78. package/dist/tests/plugin.test.js +65 -0
  79. package/dist/tests/plugins.test.js +71 -0
  80. package/dist/tests/rate-limit.test.js +77 -0
  81. package/dist/tests/resources.test.js +44 -0
  82. package/dist/tests/scheduler.test.js +46 -0
  83. package/dist/tests/schema-routes.test.js +77 -0
  84. package/dist/tests/security.test.js +83 -0
  85. package/dist/tests/server-db.test.js +72 -0
  86. package/dist/tests/smoke.test.js +10 -0
  87. package/dist/tests/sqlite-fusion.test.js +92 -0
  88. package/dist/tests/static.test.js +102 -0
  89. package/dist/tests/stream.test.js +44 -0
  90. package/dist/tests/task-metrics.test.js +53 -0
  91. package/dist/tests/tasks.test.js +62 -0
  92. package/dist/tests/testing.test.js +47 -0
  93. package/dist/tests/validation.test.js +107 -0
  94. package/dist/tests/websocket.test.js +146 -0
  95. package/dist/vitest.config.js +9 -0
  96. package/docs/AEGIS.md +76 -0
  97. package/docs/BENCHMARKS.md +36 -0
  98. package/docs/CAPABILITIES.md +70 -0
  99. package/docs/CLI.md +43 -0
  100. package/docs/DATABASE.md +142 -0
  101. package/docs/ECOSYSTEM.md +146 -0
  102. package/docs/NEXT_STEPS.md +99 -0
  103. package/docs/OPENAPI.md +99 -0
  104. package/docs/PLUGINS.md +59 -0
  105. package/docs/REAL_WORLD_EXAMPLES.md +109 -0
  106. package/docs/ROADMAP.md +366 -0
  107. package/docs/VALIDATION.md +136 -0
  108. package/eslint.config.cjs +26 -0
  109. package/examples/api-server.ts +254 -0
  110. package/package.json +61 -0
  111. package/src/benchmarks/compare-frameworks.ts +149 -0
  112. package/src/benchmarks/quantam-users.ts +70 -0
  113. package/src/benchmarks/simple-json.ts +71 -0
  114. package/src/benchmarks/ultra-mode.ts +159 -0
  115. package/src/cli/index.ts +214 -0
  116. package/src/client/index.ts +93 -0
  117. package/src/core/batch.ts +110 -0
  118. package/src/core/body-parser.ts +151 -0
  119. package/src/core/buffer-pool.ts +96 -0
  120. package/src/core/config.ts +60 -0
  121. package/src/core/fusion.ts +210 -0
  122. package/src/core/logger.ts +70 -0
  123. package/src/core/metrics.ts +166 -0
  124. package/src/core/resources.ts +38 -0
  125. package/src/core/scheduler.ts +126 -0
  126. package/src/core/scope.ts +87 -0
  127. package/src/core/serializer.ts +41 -0
  128. package/src/core/server.ts +1113 -0
  129. package/src/core/stream.ts +111 -0
  130. package/src/core/tasks.ts +138 -0
  131. package/src/core/types.ts +178 -0
  132. package/src/core/websocket.ts +112 -0
  133. package/src/core/worker-queue.ts +90 -0
  134. package/src/database/adapters/memory.ts +99 -0
  135. package/src/database/adapters/mongo.ts +116 -0
  136. package/src/database/adapters/postgres.ts +86 -0
  137. package/src/database/adapters/sqlite.ts +44 -0
  138. package/src/database/coalescer.ts +153 -0
  139. package/src/database/manager.ts +97 -0
  140. package/src/database/types.ts +24 -0
  141. package/src/index.ts +42 -0
  142. package/src/middleware/compression.ts +147 -0
  143. package/src/middleware/cors.ts +98 -0
  144. package/src/middleware/presets.ts +50 -0
  145. package/src/middleware/rate-limit.ts +106 -0
  146. package/src/middleware/security.ts +109 -0
  147. package/src/middleware/static.ts +216 -0
  148. package/src/openapi/generator.ts +167 -0
  149. package/src/router/radix-router.ts +119 -0
  150. package/src/router/radix-tree.ts +106 -0
  151. package/src/router/router.ts +190 -0
  152. package/src/testing/index.ts +104 -0
  153. package/src/utils/cookies.ts +67 -0
  154. package/src/utils/logger.ts +59 -0
  155. package/src/utils/signals.ts +45 -0
  156. package/src/utils/sse.ts +41 -0
  157. package/src/validation/index.ts +3 -0
  158. package/src/validation/simple.ts +93 -0
  159. package/src/validation/types.ts +38 -0
  160. package/src/validation/zod.ts +14 -0
  161. package/src/views/index.ts +1 -0
  162. package/src/views/types.ts +4 -0
  163. package/tests/adapters.test.ts +120 -0
  164. package/tests/batch.test.ts +139 -0
  165. package/tests/body-parser.test.ts +83 -0
  166. package/tests/compression-sse.test.ts +98 -0
  167. package/tests/cookies.test.ts +74 -0
  168. package/tests/cors.test.ts +79 -0
  169. package/tests/database.test.ts +90 -0
  170. package/tests/dx.test.ts +78 -0
  171. package/tests/ecosystem.test.ts +156 -0
  172. package/tests/features.test.ts +51 -0
  173. package/tests/fusion.test.ts +121 -0
  174. package/tests/http-basic.test.ts +161 -0
  175. package/tests/logger.test.ts +48 -0
  176. package/tests/middleware.test.ts +137 -0
  177. package/tests/observability.test.ts +91 -0
  178. package/tests/openapi.test.ts +74 -0
  179. package/tests/plugin.test.ts +85 -0
  180. package/tests/plugins.test.ts +93 -0
  181. package/tests/rate-limit.test.ts +97 -0
  182. package/tests/resources.test.ts +64 -0
  183. package/tests/scheduler.test.ts +71 -0
  184. package/tests/schema-routes.test.ts +89 -0
  185. package/tests/security.test.ts +128 -0
  186. package/tests/server-db.test.ts +72 -0
  187. package/tests/smoke.test.ts +9 -0
  188. package/tests/sqlite-fusion.test.ts +106 -0
  189. package/tests/static.test.ts +111 -0
  190. package/tests/stream.test.ts +58 -0
  191. package/tests/task-metrics.test.ts +78 -0
  192. package/tests/tasks.test.ts +90 -0
  193. package/tests/testing.test.ts +53 -0
  194. package/tests/validation.test.ts +126 -0
  195. package/tests/websocket.test.ts +132 -0
  196. package/tsconfig.json +16 -0
  197. package/vitest.config.ts +9 -0
@@ -0,0 +1,147 @@
1
+ import { QHTTPXMiddleware } from '../core/types';
2
+ import zlib from 'zlib';
3
+
4
+ export type CompressionOptions = {
5
+ threshold?: number; // Minimum size in bytes to compress
6
+ level?: number; // Zlib compression level
7
+ };
8
+
9
+ export function createCompressionMiddleware(options: CompressionOptions = {}): QHTTPXMiddleware {
10
+ const threshold = options.threshold ?? 1024;
11
+ const level = options.level ?? zlib.constants.Z_DEFAULT_COMPRESSION;
12
+
13
+ return async (ctx, next) => {
14
+ const req = ctx.req;
15
+ const res = ctx.res;
16
+ const acceptEncoding = req.headers['accept-encoding'] || '';
17
+
18
+ let stream: zlib.Gzip | zlib.BrotliCompress | zlib.Deflate | undefined;
19
+ let encoding = '';
20
+
21
+ if (/\bbr\b/.test(acceptEncoding as string)) {
22
+ stream = zlib.createBrotliCompress({
23
+ params: {
24
+ [zlib.constants.BROTLI_PARAM_QUALITY]: level,
25
+ },
26
+ });
27
+ encoding = 'br';
28
+ } else if (/\bgzip\b/.test(acceptEncoding as string)) {
29
+ stream = zlib.createGzip({ level });
30
+ encoding = 'gzip';
31
+ } else if (/\bdeflate\b/.test(acceptEncoding as string)) {
32
+ stream = zlib.createDeflate({ level });
33
+ encoding = 'deflate';
34
+ }
35
+
36
+ if (!stream) {
37
+ await next();
38
+ return;
39
+ }
40
+
41
+ const originalWrite = res.write;
42
+ const originalEnd = res.end;
43
+ // const originalSetHeader = res.setHeader;
44
+
45
+ // We need to defer compression decision until we know the content type/length
46
+ // But since we are streaming, we might just start compressing if headers are sent?
47
+ // Actually, we can hook into write/end.
48
+
49
+ let headersSent = false;
50
+ let compress = false;
51
+
52
+ // Helper to check if we should compress based on content-type
53
+ const shouldCompress = () => {
54
+ const contentType = res.getHeader('content-type');
55
+ if (!contentType) return true; // Assume yes if unknown? Or no? Usually text/json is compressed.
56
+ const type = String(contentType).toLowerCase();
57
+
58
+ if (type.includes('text/event-stream')) {
59
+ return false;
60
+ }
61
+
62
+ return (
63
+ type.includes('text') ||
64
+ type.includes('json') ||
65
+ type.includes('xml') ||
66
+ type.includes('javascript') ||
67
+ type.includes('svg')
68
+ );
69
+ };
70
+
71
+ // Override write
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ res.write = function (chunk: any, ...args: any[]): boolean {
74
+ if (!headersSent) {
75
+ if (shouldCompress()) {
76
+ compress = true;
77
+ // Disable auto-end because we handle the stream asynchronously
78
+ ctx.disableAutoEnd = true;
79
+ res.setHeader('Content-Encoding', encoding);
80
+ res.removeHeader('Content-Length');
81
+ res.setHeader('Vary', 'Accept-Encoding');
82
+
83
+ stream!.on('data', (data) => {
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ (originalWrite as any).call(res, data);
86
+ });
87
+ stream!.on('end', () => {
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ (originalEnd as any).call(res);
90
+ });
91
+ } else {
92
+ compress = false;
93
+ }
94
+ headersSent = true;
95
+ }
96
+
97
+ if (compress && stream) {
98
+ return stream.write(chunk, ...(args as [BufferEncoding, (error: Error | null | undefined) => void]));
99
+ } else {
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ return (originalWrite as any).apply(res, [chunk, ...args]);
102
+ }
103
+ };
104
+
105
+ // Override end
106
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+ res.end = function (chunk?: any, ...args: any[]): any {
108
+ if (!headersSent) {
109
+ const len = chunk ? Buffer.byteLength(chunk) : 0;
110
+
111
+ if (shouldCompress() && len >= threshold) {
112
+ compress = true;
113
+ // Disable auto-end because we handle the stream asynchronously
114
+ ctx.disableAutoEnd = true;
115
+ res.setHeader('Content-Encoding', encoding);
116
+ res.removeHeader('Content-Length');
117
+ res.setHeader('Vary', 'Accept-Encoding');
118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
+ stream!.on('data', (data) => (originalWrite as any).call(res, data));
120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
+ stream!.on('end', () => (originalEnd as any).call(res));
122
+ } else {
123
+ compress = false;
124
+ }
125
+ headersSent = true;
126
+ }
127
+
128
+ if (compress && stream) {
129
+ if (chunk) stream.write(chunk);
130
+ stream.end();
131
+ } else {
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ return (originalEnd as any).apply(res, [chunk, ...args]);
134
+ }
135
+ return res;
136
+ };
137
+
138
+ // Fix for the pipe issue:
139
+ // If we set compress=true, we should pipe stream to res ONCE.
140
+ // If we use { end: false }, we must manually end res.
141
+ // If we use { end: true }, stream.end() will end res.
142
+
143
+ // Let's refine the logic.
144
+
145
+ await next();
146
+ };
147
+ }
@@ -0,0 +1,98 @@
1
+ import { QHTTPXMiddleware } from '../core/types';
2
+
3
+ export type CorsOrigin =
4
+ | string
5
+ | string[]
6
+ | ((origin: string | undefined) => string | null | undefined);
7
+
8
+ export type CorsOptions = {
9
+ origin?: CorsOrigin;
10
+ methods?: string[];
11
+ allowedHeaders?: string[];
12
+ exposedHeaders?: string[];
13
+ credentials?: boolean;
14
+ maxAgeSeconds?: number;
15
+ };
16
+
17
+ function resolveOrigin(origin: CorsOrigin | undefined, requestOrigin: string | undefined): string | null | undefined {
18
+ if (!origin) {
19
+ return '*';
20
+ }
21
+ if (typeof origin === 'string') {
22
+ return origin;
23
+ }
24
+ if (Array.isArray(origin)) {
25
+ if (!requestOrigin) {
26
+ return null;
27
+ }
28
+ if (origin.includes(requestOrigin)) {
29
+ return requestOrigin;
30
+ }
31
+ return null;
32
+ }
33
+ return origin(requestOrigin);
34
+ }
35
+
36
+ export function createCorsMiddleware(options: CorsOptions = {}): QHTTPXMiddleware {
37
+ const methodsHeader =
38
+ options.methods && options.methods.length > 0
39
+ ? options.methods.join(', ')
40
+ : 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS';
41
+
42
+ const allowedHeadersHeader =
43
+ options.allowedHeaders && options.allowedHeaders.length > 0
44
+ ? options.allowedHeaders.join(', ')
45
+ : undefined;
46
+
47
+ const exposedHeadersHeader =
48
+ options.exposedHeaders && options.exposedHeaders.length > 0
49
+ ? options.exposedHeaders.join(', ')
50
+ : undefined;
51
+
52
+ const maxAgeHeader =
53
+ typeof options.maxAgeSeconds === 'number'
54
+ ? String(options.maxAgeSeconds)
55
+ : undefined;
56
+
57
+ const allowCredentials = options.credentials ?? false;
58
+
59
+ return async (ctx, next) => {
60
+ const requestOriginHeader = ctx.req.headers.origin;
61
+ const resolvedOrigin = resolveOrigin(options.origin, requestOriginHeader);
62
+
63
+ if (resolvedOrigin) {
64
+ ctx.res.setHeader('access-control-allow-origin', resolvedOrigin);
65
+ if (allowCredentials) {
66
+ ctx.res.setHeader('access-control-allow-credentials', 'true');
67
+ }
68
+ if (exposedHeadersHeader) {
69
+ ctx.res.setHeader('access-control-expose-headers', exposedHeadersHeader);
70
+ }
71
+ }
72
+
73
+ if (ctx.req.method === 'OPTIONS') {
74
+ ctx.res.statusCode = 204;
75
+ ctx.res.setHeader('access-control-allow-methods', methodsHeader);
76
+
77
+ const requestHeaders =
78
+ typeof ctx.req.headers['access-control-request-headers'] === 'string'
79
+ ? ctx.req.headers['access-control-request-headers']
80
+ : undefined;
81
+
82
+ const headersValue = allowedHeadersHeader || requestHeaders;
83
+ if (headersValue) {
84
+ ctx.res.setHeader('access-control-allow-headers', headersValue);
85
+ }
86
+
87
+ if (maxAgeHeader) {
88
+ ctx.res.setHeader('access-control-max-age', maxAgeHeader);
89
+ }
90
+
91
+ ctx.res.end();
92
+ return;
93
+ }
94
+
95
+ await next();
96
+ };
97
+ }
98
+
@@ -0,0 +1,50 @@
1
+ import { QHTTPXMiddleware } from '../core/types';
2
+ import { createSecureDefaults, SecureDefaultsOptions } from './security';
3
+ import { createLoggerMiddleware, LoggerOptions } from '../utils/logger';
4
+ import { createStaticMiddleware, StaticOptions } from './static';
5
+
6
+ export type ApiPresetOptions = {
7
+ security?: SecureDefaultsOptions;
8
+ logging?: LoggerOptions | boolean;
9
+ };
10
+
11
+ export function createApiPreset(options: ApiPresetOptions = {}): QHTTPXMiddleware[] {
12
+ const middlewares: QHTTPXMiddleware[] = [];
13
+
14
+ // 1. Security (CORS, Headers)
15
+ middlewares.push(...createSecureDefaults(options.security));
16
+
17
+ // 2. Logging
18
+ if (options.logging !== false) {
19
+ const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
20
+ middlewares.push(createLoggerMiddleware(loggerOptions));
21
+ }
22
+
23
+ return middlewares;
24
+ }
25
+
26
+ export type StaticAppPresetOptions = ApiPresetOptions & {
27
+ static: StaticOptions;
28
+ };
29
+
30
+ export function createStaticAppPreset(
31
+ options: StaticAppPresetOptions,
32
+ ): QHTTPXMiddleware[] {
33
+ const middlewares: QHTTPXMiddleware[] = [];
34
+
35
+ // 1. Security
36
+ middlewares.push(...createSecureDefaults(options.security));
37
+
38
+ // 2. Logging
39
+ if (options.logging !== false) {
40
+ const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
41
+ middlewares.push(createLoggerMiddleware(loggerOptions));
42
+ }
43
+
44
+ // 3. Static Files
45
+ // We force fallthrough to true so API routes can handle non-static requests
46
+ const staticOptions = { ...options.static, fallthrough: true };
47
+ middlewares.push(createStaticMiddleware(staticOptions));
48
+
49
+ return middlewares;
50
+ }
@@ -0,0 +1,106 @@
1
+ import type { QHTTPXContext, QHTTPXMiddleware } from '../core/types';
2
+
3
+ export interface RateLimitStore {
4
+ increment(key: string, windowMs: number): Promise<{ total: number; resetTime: number }>;
5
+ decrement(key: string): Promise<void>;
6
+ reset(key: string): Promise<void>;
7
+ }
8
+
9
+ export class MemoryStore implements RateLimitStore {
10
+ private hits = new Map<string, { count: number; resetTime: number }>();
11
+ private interval?: NodeJS.Timeout;
12
+
13
+ constructor(clearPeriodMs = 60000) {
14
+ // Cleanup expired entries periodically
15
+ if (clearPeriodMs > 0) {
16
+ this.interval = setInterval(() => this.cleanup(), clearPeriodMs);
17
+ this.interval.unref(); // Don't hold process open
18
+ }
19
+ }
20
+
21
+ async increment(key: string, windowMs: number): Promise<{ total: number; resetTime: number }> {
22
+ const now = Date.now();
23
+ let record = this.hits.get(key);
24
+
25
+ if (!record || now > record.resetTime) {
26
+ record = { count: 1, resetTime: now + windowMs };
27
+ this.hits.set(key, record);
28
+ } else {
29
+ record.count++;
30
+ }
31
+
32
+ return { total: record.count, resetTime: record.resetTime };
33
+ }
34
+
35
+ async decrement(key: string): Promise<void> {
36
+ const record = this.hits.get(key);
37
+ if (record && record.count > 0) {
38
+ record.count--;
39
+ }
40
+ }
41
+
42
+ async reset(key: string): Promise<void> {
43
+ this.hits.delete(key);
44
+ }
45
+
46
+ private cleanup() {
47
+ const now = Date.now();
48
+ for (const [key, record] of this.hits.entries()) {
49
+ if (now > record.resetTime) {
50
+ this.hits.delete(key);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ export interface RateLimitOptions {
57
+ windowMs?: number; // Time window in milliseconds
58
+ max?: number; // Max requests per window
59
+ message?: string | object; // Response message
60
+ statusCode?: number; // Status code (default 429)
61
+ headers?: boolean; // Send X-RateLimit headers
62
+ keyGenerator?: (ctx: QHTTPXContext) => string; // Custom key generator
63
+ skip?: (ctx: QHTTPXContext) => boolean; // Skip limiter
64
+ store?: RateLimitStore; // Storage backend
65
+ }
66
+
67
+ export const rateLimit = (options: RateLimitOptions = {}): QHTTPXMiddleware => {
68
+ const windowMs = options.windowMs ?? 60000; // 1 minute default
69
+ const max = options.max ?? 100; // 100 requests default
70
+ const message = options.message ?? 'Too many requests, please try again later.';
71
+ const statusCode = options.statusCode ?? 429;
72
+ const headers = options.headers ?? true;
73
+ const store = options.store ?? new MemoryStore();
74
+
75
+ const keyGenerator = options.keyGenerator ?? ((ctx: QHTTPXContext) => {
76
+ return ctx.req.socket.remoteAddress || 'unknown';
77
+ });
78
+
79
+ return async (ctx, next) => {
80
+ if (options.skip?.(ctx)) {
81
+ return next();
82
+ }
83
+
84
+ const key = keyGenerator(ctx);
85
+ const { total, resetTime } = await store.increment(key, windowMs);
86
+
87
+ const remaining = Math.max(0, max - total);
88
+ const resetSeconds = Math.ceil((resetTime - Date.now()) / 1000);
89
+
90
+ if (headers) {
91
+ ctx.res.setHeader('X-RateLimit-Limit', max);
92
+ ctx.res.setHeader('X-RateLimit-Remaining', remaining);
93
+ ctx.res.setHeader('X-RateLimit-Reset', resetSeconds);
94
+ }
95
+
96
+ if (total > max) {
97
+ if (headers) {
98
+ ctx.res.setHeader('Retry-After', resetSeconds);
99
+ }
100
+ ctx.json(typeof message === 'string' ? { error: message } : message, statusCode);
101
+ return;
102
+ }
103
+
104
+ await next();
105
+ };
106
+ };
@@ -0,0 +1,109 @@
1
+ import { QHTTPXContext, QHTTPXMiddleware } from '../core/types';
2
+ import { CorsOptions, createCorsMiddleware } from './cors';
3
+
4
+ export type SecurityHeadersOptions = {
5
+ contentSecurityPolicy?: string | null;
6
+ referrerPolicy?: string | null;
7
+ xFrameOptions?: 'DENY' | 'SAMEORIGIN' | null;
8
+ xContentTypeOptions?: 'nosniff' | null;
9
+ xXssProtection?: '0' | '1; mode=block' | null;
10
+ strictTransportSecurity?: string | null;
11
+ };
12
+
13
+ export function createSecurityHeadersMiddleware(
14
+ options: SecurityHeadersOptions = {},
15
+ ): QHTTPXMiddleware {
16
+ const {
17
+ contentSecurityPolicy = "default-src 'self'",
18
+ referrerPolicy = 'no-referrer',
19
+ xFrameOptions = 'SAMEORIGIN',
20
+ xContentTypeOptions = 'nosniff',
21
+ xXssProtection = '1; mode=block',
22
+ strictTransportSecurity,
23
+ } = options;
24
+
25
+ return async (ctx, next) => {
26
+ if (contentSecurityPolicy) {
27
+ ctx.res.setHeader('content-security-policy', contentSecurityPolicy);
28
+ }
29
+ if (referrerPolicy) {
30
+ ctx.res.setHeader('referrer-policy', referrerPolicy);
31
+ }
32
+ if (xFrameOptions) {
33
+ ctx.res.setHeader('x-frame-options', xFrameOptions);
34
+ }
35
+ if (xContentTypeOptions) {
36
+ ctx.res.setHeader('x-content-type-options', xContentTypeOptions);
37
+ }
38
+ if (xXssProtection) {
39
+ ctx.res.setHeader('x-xss-protection', xXssProtection);
40
+ }
41
+ if (strictTransportSecurity) {
42
+ ctx.res.setHeader('strict-transport-security', strictTransportSecurity);
43
+ }
44
+
45
+ await next();
46
+ };
47
+ }
48
+
49
+ export type SecureDefaultsOptions = {
50
+ cors?: CorsOptions;
51
+ securityHeaders?: SecurityHeadersOptions;
52
+ };
53
+
54
+ export function createSecureDefaults(
55
+ options: SecureDefaultsOptions = {},
56
+ ): QHTTPXMiddleware[] {
57
+ const middlewares: QHTTPXMiddleware[] = [];
58
+ middlewares.push(createCorsMiddleware(options.cors));
59
+ middlewares.push(createSecurityHeadersMiddleware(options.securityHeaders));
60
+ return middlewares;
61
+ }
62
+
63
+ export type RateLimitOptions = {
64
+ maxRequests: number;
65
+ windowMs: number;
66
+ keyGenerator?: (ctx: QHTTPXContext) => string;
67
+ };
68
+
69
+ export function createRateLimitMiddleware(
70
+ options: RateLimitOptions,
71
+ ): QHTTPXMiddleware {
72
+ const maxRequests = options.maxRequests;
73
+ const windowMs = options.windowMs;
74
+ const keyGenerator =
75
+ options.keyGenerator ??
76
+ ((ctx: QHTTPXContext) => {
77
+ const addr = ctx.req.socket.remoteAddress;
78
+ return addr || 'anonymous';
79
+ });
80
+
81
+ const buckets = new Map<string, { count: number; resetAt: number }>();
82
+
83
+ return async (ctx, next) => {
84
+ const now = Date.now();
85
+ const key = keyGenerator(ctx);
86
+ const existing = buckets.get(key);
87
+
88
+ let bucket = existing;
89
+ if (!bucket || bucket.resetAt <= now) {
90
+ bucket = {
91
+ count: 0,
92
+ resetAt: now + windowMs,
93
+ };
94
+ buckets.set(key, bucket);
95
+ }
96
+
97
+ bucket.count += 1;
98
+ if (bucket.count > maxRequests) {
99
+ if (!ctx.res.headersSent) {
100
+ ctx.res.statusCode = 429;
101
+ ctx.res.setHeader('content-type', 'text/plain; charset=utf-8');
102
+ }
103
+ ctx.res.end('Too Many Requests');
104
+ return;
105
+ }
106
+
107
+ await next();
108
+ };
109
+ }