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,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const server_1 = require("../src/core/server");
5
+ const rate_limit_1 = require("../src/middleware/rate-limit");
6
+ (0, vitest_1.describe)('Aegis Rate Limiter', () => {
7
+ let app;
8
+ let port;
9
+ (0, vitest_1.beforeEach)(async () => {
10
+ app = new server_1.QHTTPX();
11
+ });
12
+ (0, vitest_1.afterEach)(() => {
13
+ // Cleanup if needed
14
+ });
15
+ (0, vitest_1.it)('should block requests exceeding the limit', async () => {
16
+ app.use((0, rate_limit_1.rateLimit)({
17
+ windowMs: 1000,
18
+ max: 2, // Allow 2 requests
19
+ message: { error: 'Blocked' }
20
+ }));
21
+ app.get('/', (ctx) => ctx.json({ ok: true }));
22
+ const { port: p } = await app.listen(0);
23
+ port = p;
24
+ const url = `http://localhost:${port}`;
25
+ // 1st request (OK)
26
+ const r1 = await fetch(url);
27
+ (0, vitest_1.expect)(r1.status).toBe(200);
28
+ (0, vitest_1.expect)(r1.headers.get('x-ratelimit-remaining')).toBe('1');
29
+ // 2nd request (OK)
30
+ const r2 = await fetch(url);
31
+ (0, vitest_1.expect)(r2.status).toBe(200);
32
+ (0, vitest_1.expect)(r2.headers.get('x-ratelimit-remaining')).toBe('0');
33
+ // 3rd request (Blocked)
34
+ const r3 = await fetch(url);
35
+ (0, vitest_1.expect)(r3.status).toBe(429);
36
+ (0, vitest_1.expect)(await r3.json()).toEqual({ error: 'Blocked' });
37
+ (0, vitest_1.expect)(r3.headers.get('retry-after')).toBeDefined();
38
+ });
39
+ (0, vitest_1.it)('should reset after window expires', async () => {
40
+ app.use((0, rate_limit_1.rateLimit)({
41
+ windowMs: 100, // Short window
42
+ max: 1
43
+ }));
44
+ app.get('/', (ctx) => ctx.json({ ok: true }));
45
+ const { port: p } = await app.listen(0);
46
+ const url = `http://localhost:${p}`;
47
+ // 1st (OK)
48
+ await fetch(url);
49
+ // 2nd (Blocked)
50
+ const r2 = await fetch(url);
51
+ (0, vitest_1.expect)(r2.status).toBe(429);
52
+ // Wait for window
53
+ await new Promise(r => setTimeout(r, 150));
54
+ // 3rd (OK)
55
+ const r3 = await fetch(url);
56
+ (0, vitest_1.expect)(r3.status).toBe(200);
57
+ });
58
+ (0, vitest_1.it)('should support custom key generators (e.g. per API key)', async () => {
59
+ app.use((0, rate_limit_1.rateLimit)({
60
+ windowMs: 1000,
61
+ max: 1,
62
+ keyGenerator: (ctx) => ctx.req.headers['x-api-key'] || 'anon'
63
+ }));
64
+ app.get('/', (ctx) => ctx.json({ ok: true }));
65
+ const { port: p } = await app.listen(0);
66
+ const url = `http://localhost:${p}`;
67
+ // User A (OK)
68
+ const r1 = await fetch(url, { headers: { 'x-api-key': 'userA' } });
69
+ (0, vitest_1.expect)(r1.status).toBe(200);
70
+ // User B (OK - different key)
71
+ const r2 = await fetch(url, { headers: { 'x-api-key': 'userB' } });
72
+ (0, vitest_1.expect)(r2.status).toBe(200);
73
+ // User A Again (Blocked)
74
+ const r3 = await fetch(url, { headers: { 'x-api-key': 'userA' } });
75
+ (0, vitest_1.expect)(r3.status).toBe(429);
76
+ });
77
+ });
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const resources_1 = require("../src/core/resources");
5
+ const index_1 = require("../src/index");
6
+ (0, vitest_1.describe)('Resource helpers', () => {
7
+ (0, vitest_1.it)('calculates worker count for fixed numbers', () => {
8
+ (0, vitest_1.expect)((0, resources_1.calculateWorkerCount)(1)).toBe(1);
9
+ (0, vitest_1.expect)((0, resources_1.calculateWorkerCount)(4)).toBe(4);
10
+ (0, vitest_1.expect)((0, resources_1.calculateWorkerCount)(0)).toBe(1);
11
+ (0, vitest_1.expect)((0, resources_1.calculateWorkerCount)(-5)).toBe(1);
12
+ });
13
+ (0, vitest_1.it)('detects memory overload based on thresholds', () => {
14
+ (0, vitest_1.expect)((0, resources_1.isResourceOverloaded)({ rssBytes: 2000 }, {
15
+ maxRssBytes: 1000,
16
+ })).toBe(true);
17
+ (0, vitest_1.expect)((0, resources_1.isResourceOverloaded)({ rssBytes: 500 }, {
18
+ maxRssBytes: 1000,
19
+ })).toBe(false);
20
+ });
21
+ });
22
+ (0, vitest_1.describe)('QHTTPX resource-aware behavior', () => {
23
+ (0, vitest_1.it)('returns 503 when memory usage exceeds maxMemoryBytes', async () => {
24
+ const originalMemoryUsage = process.memoryUsage;
25
+ const fakeMemoryUsage = vitest_1.vi.fn(() => ({
26
+ ...originalMemoryUsage(),
27
+ rss: 10000000000,
28
+ }));
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ process.memoryUsage = fakeMemoryUsage;
31
+ const app = new index_1.QHTTPX({
32
+ maxMemoryBytes: 1000000,
33
+ });
34
+ const { port } = await app.listen(0, '127.0.0.1');
35
+ app.get('/', (ctx) => {
36
+ ctx.send('ok');
37
+ });
38
+ const response = await fetch(`http://127.0.0.1:${port}/`);
39
+ (0, vitest_1.expect)(response.status).toBe(503);
40
+ await app.close();
41
+ process.memoryUsage =
42
+ originalMemoryUsage;
43
+ });
44
+ });
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const scheduler_1 = require("../src/core/scheduler");
5
+ (0, vitest_1.describe)('Scheduler', () => {
6
+ (0, vitest_1.it)('runs tasks within concurrency limit', async () => {
7
+ const scheduler = new scheduler_1.Scheduler({ maxConcurrency: 4 });
8
+ const order = [];
9
+ await Promise.all([
10
+ scheduler.run(async () => {
11
+ order.push(1);
12
+ }, {}),
13
+ scheduler.run(async () => {
14
+ order.push(2);
15
+ }, {}),
16
+ ]);
17
+ (0, vitest_1.expect)(order.sort()).toEqual([1, 2]);
18
+ (0, vitest_1.expect)(scheduler.getCurrentInFlight()).toBe(0);
19
+ });
20
+ (0, vitest_1.it)('calls onOverloaded when limit is reached', async () => {
21
+ const scheduler = new scheduler_1.Scheduler({ maxConcurrency: 1 });
22
+ const overloaded = vitest_1.vi.fn();
23
+ let resolveFirst;
24
+ const firstTask = scheduler.run(() => new Promise((resolve) => {
25
+ resolveFirst = resolve;
26
+ }), {});
27
+ await scheduler.run(async () => { }, { onOverloaded: overloaded });
28
+ (0, vitest_1.expect)(overloaded).toHaveBeenCalledTimes(1);
29
+ (0, vitest_1.expect)(scheduler.getCurrentInFlight()).toBe(1);
30
+ resolveFirst?.();
31
+ await firstTask;
32
+ (0, vitest_1.expect)(scheduler.getCurrentInFlight()).toBe(0);
33
+ });
34
+ (0, vitest_1.it)('invokes timeout callback when task exceeds timeout', async () => {
35
+ const scheduler = new scheduler_1.Scheduler({ maxConcurrency: 1 });
36
+ const onTimeout = vitest_1.vi.fn();
37
+ await scheduler.run(() => new Promise((resolve) => {
38
+ setTimeout(resolve, 50);
39
+ }), {
40
+ timeoutMs: 10,
41
+ onTimeout,
42
+ });
43
+ (0, vitest_1.expect)(onTimeout).toHaveBeenCalledTimes(1);
44
+ (0, vitest_1.expect)(scheduler.getCurrentInFlight()).toBe(0);
45
+ });
46
+ });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_1 = require("../src/index");
5
+ (0, vitest_1.describe)('Schema-based Routes', () => {
6
+ let app;
7
+ let port;
8
+ (0, vitest_1.beforeAll)(async () => {
9
+ app = new index_1.QHTTPX({
10
+ performanceMode: 'ultra'
11
+ });
12
+ const UserSchema = {
13
+ type: 'object',
14
+ properties: {
15
+ id: { type: 'integer' },
16
+ name: { type: 'string' },
17
+ email: { type: 'string' }
18
+ },
19
+ required: ['id', 'name']
20
+ };
21
+ app.get('/user', {
22
+ schema: UserSchema,
23
+ handler(ctx) {
24
+ ctx.json({
25
+ id: 1,
26
+ name: 'John Doe',
27
+ email: 'john@example.com',
28
+ extra: 'should be ignored'
29
+ });
30
+ }
31
+ });
32
+ app.get('/no-schema', (ctx) => {
33
+ ctx.json({ foo: 'bar', run: ctx.state.middlewareRun });
34
+ });
35
+ app.use(async (ctx, next) => {
36
+ ctx.state.middlewareRun = true;
37
+ await next();
38
+ });
39
+ app.get('/middleware-check', (ctx) => {
40
+ ctx.json({ run: ctx.state.middlewareRun });
41
+ });
42
+ const result = await app.listen(0);
43
+ port = result.port;
44
+ });
45
+ (0, vitest_1.afterAll)(async () => {
46
+ await app.close();
47
+ });
48
+ (0, vitest_1.it)('should serialize using the provided schema', async () => {
49
+ const response = await fetch(`http://localhost:${port}/user`);
50
+ (0, vitest_1.expect)(response.status).toBe(200);
51
+ const body = await response.json();
52
+ (0, vitest_1.expect)(body).toEqual({
53
+ id: 1,
54
+ name: 'John Doe',
55
+ email: 'john@example.com'
56
+ });
57
+ // 'extra' should be missing
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ (0, vitest_1.expect)(body.extra).toBeUndefined();
60
+ });
61
+ (0, vitest_1.it)('should work without schema', async () => {
62
+ const response = await fetch(`http://localhost:${port}/no-schema`);
63
+ (0, vitest_1.expect)(response.status).toBe(200);
64
+ const body = await response.json();
65
+ (0, vitest_1.expect)(body).toEqual({ foo: 'bar' });
66
+ // Middleware was added AFTER this route, so it should NOT run
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ (0, vitest_1.expect)(body.run).toBeUndefined();
69
+ });
70
+ (0, vitest_1.it)('should run middlewares', async () => {
71
+ const response = await fetch(`http://localhost:${port}/middleware-check`);
72
+ (0, vitest_1.expect)(response.status).toBe(200);
73
+ const body = await response.json();
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ (0, vitest_1.expect)(body.run).toBe(true);
76
+ });
77
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const src_1 = require("../src");
5
+ (0, vitest_1.describe)('Security and rate limiting middleware', () => {
6
+ let app;
7
+ (0, vitest_1.afterEach)(async () => {
8
+ if (app) {
9
+ await app.close();
10
+ app = undefined;
11
+ }
12
+ });
13
+ (0, vitest_1.it)('applies default security headers', async () => {
14
+ app = new src_1.QHTTPX();
15
+ app.use((0, src_1.createSecurityHeadersMiddleware)());
16
+ app.get('/ping', (ctx) => {
17
+ ctx.send('pong');
18
+ });
19
+ const { port } = await app.listen(0, '127.0.0.1');
20
+ const response = await fetch(`http://127.0.0.1:${port}/ping`);
21
+ (0, vitest_1.expect)(response.status).toBe(200);
22
+ (0, vitest_1.expect)(await response.text()).toBe('pong');
23
+ (0, vitest_1.expect)(response.headers.get('content-security-policy')).toBe("default-src 'self'");
24
+ (0, vitest_1.expect)(response.headers.get('referrer-policy')).toBe('no-referrer');
25
+ (0, vitest_1.expect)(response.headers.get('x-frame-options')).toBe('SAMEORIGIN');
26
+ (0, vitest_1.expect)(response.headers.get('x-content-type-options')).toBe('nosniff');
27
+ (0, vitest_1.expect)(response.headers.get('x-xss-protection')).toBe('1; mode=block');
28
+ });
29
+ (0, vitest_1.it)('allows overriding and disabling specific headers', async () => {
30
+ app = new src_1.QHTTPX();
31
+ app.use((0, src_1.createSecurityHeadersMiddleware)({
32
+ contentSecurityPolicy: "default-src 'self' https://cdn.example.com",
33
+ referrerPolicy: 'strict-origin-when-cross-origin',
34
+ xFrameOptions: null,
35
+ strictTransportSecurity: 'max-age=31536000; includeSubDomains',
36
+ }));
37
+ app.get('/ping', (ctx) => {
38
+ ctx.send('pong');
39
+ });
40
+ const { port } = await app.listen(0, '127.0.0.1');
41
+ const response = await fetch(`http://127.0.0.1:${port}/ping`);
42
+ (0, vitest_1.expect)(response.status).toBe(200);
43
+ (0, vitest_1.expect)(response.headers.get('content-security-policy')).toBe("default-src 'self' https://cdn.example.com");
44
+ (0, vitest_1.expect)(response.headers.get('referrer-policy')).toBe('strict-origin-when-cross-origin');
45
+ (0, vitest_1.expect)(response.headers.get('x-frame-options')).toBeNull();
46
+ (0, vitest_1.expect)(response.headers.get('strict-transport-security')).toBe('max-age=31536000; includeSubDomains');
47
+ });
48
+ (0, vitest_1.it)('provides secure defaults preset combining CORS and headers', async () => {
49
+ app = new src_1.QHTTPX();
50
+ const middlewares = (0, src_1.createSecureDefaults)();
51
+ for (const mw of middlewares) {
52
+ app.use(mw);
53
+ }
54
+ app.get('/ping', (ctx) => {
55
+ ctx.send('pong');
56
+ });
57
+ const { port } = await app.listen(0, '127.0.0.1');
58
+ const response = await fetch(`http://127.0.0.1:${port}/ping`, {
59
+ method: 'GET',
60
+ headers: {
61
+ origin: 'http://example.com',
62
+ },
63
+ });
64
+ (0, vitest_1.expect)(response.status).toBe(200);
65
+ (0, vitest_1.expect)(response.headers.get('access-control-allow-origin')).toBe('*');
66
+ (0, vitest_1.expect)(response.headers.get('content-security-policy')).toBe("default-src 'self'");
67
+ });
68
+ (0, vitest_1.it)('applies in-memory rate limiting per remote address', async () => {
69
+ app = new src_1.QHTTPX();
70
+ app.use((0, src_1.createRateLimitMiddleware)({
71
+ maxRequests: 1,
72
+ windowMs: 1000,
73
+ }));
74
+ app.get('/ping', (ctx) => {
75
+ ctx.send('pong');
76
+ });
77
+ const { port } = await app.listen(0, '127.0.0.1');
78
+ const first = await fetch(`http://127.0.0.1:${port}/ping`);
79
+ const second = await fetch(`http://127.0.0.1:${port}/ping`);
80
+ (0, vitest_1.expect)(first.status).toBe(200);
81
+ (0, vitest_1.expect)(second.status).toBe(429);
82
+ });
83
+ });
@@ -0,0 +1,72 @@
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
+ const vitest_1 = require("vitest");
7
+ const server_1 = require("../src/core/server");
8
+ const manager_1 = require("../src/database/manager");
9
+ const memory_1 = require("../src/database/adapters/memory");
10
+ const http_1 = __importDefault(require("http"));
11
+ (0, vitest_1.describe)('Server Database Integration', () => {
12
+ let app;
13
+ let dbManager;
14
+ (0, vitest_1.beforeEach)(async () => {
15
+ manager_1.DatabaseManager.registerAdapter('memory', memory_1.MemoryAdapter);
16
+ dbManager = new manager_1.DatabaseManager({
17
+ default: 'main',
18
+ connections: {
19
+ main: {
20
+ type: 'memory',
21
+ database: 'test_db'
22
+ }
23
+ }
24
+ });
25
+ await dbManager.connect();
26
+ app = new server_1.QHTTPX({
27
+ database: dbManager
28
+ });
29
+ });
30
+ (0, vitest_1.afterEach)(async () => {
31
+ await app.close();
32
+ await dbManager.disconnect();
33
+ });
34
+ (0, vitest_1.it)('should expose database manager in context', async () => {
35
+ return new Promise(async (resolve, reject) => {
36
+ app.get('/db-test', (ctx) => {
37
+ try {
38
+ (0, vitest_1.expect)(ctx.db).toBeDefined();
39
+ (0, vitest_1.expect)(ctx.db).toBe(dbManager);
40
+ (0, vitest_1.expect)(ctx.db?.get('main').isConnected()).toBe(true);
41
+ ctx.json({ status: 'ok' });
42
+ }
43
+ catch (error) {
44
+ // If expect fails, we need to make sure the test fails
45
+ // But throwing here might be caught by the server error handler
46
+ // So we set a flag or just let it fail naturally if vitest handles async exceptions
47
+ // For now, let's reject the promise
48
+ reject(error);
49
+ }
50
+ });
51
+ try {
52
+ const { port } = await app.listen(0);
53
+ const req = http_1.default.get(`http://localhost:${port}/db-test`, (res) => {
54
+ if (res.statusCode !== 200) {
55
+ reject(new Error(`Status code: ${res.statusCode}`));
56
+ return;
57
+ }
58
+ res.on('data', () => { });
59
+ res.on('end', () => {
60
+ resolve();
61
+ });
62
+ });
63
+ req.on('error', (err) => {
64
+ reject(err);
65
+ });
66
+ }
67
+ catch (err) {
68
+ reject(err);
69
+ }
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_1 = require("../src/index");
5
+ (0, vitest_1.describe)('qhttpx smoke', () => {
6
+ (0, vitest_1.it)('constructs a QHTTPX instance', () => {
7
+ const app = new index_1.QHTTPX();
8
+ (0, vitest_1.expect)(app).toBeInstanceOf(index_1.QHTTPX);
9
+ });
10
+ });
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const server_1 = require("../src/core/server");
5
+ const manager_1 = require("../src/database/manager");
6
+ const sqlite_1 = require("../src/database/adapters/sqlite");
7
+ (0, vitest_1.describe)('SQLite Query Fusion', () => {
8
+ let app;
9
+ let dbManager;
10
+ (0, vitest_1.beforeEach)(async () => {
11
+ // Register SQLite adapter
12
+ manager_1.DatabaseManager.registerAdapter('sqlite', sqlite_1.SQLiteAdapter);
13
+ // Use in-memory SQLite DB
14
+ dbManager = new manager_1.DatabaseManager({
15
+ default: 'main',
16
+ connections: {
17
+ main: { type: 'sqlite', database: ':memory:' }
18
+ }
19
+ });
20
+ await dbManager.connect();
21
+ // Setup schema and data
22
+ const adapter = dbManager.get('main');
23
+ await adapter.query('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
24
+ await adapter.query("INSERT INTO users (id, name) VALUES (1, 'Alice')");
25
+ await adapter.query("INSERT INTO users (id, name) VALUES (2, 'Bob')");
26
+ await adapter.query("INSERT INTO users (id, name) VALUES (3, 'Charlie')");
27
+ app = new server_1.QHTTPX({
28
+ database: dbManager,
29
+ enableBatching: true
30
+ });
31
+ });
32
+ (0, vitest_1.afterEach)(async () => {
33
+ if (app)
34
+ await app.close();
35
+ if (dbManager)
36
+ await dbManager.disconnect();
37
+ });
38
+ (0, vitest_1.it)('should fuse SELECT queries with SQLite', async () => {
39
+ // Wrap the adapter to spy on queries
40
+ const realAdapter = dbManager.get('main');
41
+ const originalQuery = realAdapter.query.bind(realAdapter);
42
+ const queries = [];
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ realAdapter.query = async (sql, params) => {
45
+ queries.push(sql);
46
+ return originalQuery(sql, params);
47
+ };
48
+ app.op('getUser', async (params, ctx) => {
49
+ // This matches the coalescer pattern: "SELECT ... WHERE id = ?"
50
+ if (!ctx.db)
51
+ throw new Error('No DB');
52
+ const rows = await ctx.db.get().query('SELECT * FROM users WHERE id = ?', [params.id]);
53
+ return rows[0];
54
+ });
55
+ const { port } = await app.listen(0);
56
+ const batch = {
57
+ batch: [
58
+ { op: 'getUser', params: { id: 1 }, id: 1 },
59
+ { op: 'getUser', params: { id: 2 }, id: 2 },
60
+ { op: 'getUser', params: { id: 3 }, id: 3 }
61
+ ]
62
+ };
63
+ const response = await fetch(`http://localhost:${port}/qhttpx`, {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify(batch)
67
+ });
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ const data = await response.json();
70
+ // Verify results
71
+ (0, vitest_1.expect)(data.results).toHaveLength(3);
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ const r1 = data.results.find((r) => r.id === 1);
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ const r2 = data.results.find((r) => r.id === 2);
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ const r3 = data.results.find((r) => r.id === 3);
78
+ (0, vitest_1.expect)(r1.result).toEqual({ id: 1, name: 'Alice' });
79
+ (0, vitest_1.expect)(r2.result).toEqual({ id: 2, name: 'Bob' });
80
+ (0, vitest_1.expect)(r3.result).toEqual({ id: 3, name: 'Charlie' });
81
+ // Verify FUSION
82
+ const selectQueries = queries.filter(q => q.trim().toUpperCase().startsWith('SELECT'));
83
+ // It should contain an IN clause
84
+ const fusedQuery = selectQueries.find(q => q.includes('IN ('));
85
+ (0, vitest_1.expect)(fusedQuery).toBeDefined();
86
+ // Ensure we didn't run 3 individual queries
87
+ // The spy catches the FUSED query, but NOT the individual ones if they were fused.
88
+ // If fusion FAILED, we would see 3 queries with "= ?"
89
+ const individualQueries = selectQueries.filter(q => q.includes('= ?'));
90
+ (0, vitest_1.expect)(individualQueries).toHaveLength(0);
91
+ });
92
+ });
@@ -0,0 +1,102 @@
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
+ const vitest_1 = require("vitest");
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ // import http from 'http';
10
+ const server_1 = require("../src/core/server");
11
+ const static_1 = require("../src/middleware/static");
12
+ const TEST_DIR = path_1.default.join(__dirname, 'fixtures');
13
+ const TEST_FILE = path_1.default.join(TEST_DIR, 'test.txt');
14
+ const TEST_CONTENT = 'Hello World! This is a test file for QHTTPX static middleware.';
15
+ (0, vitest_1.describe)('static middleware', () => {
16
+ let app;
17
+ let serverUrl;
18
+ (0, vitest_1.beforeAll)(async () => {
19
+ // Create fixtures
20
+ if (!fs_1.default.existsSync(TEST_DIR))
21
+ fs_1.default.mkdirSync(TEST_DIR);
22
+ fs_1.default.writeFileSync(TEST_FILE, TEST_CONTENT);
23
+ app = new server_1.QHTTPX();
24
+ app.use((0, static_1.createStaticMiddleware)({ root: TEST_DIR }));
25
+ const { port } = await app.listen(0, '127.0.0.1');
26
+ serverUrl = `http://127.0.0.1:${port}`;
27
+ });
28
+ (0, vitest_1.afterAll)(async () => {
29
+ await app.close();
30
+ // Cleanup
31
+ if (fs_1.default.existsSync(TEST_FILE))
32
+ fs_1.default.unlinkSync(TEST_FILE);
33
+ if (fs_1.default.existsSync(TEST_DIR))
34
+ fs_1.default.rmdirSync(TEST_DIR);
35
+ });
36
+ (0, vitest_1.it)('should serve a static file', async () => {
37
+ const response = await fetch(`${serverUrl}/test.txt`);
38
+ (0, vitest_1.expect)(response.status).toBe(200);
39
+ (0, vitest_1.expect)(response.headers.get('content-type')).toContain('text/plain');
40
+ const text = await response.text();
41
+ (0, vitest_1.expect)(text).toBe(TEST_CONTENT);
42
+ });
43
+ (0, vitest_1.it)('should support HEAD requests', async () => {
44
+ const response = await fetch(`${serverUrl}/test.txt`, { method: 'HEAD' });
45
+ (0, vitest_1.expect)(response.status).toBe(200);
46
+ (0, vitest_1.expect)(response.headers.get('content-type')).toContain('text/plain');
47
+ (0, vitest_1.expect)(response.headers.get('content-length')).toBe(TEST_CONTENT.length.toString());
48
+ const text = await response.text();
49
+ (0, vitest_1.expect)(text).toBe('');
50
+ });
51
+ (0, vitest_1.it)('should support Range requests (first byte)', async () => {
52
+ const response = await fetch(`${serverUrl}/test.txt`, {
53
+ headers: { Range: 'bytes=0-0' }
54
+ });
55
+ (0, vitest_1.expect)(response.status).toBe(206);
56
+ (0, vitest_1.expect)(response.headers.get('content-range')).toBe(`bytes 0-0/${TEST_CONTENT.length}`);
57
+ (0, vitest_1.expect)(response.headers.get('content-length')).toBe('1');
58
+ const text = await response.text();
59
+ (0, vitest_1.expect)(text).toBe('H');
60
+ });
61
+ (0, vitest_1.it)('should support Range requests (partial)', async () => {
62
+ const response = await fetch(`${serverUrl}/test.txt`, {
63
+ headers: { Range: 'bytes=0-4' }
64
+ });
65
+ (0, vitest_1.expect)(response.status).toBe(206);
66
+ (0, vitest_1.expect)(response.headers.get('content-range')).toBe(`bytes 0-4/${TEST_CONTENT.length}`);
67
+ (0, vitest_1.expect)(response.headers.get('content-length')).toBe('5');
68
+ const text = await response.text();
69
+ (0, vitest_1.expect)(text).toBe('Hello');
70
+ });
71
+ (0, vitest_1.it)('should return 416 for invalid Range', async () => {
72
+ const response = await fetch(`${serverUrl}/test.txt`, {
73
+ headers: { Range: 'bytes=1000-2000' }
74
+ });
75
+ (0, vitest_1.expect)(response.status).toBe(416);
76
+ (0, vitest_1.expect)(response.headers.get('content-range')).toBe(`bytes */${TEST_CONTENT.length}`);
77
+ });
78
+ (0, vitest_1.it)('should support ETag caching (304 Not Modified)', async () => {
79
+ const response1 = await fetch(`${serverUrl}/test.txt`);
80
+ const etag = response1.headers.get('etag');
81
+ (0, vitest_1.expect)(etag).toBeTruthy();
82
+ const response2 = await fetch(`${serverUrl}/test.txt`, {
83
+ headers: { 'If-None-Match': etag }
84
+ });
85
+ (0, vitest_1.expect)(response2.status).toBe(304);
86
+ const text = await response2.text();
87
+ (0, vitest_1.expect)(text).toBe('');
88
+ });
89
+ (0, vitest_1.it)('should support Last-Modified caching (304 Not Modified)', async () => {
90
+ const response1 = await fetch(`${serverUrl}/test.txt`);
91
+ const lastModified = response1.headers.get('last-modified');
92
+ (0, vitest_1.expect)(lastModified).toBeTruthy();
93
+ const response2 = await fetch(`${serverUrl}/test.txt`, {
94
+ headers: { 'If-Modified-Since': lastModified }
95
+ });
96
+ (0, vitest_1.expect)(response2.status).toBe(304);
97
+ });
98
+ (0, vitest_1.it)('should return 404 for non-existent file', async () => {
99
+ const response = await fetch(`${serverUrl}/does-not-exist.txt`);
100
+ (0, vitest_1.expect)(response.status).toBe(404);
101
+ });
102
+ });
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const stream_1 = require("stream");
4
+ const vitest_1 = require("vitest");
5
+ const src_1 = require("../src");
6
+ (0, vitest_1.describe)('Streaming helpers', () => {
7
+ let app;
8
+ (0, vitest_1.afterEach)(async () => {
9
+ if (app) {
10
+ await app.close();
11
+ app = undefined;
12
+ }
13
+ });
14
+ (0, vitest_1.it)('sends a simple SSE event', async () => {
15
+ app = new src_1.QHTTPX({ requestTimeoutMs: 5000 });
16
+ app.get('/sse', (ctx) => {
17
+ const sse = (0, src_1.createSseStream)(ctx);
18
+ sse.send({ message: 'hello' });
19
+ sse.close();
20
+ });
21
+ const { port } = await app.listen(0, '127.0.0.1');
22
+ const response = await fetch(`http://127.0.0.1:${port}/sse`);
23
+ (0, vitest_1.expect)(response.status).toBe(200);
24
+ (0, vitest_1.expect)(response.headers.get('content-type')).toContain('text/event-stream');
25
+ const text = await response.text();
26
+ (0, vitest_1.expect)(text.trim()).toBe('data: {"message":"hello"}');
27
+ });
28
+ (0, vitest_1.it)('streams data from a readable', async () => {
29
+ app = new src_1.QHTTPX();
30
+ app.get('/stream', async (ctx) => {
31
+ const readable = stream_1.Readable.from(['hello', ' ', 'world']);
32
+ await (0, src_1.sendStream)(ctx, readable, {
33
+ contentType: 'text/plain; charset=utf-8',
34
+ status: 200,
35
+ });
36
+ });
37
+ const { port } = await app.listen(0, '127.0.0.1');
38
+ const response = await fetch(`http://127.0.0.1:${port}/stream`);
39
+ (0, vitest_1.expect)(response.status).toBe(200);
40
+ (0, vitest_1.expect)(response.headers.get('content-type')).toContain('text/plain');
41
+ const text = await response.text();
42
+ (0, vitest_1.expect)(text).toBe('hello world');
43
+ });
44
+ });