qhttpx 1.9.1 → 1.9.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.
Files changed (232) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +79 -17
  3. package/dist/examples/api-server.d.ts +1 -0
  4. package/dist/examples/api-server.js +77 -0
  5. package/dist/examples/basic.d.ts +1 -0
  6. package/dist/examples/basic.js +10 -0
  7. package/dist/examples/compression.d.ts +1 -0
  8. package/dist/examples/compression.js +17 -0
  9. package/dist/examples/cors.d.ts +1 -0
  10. package/dist/examples/cors.js +19 -0
  11. package/dist/examples/errors.d.ts +1 -0
  12. package/dist/examples/errors.js +25 -0
  13. package/dist/examples/file-upload.d.ts +1 -0
  14. package/dist/examples/file-upload.js +24 -0
  15. package/dist/examples/fusion.d.ts +1 -0
  16. package/dist/examples/fusion.js +21 -0
  17. package/dist/examples/rate-limiting.d.ts +1 -0
  18. package/dist/examples/rate-limiting.js +17 -0
  19. package/dist/examples/validation.d.ts +1 -0
  20. package/dist/examples/validation.js +23 -0
  21. package/dist/examples/websockets.d.ts +1 -0
  22. package/dist/examples/websockets.js +20 -0
  23. package/dist/package.json +112 -0
  24. package/dist/src/benchmarks/compare-frameworks.js +119 -0
  25. package/dist/src/benchmarks/compare.d.ts +1 -0
  26. package/dist/src/benchmarks/compare.js +288 -0
  27. package/dist/src/benchmarks/quantam-users.d.ts +1 -0
  28. package/dist/src/benchmarks/quantam-users.js +56 -0
  29. package/dist/src/benchmarks/simple-json.d.ts +1 -0
  30. package/dist/src/benchmarks/simple-json.js +60 -0
  31. package/dist/src/benchmarks/ultra-mode.d.ts +1 -0
  32. package/dist/src/benchmarks/ultra-mode.js +94 -0
  33. package/dist/src/buffer-pool.js +70 -0
  34. package/dist/src/cli/index.d.ts +2 -0
  35. package/dist/src/cli/index.js +222 -0
  36. package/dist/src/client/index.d.ts +17 -0
  37. package/dist/src/client/index.js +72 -0
  38. package/dist/src/config.js +50 -0
  39. package/dist/src/cookies.js +59 -0
  40. package/dist/src/core/batch.d.ts +24 -0
  41. package/dist/src/core/batch.js +97 -0
  42. package/dist/src/core/body-parser.d.ts +15 -0
  43. package/dist/src/core/body-parser.js +121 -0
  44. package/dist/src/core/buffer-pool.d.ts +41 -0
  45. package/dist/src/core/buffer-pool.js +70 -0
  46. package/dist/src/core/config.d.ts +7 -0
  47. package/dist/src/core/config.js +50 -0
  48. package/dist/src/core/errors.d.ts +34 -0
  49. package/dist/src/core/errors.js +70 -0
  50. package/dist/src/core/fusion.d.ts +20 -0
  51. package/dist/src/core/fusion.js +193 -0
  52. package/dist/src/core/logger.d.ts +22 -0
  53. package/dist/src/core/logger.js +49 -0
  54. package/dist/src/core/metrics.d.ts +48 -0
  55. package/dist/src/core/metrics.js +117 -0
  56. package/dist/src/core/native-adapter.d.ts +11 -0
  57. package/dist/src/core/native-adapter.js +211 -0
  58. package/dist/src/core/resources.d.ts +9 -0
  59. package/dist/src/core/resources.js +25 -0
  60. package/dist/src/core/scheduler.d.ts +34 -0
  61. package/dist/src/core/scheduler.js +85 -0
  62. package/dist/src/core/scope.d.ts +26 -0
  63. package/dist/src/core/scope.js +68 -0
  64. package/dist/src/core/serializer.d.ts +10 -0
  65. package/dist/src/core/serializer.js +44 -0
  66. package/dist/src/core/server.d.ts +138 -0
  67. package/dist/src/core/server.js +1082 -0
  68. package/dist/src/core/stream.d.ts +15 -0
  69. package/dist/src/core/stream.js +71 -0
  70. package/dist/src/core/tasks.d.ts +29 -0
  71. package/dist/src/core/tasks.js +87 -0
  72. package/dist/src/core/types.d.ts +173 -0
  73. package/dist/src/core/types.js +19 -0
  74. package/dist/src/core/websocket.d.ts +25 -0
  75. package/dist/src/core/websocket.js +86 -0
  76. package/dist/src/core/worker-queue.d.ts +41 -0
  77. package/dist/src/core/worker-queue.js +73 -0
  78. package/dist/src/cors.js +66 -0
  79. package/dist/src/database/adapters/memory.d.ts +21 -0
  80. package/dist/src/database/adapters/memory.js +90 -0
  81. package/dist/src/database/adapters/mongo.d.ts +11 -0
  82. package/dist/src/database/adapters/mongo.js +141 -0
  83. package/dist/src/database/adapters/postgres.d.ts +10 -0
  84. package/dist/src/database/adapters/postgres.js +111 -0
  85. package/dist/src/database/adapters/sqlite.d.ts +10 -0
  86. package/dist/src/database/adapters/sqlite.js +42 -0
  87. package/dist/src/database/coalescer.d.ts +14 -0
  88. package/dist/src/database/coalescer.js +134 -0
  89. package/dist/src/database/manager.d.ts +35 -0
  90. package/dist/src/database/manager.js +87 -0
  91. package/dist/src/database/types.d.ts +20 -0
  92. package/dist/src/database/types.js +2 -0
  93. package/dist/src/index.d.ts +50 -0
  94. package/dist/src/index.js +91 -0
  95. package/dist/src/logger.js +45 -0
  96. package/dist/src/metrics.js +111 -0
  97. package/dist/src/middleware/compression.d.ts +2 -0
  98. package/dist/src/middleware/compression.js +133 -0
  99. package/dist/src/middleware/cors.d.ts +2 -0
  100. package/dist/src/middleware/cors.js +66 -0
  101. package/dist/src/middleware/presets.d.ts +16 -0
  102. package/dist/src/middleware/presets.js +52 -0
  103. package/dist/src/middleware/rate-limit.d.ts +14 -0
  104. package/dist/src/middleware/rate-limit.js +83 -0
  105. package/dist/src/middleware/security.d.ts +21 -0
  106. package/dist/src/middleware/security.js +69 -0
  107. package/dist/src/middleware/static.d.ts +11 -0
  108. package/dist/src/middleware/static.js +191 -0
  109. package/dist/src/native/index.d.ts +32 -0
  110. package/dist/src/native/index.js +141 -0
  111. package/dist/src/openapi/generator.d.ts +19 -0
  112. package/dist/src/openapi/generator.js +149 -0
  113. package/dist/src/presets.js +33 -0
  114. package/dist/src/radix-router.js +89 -0
  115. package/dist/src/radix-tree.js +81 -0
  116. package/dist/src/resources.js +25 -0
  117. package/dist/src/router/radix-router.d.ts +18 -0
  118. package/dist/src/router/radix-router.js +89 -0
  119. package/dist/src/router/radix-tree.d.ts +18 -0
  120. package/dist/src/router/radix-tree.js +131 -0
  121. package/dist/src/router/router.d.ts +34 -0
  122. package/dist/src/router/router.js +186 -0
  123. package/dist/src/router.js +138 -0
  124. package/dist/src/scheduler.js +85 -0
  125. package/dist/src/security.js +69 -0
  126. package/dist/src/server.js +685 -0
  127. package/dist/src/signals.js +31 -0
  128. package/dist/src/static.js +107 -0
  129. package/dist/src/stream.js +71 -0
  130. package/dist/src/tasks.js +87 -0
  131. package/dist/src/testing/index.d.ts +25 -0
  132. package/dist/src/testing/index.js +84 -0
  133. package/dist/src/testing.js +40 -0
  134. package/dist/src/types.js +19 -0
  135. package/dist/src/utils/cookies.d.ts +3 -0
  136. package/dist/src/utils/cookies.js +59 -0
  137. package/dist/src/utils/logger.d.ts +12 -0
  138. package/dist/src/utils/logger.js +45 -0
  139. package/dist/src/utils/signals.d.ts +6 -0
  140. package/dist/src/utils/signals.js +31 -0
  141. package/dist/src/utils/sse.d.ts +6 -0
  142. package/dist/src/utils/sse.js +32 -0
  143. package/dist/src/utils/testing.js +40 -0
  144. package/dist/src/validation/index.d.ts +3 -0
  145. package/dist/src/validation/index.js +19 -0
  146. package/dist/src/validation/simple.d.ts +5 -0
  147. package/dist/src/validation/simple.js +102 -0
  148. package/dist/src/validation/types.d.ts +32 -0
  149. package/dist/src/validation/types.js +12 -0
  150. package/dist/src/validation/zod.d.ts +4 -0
  151. package/dist/src/validation/zod.js +18 -0
  152. package/dist/src/views/index.d.ts +1 -0
  153. package/dist/src/views/index.js +17 -0
  154. package/dist/src/views/types.d.ts +3 -0
  155. package/dist/src/views/types.js +2 -0
  156. package/dist/src/worker-queue.js +73 -0
  157. package/dist/tests/adapters.test.d.ts +1 -0
  158. package/dist/tests/adapters.test.js +106 -0
  159. package/dist/tests/batch.test.d.ts +1 -0
  160. package/dist/tests/batch.test.js +117 -0
  161. package/dist/tests/body-parser.test.d.ts +1 -0
  162. package/dist/tests/body-parser.test.js +52 -0
  163. package/dist/tests/compression-sse.test.d.ts +1 -0
  164. package/dist/tests/compression-sse.test.js +87 -0
  165. package/dist/tests/cookies.test.d.ts +1 -0
  166. package/dist/tests/cookies.test.js +63 -0
  167. package/dist/tests/cors.test.d.ts +1 -0
  168. package/dist/tests/cors.test.js +55 -0
  169. package/dist/tests/database.test.d.ts +1 -0
  170. package/dist/tests/database.test.js +80 -0
  171. package/dist/tests/dx.test.d.ts +1 -0
  172. package/dist/tests/dx.test.js +114 -0
  173. package/dist/tests/ecosystem.test.d.ts +1 -0
  174. package/dist/tests/ecosystem.test.js +133 -0
  175. package/dist/tests/features.test.d.ts +1 -0
  176. package/dist/tests/features.test.js +47 -0
  177. package/dist/tests/fusion.test.d.ts +1 -0
  178. package/dist/tests/fusion.test.js +92 -0
  179. package/dist/tests/http-basic.test.d.ts +1 -0
  180. package/dist/tests/http-basic.test.js +124 -0
  181. package/dist/tests/logger.test.d.ts +1 -0
  182. package/dist/tests/logger.test.js +33 -0
  183. package/dist/tests/middleware.test.d.ts +1 -0
  184. package/dist/tests/middleware.test.js +109 -0
  185. package/dist/tests/native-adapter.test.d.ts +1 -0
  186. package/dist/tests/native-adapter.test.js +71 -0
  187. package/dist/tests/observability.test.d.ts +1 -0
  188. package/dist/tests/observability.test.js +59 -0
  189. package/dist/tests/openapi.test.d.ts +1 -0
  190. package/dist/tests/openapi.test.js +64 -0
  191. package/dist/tests/plugin.test.d.ts +1 -0
  192. package/dist/tests/plugin.test.js +65 -0
  193. package/dist/tests/plugins.test.d.ts +1 -0
  194. package/dist/tests/plugins.test.js +71 -0
  195. package/dist/tests/rate-limit.test.d.ts +1 -0
  196. package/dist/tests/rate-limit.test.js +77 -0
  197. package/dist/tests/resources.test.d.ts +1 -0
  198. package/dist/tests/resources.test.js +47 -0
  199. package/dist/tests/scheduler.test.d.ts +1 -0
  200. package/dist/tests/scheduler.test.js +46 -0
  201. package/dist/tests/schema-routes.test.d.ts +1 -0
  202. package/dist/tests/schema-routes.test.js +77 -0
  203. package/dist/tests/security.test.d.ts +1 -0
  204. package/dist/tests/security.test.js +83 -0
  205. package/dist/tests/server-db.test.d.ts +1 -0
  206. package/dist/tests/server-db.test.js +72 -0
  207. package/dist/tests/smoke.test.d.ts +1 -0
  208. package/dist/tests/smoke.test.js +10 -0
  209. package/dist/tests/sqlite-fusion.test.d.ts +1 -0
  210. package/dist/tests/sqlite-fusion.test.js +92 -0
  211. package/dist/tests/static.test.d.ts +1 -0
  212. package/dist/tests/static.test.js +102 -0
  213. package/dist/tests/stream.test.d.ts +1 -0
  214. package/dist/tests/stream.test.js +44 -0
  215. package/dist/tests/task-metrics.test.d.ts +1 -0
  216. package/dist/tests/task-metrics.test.js +53 -0
  217. package/dist/tests/tasks.test.d.ts +1 -0
  218. package/dist/tests/tasks.test.js +62 -0
  219. package/dist/tests/testing.test.d.ts +1 -0
  220. package/dist/tests/testing.test.js +47 -0
  221. package/dist/tests/validation.test.d.ts +1 -0
  222. package/dist/tests/validation.test.js +107 -0
  223. package/dist/tests/websocket.test.d.ts +1 -0
  224. package/dist/tests/websocket.test.js +146 -0
  225. package/dist/vitest.config.d.ts +2 -0
  226. package/dist/vitest.config.js +9 -0
  227. package/docs/FUSION.md +19 -0
  228. package/package.json +4 -15
  229. package/prebuilds/darwin-arm64/qhttpx.node +0 -0
  230. package/prebuilds/linux-x64/qhttpx.node +0 -0
  231. package/prebuilds/win32-x64/qhttpx.node +0 -0
  232. package/scripts/install-native.js +26 -0
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createApiPreset = createApiPreset;
4
+ exports.createStaticAppPreset = createStaticAppPreset;
5
+ const security_1 = require("./security");
6
+ const cors_1 = require("./cors");
7
+ const compression_1 = require("./compression");
8
+ const logger_1 = require("../utils/logger");
9
+ const static_1 = require("./static");
10
+ const rate_limit_1 = require("./rate-limit");
11
+ function createApiPreset(options = {}) {
12
+ const middlewares = [];
13
+ // 1. CORS
14
+ // Default to true (enabled) if not specified, to match createSecureDefaults behavior
15
+ const corsOpts = options.cors ?? options.security?.cors ?? true;
16
+ if (corsOpts !== false) {
17
+ const opts = corsOpts === true ? {} : corsOpts;
18
+ middlewares.push((0, cors_1.createCorsMiddleware)(opts));
19
+ }
20
+ // 2. Security Headers
21
+ middlewares.push((0, security_1.createSecurityHeadersMiddleware)(options.security?.securityHeaders));
22
+ // 3. Compression
23
+ if (options.compression) {
24
+ const opts = options.compression === true ? {} : options.compression;
25
+ middlewares.push((0, compression_1.createCompressionMiddleware)(opts));
26
+ }
27
+ // 4. Logging
28
+ if (options.logging !== false) {
29
+ const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
30
+ middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
31
+ }
32
+ // 5. Rate Limit
33
+ if (options.rateLimit) {
34
+ middlewares.push((0, rate_limit_1.rateLimit)(options.rateLimit));
35
+ }
36
+ return middlewares;
37
+ }
38
+ function createStaticAppPreset(options) {
39
+ const middlewares = [];
40
+ // 1. Security
41
+ middlewares.push(...(0, security_1.createSecureDefaults)(options.security));
42
+ // 2. Logging
43
+ if (options.logging !== false) {
44
+ const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
45
+ middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
46
+ }
47
+ // 3. Static Files
48
+ // We force fallthrough to true so API routes can handle non-static requests
49
+ const staticOptions = { ...options.static, fallthrough: true };
50
+ middlewares.push((0, static_1.createStaticMiddleware)(staticOptions));
51
+ return middlewares;
52
+ }
@@ -0,0 +1,14 @@
1
+ import type { QHTTPXMiddleware, RateLimitOptions, RateLimitStore } from '../core/types';
2
+ export declare class MemoryStore implements RateLimitStore {
3
+ private hits;
4
+ private interval?;
5
+ constructor(clearPeriodMs?: number);
6
+ increment(key: string, windowMs: number): Promise<{
7
+ total: number;
8
+ resetTime: number;
9
+ }>;
10
+ decrement(key: string): Promise<void>;
11
+ reset(key: string): Promise<void>;
12
+ private cleanup;
13
+ }
14
+ export declare const rateLimit: (options?: RateLimitOptions) => QHTTPXMiddleware;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rateLimit = exports.MemoryStore = void 0;
4
+ class MemoryStore {
5
+ constructor(clearPeriodMs = 60000) {
6
+ this.hits = new Map();
7
+ // Cleanup expired entries periodically
8
+ if (clearPeriodMs > 0) {
9
+ this.interval = setInterval(() => this.cleanup(), clearPeriodMs);
10
+ this.interval.unref(); // Don't hold process open
11
+ }
12
+ }
13
+ async increment(key, windowMs) {
14
+ const now = Date.now();
15
+ let record = this.hits.get(key);
16
+ if (!record || now > record.resetTime) {
17
+ record = { count: 1, resetTime: now + windowMs };
18
+ this.hits.set(key, record);
19
+ }
20
+ else {
21
+ record.count++;
22
+ }
23
+ return { total: record.count, resetTime: record.resetTime };
24
+ }
25
+ async decrement(key) {
26
+ const record = this.hits.get(key);
27
+ if (record && record.count > 0) {
28
+ record.count--;
29
+ }
30
+ }
31
+ async reset(key) {
32
+ this.hits.delete(key);
33
+ }
34
+ cleanup() {
35
+ const now = Date.now();
36
+ for (const [key, record] of this.hits.entries()) {
37
+ if (now > record.resetTime) {
38
+ this.hits.delete(key);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ exports.MemoryStore = MemoryStore;
44
+ const rateLimit = (options = {}) => {
45
+ const windowMs = options.windowMs ?? 60000; // 1 minute default
46
+ const max = options.max ?? 100; // 100 requests default
47
+ const message = options.message ?? 'Too many requests, please try again later.';
48
+ const statusCode = options.statusCode ?? 429;
49
+ const headers = options.headers ?? true;
50
+ const store = options.store ?? new MemoryStore();
51
+ const keyGenerator = options.keyGenerator ?? ((ctx) => {
52
+ if (options.trustProxy) {
53
+ const forwarded = ctx.req.headers['x-forwarded-for'];
54
+ if (forwarded) {
55
+ return Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0].trim();
56
+ }
57
+ }
58
+ return ctx.req.socket.remoteAddress || 'unknown';
59
+ });
60
+ return async (ctx, next) => {
61
+ if (options.skip?.(ctx)) {
62
+ return next();
63
+ }
64
+ const key = keyGenerator(ctx);
65
+ const { total, resetTime } = await store.increment(key, windowMs);
66
+ const remaining = Math.max(0, max - total);
67
+ const resetSeconds = Math.ceil((resetTime - Date.now()) / 1000);
68
+ if (headers) {
69
+ ctx.res.setHeader('X-RateLimit-Limit', max);
70
+ ctx.res.setHeader('X-RateLimit-Remaining', remaining);
71
+ ctx.res.setHeader('X-RateLimit-Reset', resetSeconds);
72
+ }
73
+ if (total > max) {
74
+ if (headers) {
75
+ ctx.res.setHeader('Retry-After', resetSeconds);
76
+ }
77
+ ctx.json(typeof message === 'string' ? { error: message } : message, statusCode);
78
+ return;
79
+ }
80
+ await next();
81
+ };
82
+ };
83
+ exports.rateLimit = rateLimit;
@@ -0,0 +1,21 @@
1
+ import { QHTTPXContext, QHTTPXMiddleware, CorsOptions } from '../core/types';
2
+ export type SecurityHeadersOptions = {
3
+ contentSecurityPolicy?: string | null;
4
+ referrerPolicy?: string | null;
5
+ xFrameOptions?: 'DENY' | 'SAMEORIGIN' | null;
6
+ xContentTypeOptions?: 'nosniff' | null;
7
+ xXssProtection?: '0' | '1; mode=block' | null;
8
+ strictTransportSecurity?: string | null;
9
+ };
10
+ export declare function createSecurityHeadersMiddleware(options?: SecurityHeadersOptions): QHTTPXMiddleware;
11
+ export type SecureDefaultsOptions = {
12
+ cors?: CorsOptions;
13
+ securityHeaders?: SecurityHeadersOptions;
14
+ };
15
+ export declare function createSecureDefaults(options?: SecureDefaultsOptions): QHTTPXMiddleware[];
16
+ export type RateLimitOptions = {
17
+ maxRequests: number;
18
+ windowMs: number;
19
+ keyGenerator?: (ctx: QHTTPXContext) => string;
20
+ };
21
+ export declare function createRateLimitMiddleware(options: RateLimitOptions): QHTTPXMiddleware;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSecurityHeadersMiddleware = createSecurityHeadersMiddleware;
4
+ exports.createSecureDefaults = createSecureDefaults;
5
+ exports.createRateLimitMiddleware = createRateLimitMiddleware;
6
+ const cors_1 = require("./cors");
7
+ function createSecurityHeadersMiddleware(options = {}) {
8
+ const { contentSecurityPolicy = "default-src 'self'", referrerPolicy = 'no-referrer', xFrameOptions = 'SAMEORIGIN', xContentTypeOptions = 'nosniff', xXssProtection = '1; mode=block', strictTransportSecurity, } = options;
9
+ return async (ctx, next) => {
10
+ if (contentSecurityPolicy) {
11
+ ctx.res.setHeader('content-security-policy', contentSecurityPolicy);
12
+ }
13
+ if (referrerPolicy) {
14
+ ctx.res.setHeader('referrer-policy', referrerPolicy);
15
+ }
16
+ if (xFrameOptions) {
17
+ ctx.res.setHeader('x-frame-options', xFrameOptions);
18
+ }
19
+ if (xContentTypeOptions) {
20
+ ctx.res.setHeader('x-content-type-options', xContentTypeOptions);
21
+ }
22
+ if (xXssProtection) {
23
+ ctx.res.setHeader('x-xss-protection', xXssProtection);
24
+ }
25
+ if (strictTransportSecurity) {
26
+ ctx.res.setHeader('strict-transport-security', strictTransportSecurity);
27
+ }
28
+ await next();
29
+ };
30
+ }
31
+ function createSecureDefaults(options = {}) {
32
+ const middlewares = [];
33
+ middlewares.push((0, cors_1.createCorsMiddleware)(options.cors));
34
+ middlewares.push(createSecurityHeadersMiddleware(options.securityHeaders));
35
+ return middlewares;
36
+ }
37
+ function createRateLimitMiddleware(options) {
38
+ const maxRequests = options.maxRequests;
39
+ const windowMs = options.windowMs;
40
+ const keyGenerator = options.keyGenerator ??
41
+ ((ctx) => {
42
+ const addr = ctx.req.socket.remoteAddress;
43
+ return addr || 'anonymous';
44
+ });
45
+ const buckets = new Map();
46
+ return async (ctx, next) => {
47
+ const now = Date.now();
48
+ const key = keyGenerator(ctx);
49
+ const existing = buckets.get(key);
50
+ let bucket = existing;
51
+ if (!bucket || bucket.resetAt <= now) {
52
+ bucket = {
53
+ count: 0,
54
+ resetAt: now + windowMs,
55
+ };
56
+ buckets.set(key, bucket);
57
+ }
58
+ bucket.count += 1;
59
+ if (bucket.count > maxRequests) {
60
+ if (!ctx.res.headersSent) {
61
+ ctx.res.statusCode = 429;
62
+ ctx.res.setHeader('content-type', 'text/plain; charset=utf-8');
63
+ }
64
+ ctx.res.end('Too Many Requests');
65
+ return;
66
+ }
67
+ await next();
68
+ };
69
+ }
@@ -0,0 +1,11 @@
1
+ import { QHTTPXMiddleware } from '../core/types';
2
+ export type StaticOptions = {
3
+ root: string;
4
+ index?: string;
5
+ fallthrough?: boolean;
6
+ etag?: boolean;
7
+ lastModified?: boolean;
8
+ maxAge?: number;
9
+ immutable?: boolean;
10
+ };
11
+ export declare function createStaticMiddleware(options: StaticOptions): QHTTPXMiddleware;
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createStaticMiddleware = createStaticMiddleware;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function guessContentType(filePath) {
10
+ const ext = path_1.default.extname(filePath).toLowerCase();
11
+ const types = {
12
+ '.html': 'text/html; charset=utf-8',
13
+ '.htm': 'text/html; charset=utf-8',
14
+ '.js': 'application/javascript; charset=utf-8',
15
+ '.mjs': 'application/javascript; charset=utf-8',
16
+ '.css': 'text/css; charset=utf-8',
17
+ '.json': 'application/json; charset=utf-8',
18
+ '.png': 'image/png',
19
+ '.jpg': 'image/jpeg',
20
+ '.jpeg': 'image/jpeg',
21
+ '.gif': 'image/gif',
22
+ '.svg': 'image/svg+xml',
23
+ '.ico': 'image/x-icon',
24
+ '.txt': 'text/plain; charset=utf-8',
25
+ '.mp4': 'video/mp4',
26
+ '.webm': 'video/webm',
27
+ '.mp3': 'audio/mpeg',
28
+ '.wav': 'audio/wav',
29
+ '.pdf': 'application/pdf',
30
+ '.zip': 'application/zip',
31
+ };
32
+ return types[ext] || 'application/octet-stream';
33
+ }
34
+ function generateETag(stat) {
35
+ const mtime = stat.mtimeMs.toString(16);
36
+ const size = stat.size.toString(16);
37
+ return `W/"${size}-${mtime}"`;
38
+ }
39
+ function createStaticMiddleware(options) {
40
+ const root = path_1.default.resolve(options.root);
41
+ const indexFile = options.index ?? 'index.html';
42
+ const fallthrough = options.fallthrough ?? false;
43
+ const etag = options.etag ?? true;
44
+ const lastModified = options.lastModified ?? true;
45
+ const maxAge = options.maxAge ?? 0;
46
+ const immutable = options.immutable ?? false;
47
+ return async (ctx, next) => {
48
+ const method = ctx.req.method || 'GET';
49
+ if (method !== 'GET' && method !== 'HEAD') {
50
+ if (fallthrough) {
51
+ await next();
52
+ return;
53
+ }
54
+ ctx.res.statusCode = 405;
55
+ ctx.res.setHeader('Allow', 'GET, HEAD');
56
+ ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
57
+ ctx.res.end('Method Not Allowed');
58
+ return;
59
+ }
60
+ let requestPath = ctx.url.pathname || '/';
61
+ // Prevent directory traversal
62
+ requestPath = requestPath.replace(/^(\.\.(\/|\\|$))+/, '');
63
+ // Normalize path
64
+ let safePath = path_1.default.normalize(requestPath);
65
+ if (safePath.startsWith('..'))
66
+ safePath = '/'; // Extra safety
67
+ // Handle root/directory requests
68
+ let filePath = path_1.default.join(root, safePath);
69
+ let stat;
70
+ try {
71
+ stat = await fs_1.default.promises.stat(filePath);
72
+ if (stat.isDirectory()) {
73
+ filePath = path_1.default.join(filePath, indexFile);
74
+ stat = await fs_1.default.promises.stat(filePath);
75
+ }
76
+ }
77
+ catch {
78
+ if (fallthrough) {
79
+ await next();
80
+ return;
81
+ }
82
+ ctx.res.statusCode = 404;
83
+ ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
84
+ ctx.res.end('Not Found');
85
+ return;
86
+ }
87
+ if (!stat.isFile()) {
88
+ if (fallthrough) {
89
+ await next();
90
+ return;
91
+ }
92
+ ctx.res.statusCode = 404;
93
+ ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
94
+ ctx.res.end('Not Found');
95
+ return;
96
+ }
97
+ // Caching Headers
98
+ if (maxAge > 0 || immutable) {
99
+ let cacheControl = `public, max-age=${maxAge}`;
100
+ if (immutable)
101
+ cacheControl += ', immutable';
102
+ ctx.res.setHeader('Cache-Control', cacheControl);
103
+ }
104
+ if (lastModified) {
105
+ ctx.res.setHeader('Last-Modified', stat.mtime.toUTCString());
106
+ }
107
+ if (etag) {
108
+ const tag = generateETag(stat);
109
+ ctx.res.setHeader('ETag', tag);
110
+ // ETag Freshness Check
111
+ const ifNoneMatch = ctx.req.headers['if-none-match'];
112
+ if (ifNoneMatch === tag) {
113
+ ctx.res.statusCode = 304;
114
+ ctx.res.end();
115
+ return;
116
+ }
117
+ }
118
+ // Last-Modified Freshness Check
119
+ const ifModifiedSince = ctx.req.headers['if-modified-since'];
120
+ if (ifModifiedSince) {
121
+ const since = new Date(ifModifiedSince).getTime();
122
+ // Round down mtime to seconds for comparison
123
+ const mtime = Math.floor(stat.mtimeMs / 1000) * 1000;
124
+ if (mtime <= since) {
125
+ ctx.res.statusCode = 304;
126
+ ctx.res.end();
127
+ return;
128
+ }
129
+ }
130
+ const contentType = guessContentType(filePath);
131
+ ctx.res.setHeader('Content-Type', contentType);
132
+ ctx.res.setHeader('Accept-Ranges', 'bytes');
133
+ // Range Support
134
+ const range = ctx.req.headers['range'];
135
+ let start = 0;
136
+ let end = stat.size - 1;
137
+ // let isRange = false;
138
+ if (range) {
139
+ const parts = range.replace(/bytes=/, '').split('-');
140
+ const partialStart = parts[0];
141
+ const partialEnd = parts[1];
142
+ const parsedStart = parseInt(partialStart, 10);
143
+ const parsedEnd = partialEnd ? parseInt(partialEnd, 10) : end;
144
+ if (!isNaN(parsedStart) && !isNaN(parsedEnd) && parsedStart <= parsedEnd && parsedEnd < stat.size) {
145
+ start = parsedStart;
146
+ end = parsedEnd;
147
+ // isRange = true;
148
+ ctx.res.statusCode = 206;
149
+ ctx.res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
150
+ ctx.res.setHeader('Content-Length', end - start + 1);
151
+ }
152
+ else {
153
+ // Invalid range
154
+ ctx.res.statusCode = 416;
155
+ ctx.res.setHeader('Content-Range', `bytes */${stat.size}`);
156
+ ctx.res.end();
157
+ return;
158
+ }
159
+ }
160
+ else {
161
+ ctx.res.statusCode = 200;
162
+ ctx.res.setHeader('Content-Length', stat.size);
163
+ }
164
+ if (method === 'HEAD') {
165
+ ctx.res.end();
166
+ return;
167
+ }
168
+ const stream = fs_1.default.createReadStream(filePath, { start, end });
169
+ // Disable auto-end if we are streaming?
170
+ // Actually, we can just pipe. But we need to make sure the server doesn't call res.end()
171
+ // The middleware architecture awaits next(), then server calls end().
172
+ // If we handle the response here, we should probably set a flag or just return without calling next() (which we do).
173
+ // But the server might still try to end it if we don't tell it we are done.
174
+ // In QHTTPX, if a handler returns, server checks `res.writableEnded`.
175
+ // Pipe calls end() by default.
176
+ // We need to wait for the stream to finish before returning from middleware
177
+ // so that the server doesn't race.
178
+ await new Promise((resolve, reject) => {
179
+ stream.pipe(ctx.res);
180
+ stream.on('end', resolve);
181
+ stream.on('error', (err) => {
182
+ stream.destroy();
183
+ reject(err);
184
+ });
185
+ ctx.res.on('close', () => {
186
+ stream.destroy();
187
+ resolve();
188
+ });
189
+ });
190
+ };
191
+ }
@@ -0,0 +1,32 @@
1
+ export interface NativeServerBinding {
2
+ parse(buffer: Buffer): {
3
+ method: string;
4
+ path: string;
5
+ version: number;
6
+ headers: Record<string, string>;
7
+ bodyOffset: number;
8
+ } | null;
9
+ createResponse(statusCode: number, headers: Record<string, string>, body?: string | Buffer): Buffer;
10
+ createJSONResponse(obj: unknown): Buffer;
11
+ writeResponse(fd: number, chunks: (Buffer | string)[]): void;
12
+ setCPUAffinity(cpuId: number): boolean;
13
+ }
14
+ export declare class NativeServer {
15
+ private binding;
16
+ constructor();
17
+ get isAvailable(): boolean;
18
+ parse(buffer: Buffer): {
19
+ method: string;
20
+ path: string;
21
+ version: number;
22
+ headers: Record<string, string>;
23
+ bodyOffset: number;
24
+ } | null;
25
+ createResponse(statusCode: number, headers: Record<string, string>, body?: string | Buffer): Buffer<ArrayBufferLike>;
26
+ createJSONResponse(obj: unknown): Buffer<ArrayBufferLike>;
27
+ writeResponse(fd: number, chunks: (Buffer | string)[]): void;
28
+ setCPUAffinity(cpuId: number): boolean;
29
+ private disableNative;
30
+ private jsCreateResponse;
31
+ private jsCreateJSONResponse;
32
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NativeServer = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ const loadBinding = require('node-gyp-build');
10
+ // EXPECTED ABI VERSION
11
+ const EXPECTED_ABI = 1;
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ let nativeBinding = null;
14
+ try {
15
+ // Look for prebuilds or built binary in root
16
+ const rootDir = __dirname.includes('dist')
17
+ ? path_1.default.join(__dirname, '..', '..', '..')
18
+ : path_1.default.join(__dirname, '..', '..');
19
+ const binding = loadBinding(rootDir);
20
+ // 5. Version lock the ABI
21
+ if (binding && binding.abi === EXPECTED_ABI) {
22
+ nativeBinding = binding;
23
+ }
24
+ else if (binding) {
25
+ // Version mismatch - disable native
26
+ // console.warn(`QHTTPX: Native ABI mismatch (Expected: ${EXPECTED_ABI}, Got: ${binding.abi}). Falling back to JS.`);
27
+ nativeBinding = null;
28
+ }
29
+ }
30
+ catch {
31
+ // Graceful fallback to JS implementation
32
+ }
33
+ class NativeServer {
34
+ constructor() {
35
+ if (nativeBinding && nativeBinding.NativeServer) {
36
+ try {
37
+ this.binding = new nativeBinding.NativeServer();
38
+ }
39
+ catch {
40
+ this.binding = null;
41
+ }
42
+ }
43
+ else {
44
+ this.binding = null;
45
+ }
46
+ }
47
+ get isAvailable() {
48
+ return this.binding !== null;
49
+ }
50
+ // 4. Guard every native call
51
+ parse(buffer) {
52
+ if (this.binding) {
53
+ try {
54
+ return this.binding.parse(buffer);
55
+ }
56
+ catch {
57
+ this.disableNative();
58
+ }
59
+ }
60
+ // Fallback: If native parse fails, we return null.
61
+ // The NativeAdapter will see null and might close the connection or handle error.
62
+ // We cannot easily implement a full HTTP parser in JS here without adding dependencies.
63
+ // But since NativeAdapter falls back to http.Server if !isAvailable,
64
+ // disabling native here ensures subsequent requests use the safe path.
65
+ return null;
66
+ }
67
+ createResponse(statusCode, headers, body) {
68
+ if (this.binding) {
69
+ try {
70
+ return this.binding.createResponse(statusCode, headers, body);
71
+ }
72
+ catch {
73
+ this.disableNative();
74
+ }
75
+ }
76
+ return this.jsCreateResponse(statusCode, headers, body);
77
+ }
78
+ createJSONResponse(obj) {
79
+ if (this.binding) {
80
+ try {
81
+ return this.binding.createJSONResponse(obj);
82
+ }
83
+ catch {
84
+ this.disableNative();
85
+ }
86
+ }
87
+ // Fallback: Use JSON.stringify + Buffer
88
+ return this.jsCreateJSONResponse(obj);
89
+ }
90
+ writeResponse(fd, chunks) {
91
+ if (this.binding) {
92
+ try {
93
+ return this.binding.writeResponse(fd, chunks);
94
+ }
95
+ catch {
96
+ this.disableNative();
97
+ }
98
+ }
99
+ // No JS fallback for raw FD writes (requires net.Socket usually)
100
+ // The caller (NativeAdapter) should handle this by using socket.write()
101
+ // We throw here so caller knows to use fallback
102
+ throw new Error('Native writeResponse not available');
103
+ }
104
+ setCPUAffinity(cpuId) {
105
+ if (this.binding) {
106
+ try {
107
+ return this.binding.setCPUAffinity(cpuId);
108
+ }
109
+ catch {
110
+ this.disableNative();
111
+ }
112
+ }
113
+ return false;
114
+ }
115
+ disableNative() {
116
+ this.binding = null;
117
+ // console.warn('QHTTPX: Native module disabled due to error. Falling back to JS.');
118
+ }
119
+ // --- JS Fallbacks ---
120
+ jsCreateResponse(statusCode, headers, body) {
121
+ const statusMessage = statusCode === 200 ? 'OK' : 'Unknown'; // Simplified
122
+ let head = `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`;
123
+ for (const [key, value] of Object.entries(headers)) {
124
+ head += `${key}: ${value}\r\n`;
125
+ }
126
+ head += '\r\n';
127
+ const headBuf = Buffer.from(head);
128
+ if (body) {
129
+ const bodyBuf = Buffer.isBuffer(body) ? body : Buffer.from(body);
130
+ return Buffer.concat([headBuf, bodyBuf]);
131
+ }
132
+ return headBuf;
133
+ }
134
+ jsCreateJSONResponse(obj) {
135
+ const json = JSON.stringify(obj);
136
+ const contentLen = Buffer.byteLength(json);
137
+ const head = `HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ${contentLen}\r\n\r\n`;
138
+ return Buffer.concat([Buffer.from(head), Buffer.from(json)]);
139
+ }
140
+ }
141
+ exports.NativeServer = NativeServer;
@@ -0,0 +1,19 @@
1
+ import { Router } from '../router/router';
2
+ export type OpenAPIOptions = {
3
+ info: {
4
+ title: string;
5
+ version: string;
6
+ description?: string;
7
+ };
8
+ servers?: {
9
+ url: string;
10
+ description?: string;
11
+ }[];
12
+ };
13
+ export declare class OpenAPIGenerator {
14
+ private router;
15
+ private options;
16
+ constructor(router: Router, options: OpenAPIOptions);
17
+ generate(): object;
18
+ private convertSchema;
19
+ }