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,42 @@
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.SQLiteAdapter = void 0;
7
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
8
+ class SQLiteAdapter {
9
+ constructor(config) {
10
+ this.db = null;
11
+ this.config = config;
12
+ }
13
+ async connect() {
14
+ const filename = this.config.database || ':memory:';
15
+ this.db = new better_sqlite3_1.default(filename, this.config.options);
16
+ }
17
+ async disconnect() {
18
+ if (this.db) {
19
+ this.db.close();
20
+ this.db = null;
21
+ }
22
+ }
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ async query(sql, params) {
25
+ if (!this.db) {
26
+ throw new Error('Database not connected');
27
+ }
28
+ const stmt = this.db.prepare(sql);
29
+ // better-sqlite3 handles '?' params automatically
30
+ // Determine if it's a read or write operation
31
+ if (sql.trim().toLowerCase().startsWith('select')) {
32
+ return stmt.all(params || []);
33
+ }
34
+ else {
35
+ return stmt.run(params || []);
36
+ }
37
+ }
38
+ isConnected() {
39
+ return this.db !== null && this.db.open;
40
+ }
41
+ }
42
+ exports.SQLiteAdapter = SQLiteAdapter;
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryCoalescer = void 0;
4
+ // eslint-enable @typescript-eslint/no-explicit-any
5
+ class QueryCoalescer {
6
+ constructor(adapter) {
7
+ this.pending = new Map();
8
+ this.timeout = null;
9
+ this.adapter = adapter;
10
+ }
11
+ /**
12
+ * Intercepts a query and attempts to coalesce it with others.
13
+ * Only supports simple queries of the form "SELECT ... WHERE col = ?" for now.
14
+ */
15
+ async query(query, params) {
16
+ // Basic check for coalescing eligibility:
17
+ // 1. Must have exactly one parameter (for simplicity in this v1)
18
+ // 2. Must contain " = ?" pattern
19
+ if (!params || params.length !== 1 || !query.includes(' = ?')) {
20
+ return this.adapter.query(query, params);
21
+ }
22
+ return new Promise((resolve, reject) => {
23
+ if (!this.pending.has(query)) {
24
+ this.pending.set(query, []);
25
+ }
26
+ this.pending.get(query).push({
27
+ query,
28
+ paramValue: params[0],
29
+ resolve: resolve,
30
+ reject
31
+ });
32
+ this.scheduleFlush();
33
+ });
34
+ }
35
+ scheduleFlush() {
36
+ if (this.timeout)
37
+ return;
38
+ // Use microtask or short timeout to gather queries from the current event loop tick
39
+ this.timeout = setTimeout(() => this.flush(), 0);
40
+ }
41
+ async flush() {
42
+ this.timeout = null;
43
+ const currentBatch = this.pending;
44
+ this.pending = new Map(); // Clear for next batch
45
+ for (const [originalQuery, items] of currentBatch.entries()) {
46
+ if (items.length === 1) {
47
+ // No fusion needed
48
+ const item = items[0];
49
+ try {
50
+ const result = await this.adapter.query(item.query, [item.paramValue]);
51
+ item.resolve(result);
52
+ }
53
+ catch (err) {
54
+ item.reject(err);
55
+ }
56
+ continue;
57
+ }
58
+ // Fuse queries
59
+ // Transform "SELECT * FROM table WHERE id = ?"
60
+ // into "SELECT * FROM table WHERE id IN (?, ?, ...)"
61
+ // Basic string manipulation (safe enough for this controlled feature)
62
+ // We assume the query ends with " = ?" or contains it clearly.
63
+ const fusedQuery = originalQuery.replace(' = ?', ` IN (${items.map(() => '?').join(', ')})`);
64
+ const allParams = items.map(i => i.paramValue);
65
+ try {
66
+ const results = await this.adapter.query(fusedQuery, allParams);
67
+ // Distribute results back to callers
68
+ // We need to map results back to params.
69
+ // This is the tricky part: standard SQL doesn't guarantee order matching input IN clause.
70
+ // We must assume the result objects contain the key used in WHERE.
71
+ // Extract the column name from the query
72
+ // "WHERE id = ?" -> "id"
73
+ const match = originalQuery.match(/WHERE\s+(\w+)\s*=\s*\?/i);
74
+ if (!match) {
75
+ // Fallback if we can't parse: execute individually (should not happen given check above)
76
+ await Promise.all(items.map(async (item) => {
77
+ try {
78
+ const r = await this.adapter.query(item.query, [item.paramValue]);
79
+ item.resolve(r);
80
+ }
81
+ catch (e) {
82
+ item.reject(e);
83
+ }
84
+ }));
85
+ continue;
86
+ }
87
+ const keyColumn = match[1];
88
+ // Map results by key
89
+ // Note: This assumes the result is an array of objects
90
+ // If the adapter returns something else, this might fail.
91
+ if (!Array.isArray(results)) {
92
+ // Fallback for non-array results
93
+ // Just give everyone the full result? No, that's wrong.
94
+ // If it's not an array, we probably can't split it.
95
+ // Reject? Or execute individually?
96
+ // Let's execute individually as fallback.
97
+ await Promise.all(items.map(async (item) => {
98
+ try {
99
+ const r = await this.adapter.query(item.query, [item.paramValue]);
100
+ item.resolve(r);
101
+ }
102
+ catch (e) {
103
+ item.reject(e);
104
+ }
105
+ }));
106
+ continue;
107
+ }
108
+ const resultMap = new Map();
109
+ results.forEach(row => {
110
+ if (typeof row === 'object' && row !== null && keyColumn in row) {
111
+ const typedRow = row;
112
+ const key = typedRow[keyColumn];
113
+ if (typeof key === 'string' || typeof key === 'number') {
114
+ if (!resultMap.has(key)) {
115
+ resultMap.set(key, []);
116
+ }
117
+ resultMap.get(key).push(row);
118
+ }
119
+ }
120
+ });
121
+ items.forEach(item => {
122
+ const key = typeof item.paramValue === 'string' || typeof item.paramValue === 'number' ? item.paramValue : undefined;
123
+ const res = key !== undefined ? resultMap.get(key) || [] : [];
124
+ item.resolve(res);
125
+ });
126
+ }
127
+ catch (err) {
128
+ // Fail all
129
+ items.forEach(item => item.reject(err));
130
+ }
131
+ }
132
+ }
133
+ }
134
+ exports.QueryCoalescer = QueryCoalescer;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DatabaseManager = void 0;
4
+ class DatabaseManager {
5
+ constructor(config) {
6
+ this.config = config;
7
+ this.connections = new Map();
8
+ }
9
+ /**
10
+ * Manually register an already initialized adapter instance
11
+ * @param name The connection name
12
+ * @param adapter The initialized adapter instance
13
+ */
14
+ registerConnection(name, adapter) {
15
+ this.connections.set(name, adapter);
16
+ }
17
+ /**
18
+ * Register a new database adapter type
19
+ * @param type The type identifier (e.g., 'postgres', 'mysql', 'mongo')
20
+ * @param adapter The adapter class
21
+ */
22
+ static registerAdapter(type, adapter) {
23
+ DatabaseManager.adapterRegistry.set(type, adapter);
24
+ }
25
+ /**
26
+ * Connect to a specific database or the default one
27
+ * @param name Connection name from config
28
+ */
29
+ async connect(name) {
30
+ const connectionName = name || this.config.default;
31
+ if (!connectionName) {
32
+ throw new Error('No connection name provided and no default connection configured.');
33
+ }
34
+ // Return existing connection if available and connected
35
+ const existingConnection = this.connections.get(connectionName);
36
+ if (existingConnection && existingConnection.isConnected()) {
37
+ return existingConnection;
38
+ }
39
+ const dbConfig = this.config.connections[connectionName];
40
+ if (!dbConfig) {
41
+ throw new Error(`Connection configuration for '${connectionName}' not found.`);
42
+ }
43
+ const AdapterClass = DatabaseManager.adapterRegistry.get(dbConfig.type);
44
+ if (!AdapterClass) {
45
+ throw new Error(`No adapter registered for database type '${dbConfig.type}'.`);
46
+ }
47
+ const adapter = new AdapterClass(dbConfig);
48
+ await adapter.connect();
49
+ this.connections.set(connectionName, adapter);
50
+ return adapter;
51
+ }
52
+ /**
53
+ * Disconnect a specific connection or all connections
54
+ * @param name Connection name (optional). If not provided, disconnects all.
55
+ */
56
+ async disconnect(name) {
57
+ if (name) {
58
+ const adapter = this.connections.get(name);
59
+ if (adapter) {
60
+ await adapter.disconnect();
61
+ this.connections.delete(name);
62
+ }
63
+ }
64
+ else {
65
+ const promises = Array.from(this.connections.values()).map(adapter => adapter.disconnect());
66
+ await Promise.all(promises);
67
+ this.connections.clear();
68
+ }
69
+ }
70
+ /**
71
+ * Get an active connection
72
+ * @param name Connection name
73
+ */
74
+ get(name) {
75
+ const connectionName = name || this.config.default;
76
+ if (!connectionName) {
77
+ throw new Error('No connection name provided and no default connection configured.');
78
+ }
79
+ const adapter = this.connections.get(connectionName);
80
+ if (!adapter) {
81
+ throw new Error(`Connection '${connectionName}' is not active. Call connect() first.`);
82
+ }
83
+ return adapter;
84
+ }
85
+ }
86
+ exports.DatabaseManager = DatabaseManager;
87
+ DatabaseManager.adapterRegistry = new Map();
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.getStringifier = exports.fastJsonStringify = exports.BufferPool = exports.QHTTPX = void 0;
18
+ exports.createHttpApp = createHttpApp;
19
+ const server_1 = require("./core/server");
20
+ const presets_1 = require("./middleware/presets");
21
+ var server_2 = require("./core/server");
22
+ Object.defineProperty(exports, "QHTTPX", { enumerable: true, get: function () { return server_2.QHTTPX; } });
23
+ __exportStar(require("./core/types"), exports);
24
+ __exportStar(require("./middleware/cors"), exports);
25
+ __exportStar(require("./middleware/security"), exports);
26
+ __exportStar(require("./middleware/static"), exports);
27
+ __exportStar(require("./middleware/compression"), exports);
28
+ __exportStar(require("./core/stream"), exports);
29
+ __exportStar(require("./utils/logger"), exports);
30
+ var buffer_pool_1 = require("./core/buffer-pool");
31
+ Object.defineProperty(exports, "BufferPool", { enumerable: true, get: function () { return buffer_pool_1.BufferPool; } });
32
+ __exportStar(require("./testing"), exports);
33
+ __exportStar(require("./utils/signals"), exports);
34
+ __exportStar(require("./middleware/presets"), exports);
35
+ __exportStar(require("./utils/cookies"), exports);
36
+ __exportStar(require("./utils/sse"), exports);
37
+ var serializer_1 = require("./core/serializer");
38
+ Object.defineProperty(exports, "fastJsonStringify", { enumerable: true, get: function () { return serializer_1.fastJsonStringify; } });
39
+ Object.defineProperty(exports, "getStringifier", { enumerable: true, get: function () { return serializer_1.getStringifier; } });
40
+ __exportStar(require("./database/types"), exports);
41
+ __exportStar(require("./database/manager"), exports);
42
+ __exportStar(require("./database/adapters/memory"), exports);
43
+ __exportStar(require("./views"), exports);
44
+ __exportStar(require("./validation"), exports);
45
+ __exportStar(require("./database/adapters/sqlite"), exports);
46
+ __exportStar(require("./database/adapters/postgres"), exports);
47
+ __exportStar(require("./database/adapters/mongo"), exports);
48
+ __exportStar(require("./core/fusion"), exports);
49
+ __exportStar(require("./validation/types"), exports);
50
+ __exportStar(require("./validation/simple"), exports);
51
+ __exportStar(require("./openapi/generator"), exports);
52
+ __exportStar(require("./client"), exports);
53
+ function createHttpApp(options = {}) {
54
+ const app = new server_1.QHTTPX(options);
55
+ // Skip middleware in ultra mode for maximum performance
56
+ if (options.performanceMode !== 'ultra') {
57
+ const middlewares = (0, presets_1.createApiPreset)();
58
+ middlewares.forEach((mw) => app.use(mw));
59
+ }
60
+ return app;
61
+ }
@@ -0,0 +1,133 @@
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.createCompressionMiddleware = createCompressionMiddleware;
7
+ const zlib_1 = __importDefault(require("zlib"));
8
+ function createCompressionMiddleware(options = {}) {
9
+ const threshold = options.threshold ?? 1024;
10
+ const level = options.level ?? zlib_1.default.constants.Z_DEFAULT_COMPRESSION;
11
+ return async (ctx, next) => {
12
+ const req = ctx.req;
13
+ const res = ctx.res;
14
+ const acceptEncoding = req.headers['accept-encoding'] || '';
15
+ let stream;
16
+ let encoding = '';
17
+ if (/\bbr\b/.test(acceptEncoding)) {
18
+ stream = zlib_1.default.createBrotliCompress({
19
+ params: {
20
+ [zlib_1.default.constants.BROTLI_PARAM_QUALITY]: level,
21
+ },
22
+ });
23
+ encoding = 'br';
24
+ }
25
+ else if (/\bgzip\b/.test(acceptEncoding)) {
26
+ stream = zlib_1.default.createGzip({ level });
27
+ encoding = 'gzip';
28
+ }
29
+ else if (/\bdeflate\b/.test(acceptEncoding)) {
30
+ stream = zlib_1.default.createDeflate({ level });
31
+ encoding = 'deflate';
32
+ }
33
+ if (!stream) {
34
+ await next();
35
+ return;
36
+ }
37
+ const originalWrite = res.write;
38
+ const originalEnd = res.end;
39
+ // const originalSetHeader = res.setHeader;
40
+ // We need to defer compression decision until we know the content type/length
41
+ // But since we are streaming, we might just start compressing if headers are sent?
42
+ // Actually, we can hook into write/end.
43
+ let headersSent = false;
44
+ let compress = false;
45
+ // Helper to check if we should compress based on content-type
46
+ const shouldCompress = () => {
47
+ const contentType = res.getHeader('content-type');
48
+ if (!contentType)
49
+ return true; // Assume yes if unknown? Or no? Usually text/json is compressed.
50
+ const type = String(contentType).toLowerCase();
51
+ if (type.includes('text/event-stream')) {
52
+ return false;
53
+ }
54
+ return (type.includes('text') ||
55
+ type.includes('json') ||
56
+ type.includes('xml') ||
57
+ type.includes('javascript') ||
58
+ type.includes('svg'));
59
+ };
60
+ // Override write
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ res.write = function (chunk, ...args) {
63
+ if (!headersSent) {
64
+ if (shouldCompress()) {
65
+ compress = true;
66
+ // Disable auto-end because we handle the stream asynchronously
67
+ ctx.disableAutoEnd = true;
68
+ res.setHeader('Content-Encoding', encoding);
69
+ res.removeHeader('Content-Length');
70
+ res.setHeader('Vary', 'Accept-Encoding');
71
+ stream.on('data', (data) => {
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ originalWrite.call(res, data);
74
+ });
75
+ stream.on('end', () => {
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ originalEnd.call(res);
78
+ });
79
+ }
80
+ else {
81
+ compress = false;
82
+ }
83
+ headersSent = true;
84
+ }
85
+ if (compress && stream) {
86
+ return stream.write(chunk, ...args);
87
+ }
88
+ else {
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ return originalWrite.apply(res, [chunk, ...args]);
91
+ }
92
+ };
93
+ // Override end
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ res.end = function (chunk, ...args) {
96
+ if (!headersSent) {
97
+ const len = chunk ? Buffer.byteLength(chunk) : 0;
98
+ if (shouldCompress() && len >= threshold) {
99
+ compress = true;
100
+ // Disable auto-end because we handle the stream asynchronously
101
+ ctx.disableAutoEnd = true;
102
+ res.setHeader('Content-Encoding', encoding);
103
+ res.removeHeader('Content-Length');
104
+ res.setHeader('Vary', 'Accept-Encoding');
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ stream.on('data', (data) => originalWrite.call(res, data));
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ stream.on('end', () => originalEnd.call(res));
109
+ }
110
+ else {
111
+ compress = false;
112
+ }
113
+ headersSent = true;
114
+ }
115
+ if (compress && stream) {
116
+ if (chunk)
117
+ stream.write(chunk);
118
+ stream.end();
119
+ }
120
+ else {
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ return originalEnd.apply(res, [chunk, ...args]);
123
+ }
124
+ return res;
125
+ };
126
+ // Fix for the pipe issue:
127
+ // If we set compress=true, we should pipe stream to res ONCE.
128
+ // If we use { end: false }, we must manually end res.
129
+ // If we use { end: true }, stream.end() will end res.
130
+ // Let's refine the logic.
131
+ await next();
132
+ };
133
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCorsMiddleware = createCorsMiddleware;
4
+ function resolveOrigin(origin, requestOrigin) {
5
+ if (!origin) {
6
+ return '*';
7
+ }
8
+ if (typeof origin === 'string') {
9
+ return origin;
10
+ }
11
+ if (Array.isArray(origin)) {
12
+ if (!requestOrigin) {
13
+ return null;
14
+ }
15
+ if (origin.includes(requestOrigin)) {
16
+ return requestOrigin;
17
+ }
18
+ return null;
19
+ }
20
+ return origin(requestOrigin);
21
+ }
22
+ function createCorsMiddleware(options = {}) {
23
+ const methodsHeader = options.methods && options.methods.length > 0
24
+ ? options.methods.join(', ')
25
+ : 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS';
26
+ const allowedHeadersHeader = options.allowedHeaders && options.allowedHeaders.length > 0
27
+ ? options.allowedHeaders.join(', ')
28
+ : undefined;
29
+ const exposedHeadersHeader = options.exposedHeaders && options.exposedHeaders.length > 0
30
+ ? options.exposedHeaders.join(', ')
31
+ : undefined;
32
+ const maxAgeHeader = typeof options.maxAgeSeconds === 'number'
33
+ ? String(options.maxAgeSeconds)
34
+ : undefined;
35
+ const allowCredentials = options.credentials ?? false;
36
+ return async (ctx, next) => {
37
+ const requestOriginHeader = ctx.req.headers.origin;
38
+ const resolvedOrigin = resolveOrigin(options.origin, requestOriginHeader);
39
+ if (resolvedOrigin) {
40
+ ctx.res.setHeader('access-control-allow-origin', resolvedOrigin);
41
+ if (allowCredentials) {
42
+ ctx.res.setHeader('access-control-allow-credentials', 'true');
43
+ }
44
+ if (exposedHeadersHeader) {
45
+ ctx.res.setHeader('access-control-expose-headers', exposedHeadersHeader);
46
+ }
47
+ }
48
+ if (ctx.req.method === 'OPTIONS') {
49
+ ctx.res.statusCode = 204;
50
+ ctx.res.setHeader('access-control-allow-methods', methodsHeader);
51
+ const requestHeaders = typeof ctx.req.headers['access-control-request-headers'] === 'string'
52
+ ? ctx.req.headers['access-control-request-headers']
53
+ : undefined;
54
+ const headersValue = allowedHeadersHeader || requestHeaders;
55
+ if (headersValue) {
56
+ ctx.res.setHeader('access-control-allow-headers', headersValue);
57
+ }
58
+ if (maxAgeHeader) {
59
+ ctx.res.setHeader('access-control-max-age', maxAgeHeader);
60
+ }
61
+ ctx.res.end();
62
+ return;
63
+ }
64
+ await next();
65
+ };
66
+ }
@@ -0,0 +1,33 @@
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 logger_1 = require("../utils/logger");
7
+ const static_1 = require("./static");
8
+ function createApiPreset(options = {}) {
9
+ const middlewares = [];
10
+ // 1. Security (CORS, Headers)
11
+ middlewares.push(...(0, security_1.createSecureDefaults)(options.security));
12
+ // 2. Logging
13
+ if (options.logging !== false) {
14
+ const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
15
+ middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
16
+ }
17
+ return middlewares;
18
+ }
19
+ function createStaticAppPreset(options) {
20
+ const middlewares = [];
21
+ // 1. Security
22
+ middlewares.push(...(0, security_1.createSecureDefaults)(options.security));
23
+ // 2. Logging
24
+ if (options.logging !== false) {
25
+ const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
26
+ middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
27
+ }
28
+ // 3. Static Files
29
+ // We force fallthrough to true so API routes can handle non-static requests
30
+ const staticOptions = { ...options.static, fallthrough: true };
31
+ middlewares.push((0, static_1.createStaticMiddleware)(staticOptions));
32
+ return middlewares;
33
+ }
@@ -0,0 +1,77 @@
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
+ return ctx.req.socket.remoteAddress || 'unknown';
53
+ });
54
+ return async (ctx, next) => {
55
+ if (options.skip?.(ctx)) {
56
+ return next();
57
+ }
58
+ const key = keyGenerator(ctx);
59
+ const { total, resetTime } = await store.increment(key, windowMs);
60
+ const remaining = Math.max(0, max - total);
61
+ const resetSeconds = Math.ceil((resetTime - Date.now()) / 1000);
62
+ if (headers) {
63
+ ctx.res.setHeader('X-RateLimit-Limit', max);
64
+ ctx.res.setHeader('X-RateLimit-Remaining', remaining);
65
+ ctx.res.setHeader('X-RateLimit-Reset', resetSeconds);
66
+ }
67
+ if (total > max) {
68
+ if (headers) {
69
+ ctx.res.setHeader('Retry-After', resetSeconds);
70
+ }
71
+ ctx.json(typeof message === 'string' ? { error: message } : message, statusCode);
72
+ return;
73
+ }
74
+ await next();
75
+ };
76
+ };
77
+ exports.rateLimit = rateLimit;