@morojs/moro 1.0.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 (345) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/core/config/index.d.ts +19 -0
  4. package/dist/core/config/index.js +59 -0
  5. package/dist/core/config/index.js.map +1 -0
  6. package/dist/core/config/loader.d.ts +6 -0
  7. package/dist/core/config/loader.js +288 -0
  8. package/dist/core/config/loader.js.map +1 -0
  9. package/dist/core/config/schema.d.ts +335 -0
  10. package/dist/core/config/schema.js +286 -0
  11. package/dist/core/config/schema.js.map +1 -0
  12. package/dist/core/config/utils.d.ts +50 -0
  13. package/dist/core/config/utils.js +185 -0
  14. package/dist/core/config/utils.js.map +1 -0
  15. package/dist/core/database/adapters/drizzle.d.ts +29 -0
  16. package/dist/core/database/adapters/drizzle.js +366 -0
  17. package/dist/core/database/adapters/drizzle.js.map +1 -0
  18. package/dist/core/database/adapters/index.d.ts +8 -0
  19. package/dist/core/database/adapters/index.js +48 -0
  20. package/dist/core/database/adapters/index.js.map +1 -0
  21. package/dist/core/database/adapters/mongodb.d.ts +35 -0
  22. package/dist/core/database/adapters/mongodb.js +215 -0
  23. package/dist/core/database/adapters/mongodb.js.map +1 -0
  24. package/dist/core/database/adapters/mysql.d.ts +23 -0
  25. package/dist/core/database/adapters/mysql.js +149 -0
  26. package/dist/core/database/adapters/mysql.js.map +1 -0
  27. package/dist/core/database/adapters/postgresql.d.ts +24 -0
  28. package/dist/core/database/adapters/postgresql.js +160 -0
  29. package/dist/core/database/adapters/postgresql.js.map +1 -0
  30. package/dist/core/database/adapters/redis.d.ts +50 -0
  31. package/dist/core/database/adapters/redis.js +266 -0
  32. package/dist/core/database/adapters/redis.js.map +1 -0
  33. package/dist/core/database/adapters/sqlite.d.ts +23 -0
  34. package/dist/core/database/adapters/sqlite.js +194 -0
  35. package/dist/core/database/adapters/sqlite.js.map +1 -0
  36. package/dist/core/database/index.d.ts +2 -0
  37. package/dist/core/database/index.js +20 -0
  38. package/dist/core/database/index.js.map +1 -0
  39. package/dist/core/docs/index.d.ts +63 -0
  40. package/dist/core/docs/index.js +170 -0
  41. package/dist/core/docs/index.js.map +1 -0
  42. package/dist/core/docs/openapi-generator.d.ts +124 -0
  43. package/dist/core/docs/openapi-generator.js +413 -0
  44. package/dist/core/docs/openapi-generator.js.map +1 -0
  45. package/dist/core/docs/simple-docs.d.ts +21 -0
  46. package/dist/core/docs/simple-docs.js +268 -0
  47. package/dist/core/docs/simple-docs.js.map +1 -0
  48. package/dist/core/docs/swagger-ui.d.ts +28 -0
  49. package/dist/core/docs/swagger-ui.js +317 -0
  50. package/dist/core/docs/swagger-ui.js.map +1 -0
  51. package/dist/core/docs/zod-to-openapi.d.ts +29 -0
  52. package/dist/core/docs/zod-to-openapi.js +414 -0
  53. package/dist/core/docs/zod-to-openapi.js.map +1 -0
  54. package/dist/core/events/event-bus.d.ts +27 -0
  55. package/dist/core/events/event-bus.js +193 -0
  56. package/dist/core/events/event-bus.js.map +1 -0
  57. package/dist/core/events/index.d.ts +2 -0
  58. package/dist/core/events/index.js +7 -0
  59. package/dist/core/events/index.js.map +1 -0
  60. package/dist/core/framework.d.ts +57 -0
  61. package/dist/core/framework.js +432 -0
  62. package/dist/core/framework.js.map +1 -0
  63. package/dist/core/http/http-server.d.ts +114 -0
  64. package/dist/core/http/http-server.js +1154 -0
  65. package/dist/core/http/http-server.js.map +1 -0
  66. package/dist/core/http/index.d.ts +3 -0
  67. package/dist/core/http/index.js +10 -0
  68. package/dist/core/http/index.js.map +1 -0
  69. package/dist/core/http/router.d.ts +14 -0
  70. package/dist/core/http/router.js +113 -0
  71. package/dist/core/http/router.js.map +1 -0
  72. package/dist/core/logger/filters.d.ts +9 -0
  73. package/dist/core/logger/filters.js +134 -0
  74. package/dist/core/logger/filters.js.map +1 -0
  75. package/dist/core/logger/index.d.ts +3 -0
  76. package/dist/core/logger/index.js +26 -0
  77. package/dist/core/logger/index.js.map +1 -0
  78. package/dist/core/logger/logger.d.ts +49 -0
  79. package/dist/core/logger/logger.js +332 -0
  80. package/dist/core/logger/logger.js.map +1 -0
  81. package/dist/core/logger/outputs.d.ts +42 -0
  82. package/dist/core/logger/outputs.js +110 -0
  83. package/dist/core/logger/outputs.js.map +1 -0
  84. package/dist/core/middleware/built-in/adapters/cache/file.d.ts +15 -0
  85. package/dist/core/middleware/built-in/adapters/cache/file.js +128 -0
  86. package/dist/core/middleware/built-in/adapters/cache/file.js.map +1 -0
  87. package/dist/core/middleware/built-in/adapters/cache/index.d.ts +5 -0
  88. package/dist/core/middleware/built-in/adapters/cache/index.js +28 -0
  89. package/dist/core/middleware/built-in/adapters/cache/index.js.map +1 -0
  90. package/dist/core/middleware/built-in/adapters/cache/memory.d.ts +11 -0
  91. package/dist/core/middleware/built-in/adapters/cache/memory.js +65 -0
  92. package/dist/core/middleware/built-in/adapters/cache/memory.js.map +1 -0
  93. package/dist/core/middleware/built-in/adapters/cache/redis.d.ts +17 -0
  94. package/dist/core/middleware/built-in/adapters/cache/redis.js +91 -0
  95. package/dist/core/middleware/built-in/adapters/cache/redis.js.map +1 -0
  96. package/dist/core/middleware/built-in/adapters/cdn/azure.d.ts +21 -0
  97. package/dist/core/middleware/built-in/adapters/cdn/azure.js +40 -0
  98. package/dist/core/middleware/built-in/adapters/cdn/azure.js.map +1 -0
  99. package/dist/core/middleware/built-in/adapters/cdn/cloudflare.d.ts +14 -0
  100. package/dist/core/middleware/built-in/adapters/cdn/cloudflare.js +77 -0
  101. package/dist/core/middleware/built-in/adapters/cdn/cloudflare.js.map +1 -0
  102. package/dist/core/middleware/built-in/adapters/cdn/cloudfront.d.ts +15 -0
  103. package/dist/core/middleware/built-in/adapters/cdn/cloudfront.js +73 -0
  104. package/dist/core/middleware/built-in/adapters/cdn/cloudfront.js.map +1 -0
  105. package/dist/core/middleware/built-in/adapters/cdn/index.d.ts +5 -0
  106. package/dist/core/middleware/built-in/adapters/cdn/index.js +28 -0
  107. package/dist/core/middleware/built-in/adapters/cdn/index.js.map +1 -0
  108. package/dist/core/middleware/built-in/adapters/index.d.ts +4 -0
  109. package/dist/core/middleware/built-in/adapters/index.js +26 -0
  110. package/dist/core/middleware/built-in/adapters/index.js.map +1 -0
  111. package/dist/core/middleware/built-in/auth.d.ts +2 -0
  112. package/dist/core/middleware/built-in/auth.js +38 -0
  113. package/dist/core/middleware/built-in/auth.js.map +1 -0
  114. package/dist/core/middleware/built-in/cache.d.ts +3 -0
  115. package/dist/core/middleware/built-in/cache.js +188 -0
  116. package/dist/core/middleware/built-in/cache.js.map +1 -0
  117. package/dist/core/middleware/built-in/cdn.d.ts +3 -0
  118. package/dist/core/middleware/built-in/cdn.js +115 -0
  119. package/dist/core/middleware/built-in/cdn.js.map +1 -0
  120. package/dist/core/middleware/built-in/cookie.d.ts +14 -0
  121. package/dist/core/middleware/built-in/cookie.js +68 -0
  122. package/dist/core/middleware/built-in/cookie.js.map +1 -0
  123. package/dist/core/middleware/built-in/cors.d.ts +2 -0
  124. package/dist/core/middleware/built-in/cors.js +29 -0
  125. package/dist/core/middleware/built-in/cors.js.map +1 -0
  126. package/dist/core/middleware/built-in/csp.d.ts +22 -0
  127. package/dist/core/middleware/built-in/csp.js +74 -0
  128. package/dist/core/middleware/built-in/csp.js.map +1 -0
  129. package/dist/core/middleware/built-in/csrf.d.ts +9 -0
  130. package/dist/core/middleware/built-in/csrf.js +66 -0
  131. package/dist/core/middleware/built-in/csrf.js.map +1 -0
  132. package/dist/core/middleware/built-in/error-tracker.d.ts +1 -0
  133. package/dist/core/middleware/built-in/error-tracker.js +19 -0
  134. package/dist/core/middleware/built-in/error-tracker.js.map +1 -0
  135. package/dist/core/middleware/built-in/index.d.ts +70 -0
  136. package/dist/core/middleware/built-in/index.js +70 -0
  137. package/dist/core/middleware/built-in/index.js.map +1 -0
  138. package/dist/core/middleware/built-in/performance-monitor.d.ts +1 -0
  139. package/dist/core/middleware/built-in/performance-monitor.js +22 -0
  140. package/dist/core/middleware/built-in/performance-monitor.js.map +1 -0
  141. package/dist/core/middleware/built-in/rate-limit.d.ts +6 -0
  142. package/dist/core/middleware/built-in/rate-limit.js +47 -0
  143. package/dist/core/middleware/built-in/rate-limit.js.map +1 -0
  144. package/dist/core/middleware/built-in/request-logger.d.ts +1 -0
  145. package/dist/core/middleware/built-in/request-logger.js +15 -0
  146. package/dist/core/middleware/built-in/request-logger.js.map +1 -0
  147. package/dist/core/middleware/built-in/session.d.ts +41 -0
  148. package/dist/core/middleware/built-in/session.js +209 -0
  149. package/dist/core/middleware/built-in/session.js.map +1 -0
  150. package/dist/core/middleware/built-in/sse.d.ts +6 -0
  151. package/dist/core/middleware/built-in/sse.js +73 -0
  152. package/dist/core/middleware/built-in/sse.js.map +1 -0
  153. package/dist/core/middleware/built-in/validation.d.ts +2 -0
  154. package/dist/core/middleware/built-in/validation.js +31 -0
  155. package/dist/core/middleware/built-in/validation.js.map +1 -0
  156. package/dist/core/middleware/index.d.ts +21 -0
  157. package/dist/core/middleware/index.js +152 -0
  158. package/dist/core/middleware/index.js.map +1 -0
  159. package/dist/core/modules/auto-discovery.d.ts +27 -0
  160. package/dist/core/modules/auto-discovery.js +255 -0
  161. package/dist/core/modules/auto-discovery.js.map +1 -0
  162. package/dist/core/modules/index.d.ts +2 -0
  163. package/dist/core/modules/index.js +11 -0
  164. package/dist/core/modules/index.js.map +1 -0
  165. package/dist/core/modules/modules.d.ts +10 -0
  166. package/dist/core/modules/modules.js +137 -0
  167. package/dist/core/modules/modules.js.map +1 -0
  168. package/dist/core/networking/index.d.ts +2 -0
  169. package/dist/core/networking/index.js +9 -0
  170. package/dist/core/networking/index.js.map +1 -0
  171. package/dist/core/networking/service-discovery.d.ts +38 -0
  172. package/dist/core/networking/service-discovery.js +233 -0
  173. package/dist/core/networking/service-discovery.js.map +1 -0
  174. package/dist/core/networking/websocket-manager.d.ts +27 -0
  175. package/dist/core/networking/websocket-manager.js +211 -0
  176. package/dist/core/networking/websocket-manager.js.map +1 -0
  177. package/dist/core/routing/app-integration.d.ts +42 -0
  178. package/dist/core/routing/app-integration.js +152 -0
  179. package/dist/core/routing/app-integration.js.map +1 -0
  180. package/dist/core/routing/index.d.ts +106 -0
  181. package/dist/core/routing/index.js +343 -0
  182. package/dist/core/routing/index.js.map +1 -0
  183. package/dist/core/runtime/aws-lambda-adapter.d.ts +43 -0
  184. package/dist/core/runtime/aws-lambda-adapter.js +108 -0
  185. package/dist/core/runtime/aws-lambda-adapter.js.map +1 -0
  186. package/dist/core/runtime/base-adapter.d.ts +16 -0
  187. package/dist/core/runtime/base-adapter.js +105 -0
  188. package/dist/core/runtime/base-adapter.js.map +1 -0
  189. package/dist/core/runtime/cloudflare-workers-adapter.d.ts +18 -0
  190. package/dist/core/runtime/cloudflare-workers-adapter.js +131 -0
  191. package/dist/core/runtime/cloudflare-workers-adapter.js.map +1 -0
  192. package/dist/core/runtime/index.d.ts +14 -0
  193. package/dist/core/runtime/index.js +56 -0
  194. package/dist/core/runtime/index.js.map +1 -0
  195. package/dist/core/runtime/node-adapter.d.ts +15 -0
  196. package/dist/core/runtime/node-adapter.js +204 -0
  197. package/dist/core/runtime/node-adapter.js.map +1 -0
  198. package/dist/core/runtime/vercel-edge-adapter.d.ts +10 -0
  199. package/dist/core/runtime/vercel-edge-adapter.js +106 -0
  200. package/dist/core/runtime/vercel-edge-adapter.js.map +1 -0
  201. package/dist/core/utilities/circuit-breaker.d.ts +14 -0
  202. package/dist/core/utilities/circuit-breaker.js +42 -0
  203. package/dist/core/utilities/circuit-breaker.js.map +1 -0
  204. package/dist/core/utilities/container.d.ts +116 -0
  205. package/dist/core/utilities/container.js +529 -0
  206. package/dist/core/utilities/container.js.map +1 -0
  207. package/dist/core/utilities/hooks.d.ts +24 -0
  208. package/dist/core/utilities/hooks.js +131 -0
  209. package/dist/core/utilities/hooks.js.map +1 -0
  210. package/dist/core/utilities/index.d.ts +4 -0
  211. package/dist/core/utilities/index.js +22 -0
  212. package/dist/core/utilities/index.js.map +1 -0
  213. package/dist/core/validation/index.d.ts +30 -0
  214. package/dist/core/validation/index.js +144 -0
  215. package/dist/core/validation/index.js.map +1 -0
  216. package/dist/index.d.ts +30 -0
  217. package/dist/index.js +72 -0
  218. package/dist/index.js.map +1 -0
  219. package/dist/moro.d.ts +82 -0
  220. package/dist/moro.js +679 -0
  221. package/dist/moro.js.map +1 -0
  222. package/dist/types/cache.d.ts +34 -0
  223. package/dist/types/cache.js +3 -0
  224. package/dist/types/cache.js.map +1 -0
  225. package/dist/types/cdn.d.ts +19 -0
  226. package/dist/types/cdn.js +3 -0
  227. package/dist/types/cdn.js.map +1 -0
  228. package/dist/types/core.d.ts +13 -0
  229. package/dist/types/core.js +3 -0
  230. package/dist/types/core.js.map +1 -0
  231. package/dist/types/database.d.ts +29 -0
  232. package/dist/types/database.js +3 -0
  233. package/dist/types/database.js.map +1 -0
  234. package/dist/types/discovery.d.ts +6 -0
  235. package/dist/types/discovery.js +3 -0
  236. package/dist/types/discovery.js.map +1 -0
  237. package/dist/types/events.d.ts +116 -0
  238. package/dist/types/events.js +3 -0
  239. package/dist/types/events.js.map +1 -0
  240. package/dist/types/hooks.d.ts +38 -0
  241. package/dist/types/hooks.js +3 -0
  242. package/dist/types/hooks.js.map +1 -0
  243. package/dist/types/http.d.ts +51 -0
  244. package/dist/types/http.js +3 -0
  245. package/dist/types/http.js.map +1 -0
  246. package/dist/types/logger.d.ts +77 -0
  247. package/dist/types/logger.js +3 -0
  248. package/dist/types/logger.js.map +1 -0
  249. package/dist/types/module.d.ts +91 -0
  250. package/dist/types/module.js +3 -0
  251. package/dist/types/module.js.map +1 -0
  252. package/dist/types/runtime.d.ts +48 -0
  253. package/dist/types/runtime.js +3 -0
  254. package/dist/types/runtime.js.map +1 -0
  255. package/dist/types/session.d.ts +66 -0
  256. package/dist/types/session.js +3 -0
  257. package/dist/types/session.js.map +1 -0
  258. package/package.json +176 -0
  259. package/src/core/config/index.ts +47 -0
  260. package/src/core/config/loader.ts +366 -0
  261. package/src/core/config/schema.ts +346 -0
  262. package/src/core/config/utils.ts +220 -0
  263. package/src/core/database/README.md +228 -0
  264. package/src/core/database/adapters/drizzle.ts +425 -0
  265. package/src/core/database/adapters/index.ts +45 -0
  266. package/src/core/database/adapters/mongodb.ts +292 -0
  267. package/src/core/database/adapters/mysql.ts +217 -0
  268. package/src/core/database/adapters/postgresql.ts +211 -0
  269. package/src/core/database/adapters/redis.ts +331 -0
  270. package/src/core/database/adapters/sqlite.ts +255 -0
  271. package/src/core/database/index.ts +3 -0
  272. package/src/core/docs/index.ts +245 -0
  273. package/src/core/docs/openapi-generator.ts +588 -0
  274. package/src/core/docs/simple-docs.ts +305 -0
  275. package/src/core/docs/swagger-ui.ts +370 -0
  276. package/src/core/docs/zod-to-openapi.ts +532 -0
  277. package/src/core/events/event-bus.ts +249 -0
  278. package/src/core/events/index.ts +12 -0
  279. package/src/core/framework.ts +621 -0
  280. package/src/core/http/http-server.ts +1421 -0
  281. package/src/core/http/index.ts +11 -0
  282. package/src/core/http/router.ts +153 -0
  283. package/src/core/logger/filters.ts +148 -0
  284. package/src/core/logger/index.ts +20 -0
  285. package/src/core/logger/logger.ts +434 -0
  286. package/src/core/logger/outputs.ts +136 -0
  287. package/src/core/middleware/built-in/adapters/cache/file.ts +106 -0
  288. package/src/core/middleware/built-in/adapters/cache/index.ts +26 -0
  289. package/src/core/middleware/built-in/adapters/cache/memory.ts +73 -0
  290. package/src/core/middleware/built-in/adapters/cache/redis.ts +103 -0
  291. package/src/core/middleware/built-in/adapters/cdn/azure.ts +68 -0
  292. package/src/core/middleware/built-in/adapters/cdn/cloudflare.ts +100 -0
  293. package/src/core/middleware/built-in/adapters/cdn/cloudfront.ts +92 -0
  294. package/src/core/middleware/built-in/adapters/cdn/index.ts +23 -0
  295. package/src/core/middleware/built-in/adapters/index.ts +7 -0
  296. package/src/core/middleware/built-in/auth.ts +39 -0
  297. package/src/core/middleware/built-in/cache.ts +228 -0
  298. package/src/core/middleware/built-in/cdn.ts +151 -0
  299. package/src/core/middleware/built-in/cookie.ts +90 -0
  300. package/src/core/middleware/built-in/cors.ts +38 -0
  301. package/src/core/middleware/built-in/csp.ts +107 -0
  302. package/src/core/middleware/built-in/csrf.ts +87 -0
  303. package/src/core/middleware/built-in/error-tracker.ts +16 -0
  304. package/src/core/middleware/built-in/index.ts +57 -0
  305. package/src/core/middleware/built-in/performance-monitor.ts +25 -0
  306. package/src/core/middleware/built-in/rate-limit.ts +60 -0
  307. package/src/core/middleware/built-in/request-logger.ts +14 -0
  308. package/src/core/middleware/built-in/session.ts +311 -0
  309. package/src/core/middleware/built-in/sse.ts +91 -0
  310. package/src/core/middleware/built-in/validation.ts +33 -0
  311. package/src/core/middleware/index.ts +188 -0
  312. package/src/core/modules/auto-discovery.ts +265 -0
  313. package/src/core/modules/index.ts +6 -0
  314. package/src/core/modules/modules.ts +125 -0
  315. package/src/core/networking/index.ts +7 -0
  316. package/src/core/networking/service-discovery.ts +309 -0
  317. package/src/core/networking/websocket-manager.ts +259 -0
  318. package/src/core/routing/app-integration.ts +229 -0
  319. package/src/core/routing/index.ts +519 -0
  320. package/src/core/runtime/aws-lambda-adapter.ts +157 -0
  321. package/src/core/runtime/base-adapter.ts +140 -0
  322. package/src/core/runtime/cloudflare-workers-adapter.ts +166 -0
  323. package/src/core/runtime/index.ts +74 -0
  324. package/src/core/runtime/node-adapter.ts +210 -0
  325. package/src/core/runtime/vercel-edge-adapter.ts +125 -0
  326. package/src/core/utilities/circuit-breaker.ts +46 -0
  327. package/src/core/utilities/container.ts +760 -0
  328. package/src/core/utilities/hooks.ts +148 -0
  329. package/src/core/utilities/index.ts +16 -0
  330. package/src/core/validation/index.ts +216 -0
  331. package/src/index.ts +120 -0
  332. package/src/moro.ts +842 -0
  333. package/src/types/cache.ts +38 -0
  334. package/src/types/cdn.ts +22 -0
  335. package/src/types/core.ts +17 -0
  336. package/src/types/database.ts +40 -0
  337. package/src/types/discovery.ts +7 -0
  338. package/src/types/events.ts +90 -0
  339. package/src/types/hooks.ts +47 -0
  340. package/src/types/http.ts +70 -0
  341. package/src/types/logger.ts +109 -0
  342. package/src/types/module.ts +87 -0
  343. package/src/types/runtime.ts +91 -0
  344. package/src/types/session.ts +89 -0
  345. package/tsconfig.json +21 -0
@@ -0,0 +1,1421 @@
1
+ // src/core/http-server.ts
2
+ import { IncomingMessage, ServerResponse, createServer, Server } from "http";
3
+ import { URL } from "url";
4
+ import * as zlib from "zlib";
5
+ import { promisify } from "util";
6
+ import { createFrameworkLogger } from "../logger";
7
+ import {
8
+ HttpRequest,
9
+ HttpResponse,
10
+ HttpHandler,
11
+ Middleware,
12
+ RouteEntry,
13
+ } from "../../types/http";
14
+
15
+ const gzip = promisify(zlib.gzip);
16
+ const deflate = promisify(zlib.deflate);
17
+
18
+ export class MoroHttpServer {
19
+ private server: Server;
20
+ private routes: RouteEntry[] = [];
21
+ private globalMiddleware: Middleware[] = [];
22
+ private compressionEnabled = true;
23
+ private compressionThreshold = 1024;
24
+ private logger = createFrameworkLogger("HttpServer");
25
+
26
+ constructor() {
27
+ this.server = createServer(this.handleRequest.bind(this));
28
+ }
29
+
30
+ // Middleware management
31
+ use(middleware: Middleware): void {
32
+ this.globalMiddleware.push(middleware);
33
+ }
34
+
35
+ // Routing methods
36
+ get(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
37
+ this.addRoute("GET", path, handlers);
38
+ }
39
+
40
+ post(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
41
+ this.addRoute("POST", path, handlers);
42
+ }
43
+
44
+ put(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
45
+ this.addRoute("PUT", path, handlers);
46
+ }
47
+
48
+ delete(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
49
+ this.addRoute("DELETE", path, handlers);
50
+ }
51
+
52
+ patch(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
53
+ this.addRoute("PATCH", path, handlers);
54
+ }
55
+
56
+ private addRoute(
57
+ method: string,
58
+ path: string,
59
+ handlers: (Middleware | HttpHandler)[],
60
+ ): void {
61
+ const { pattern, paramNames } = this.pathToRegex(path);
62
+ const handler = handlers.pop() as HttpHandler;
63
+ const middleware = handlers as Middleware[];
64
+
65
+ this.routes.push({
66
+ method,
67
+ path,
68
+ pattern,
69
+ paramNames,
70
+ handler,
71
+ middleware,
72
+ });
73
+ }
74
+
75
+ private pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
76
+ const paramNames: string[] = [];
77
+
78
+ // Convert parameterized routes to regex
79
+ const regexPattern = path
80
+ .replace(/\/:([^/]+)/g, (match, paramName) => {
81
+ paramNames.push(paramName);
82
+ return "/([^/]+)";
83
+ })
84
+ .replace(/\//g, "\\/");
85
+
86
+ return {
87
+ pattern: new RegExp(`^${regexPattern}$`),
88
+ paramNames,
89
+ };
90
+ }
91
+
92
+ private async handleRequest(
93
+ req: IncomingMessage,
94
+ res: ServerResponse,
95
+ ): Promise<void> {
96
+ const httpReq = this.enhanceRequest(req);
97
+ const httpRes = this.enhanceResponse(res);
98
+
99
+ try {
100
+ // Parse URL and query parameters
101
+ const url = new URL(req.url!, `http://${req.headers.host}`);
102
+ httpReq.path = url.pathname;
103
+ httpReq.query = Object.fromEntries(url.searchParams);
104
+
105
+ // Parse body for POST/PUT/PATCH requests
106
+ if (["POST", "PUT", "PATCH"].includes(req.method!)) {
107
+ httpReq.body = await this.parseBody(req);
108
+ }
109
+
110
+ // Execute global middleware first
111
+ await this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
112
+
113
+ // If middleware handled the request, don't continue
114
+ if (httpRes.headersSent) {
115
+ return;
116
+ }
117
+
118
+ // Find matching route
119
+ const route = this.findRoute(req.method!, httpReq.path);
120
+ if (!route) {
121
+ httpRes.status(404).json({ success: false, error: "Not found" });
122
+ return;
123
+ }
124
+
125
+ // Extract path parameters
126
+ const matches = httpReq.path.match(route.pattern);
127
+ if (matches) {
128
+ httpReq.params = {};
129
+ route.paramNames.forEach((name, index) => {
130
+ httpReq.params[name] = matches[index + 1];
131
+ });
132
+ }
133
+
134
+ // Execute middleware chain
135
+ await this.executeMiddleware(route.middleware, httpReq, httpRes);
136
+
137
+ // Execute handler
138
+ await route.handler(httpReq, httpRes);
139
+ } catch (error) {
140
+ this.logger.error("Request error", "RequestHandler", {
141
+ error: error instanceof Error ? error.message : String(error),
142
+ requestId: httpReq.requestId,
143
+ method: req.method,
144
+ path: req.url,
145
+ });
146
+
147
+ if (!httpRes.headersSent) {
148
+ httpRes.status(500).json({
149
+ success: false,
150
+ error: "Internal server error",
151
+ requestId: httpReq.requestId,
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ private enhanceRequest(req: IncomingMessage): HttpRequest {
158
+ const httpReq = req as HttpRequest;
159
+ httpReq.params = {};
160
+ httpReq.query = {};
161
+ httpReq.body = null;
162
+ httpReq.path = "";
163
+ httpReq.ip = req.socket.remoteAddress || "";
164
+ httpReq.requestId = Math.random().toString(36).substring(7);
165
+ httpReq.headers = req.headers as Record<string, string>;
166
+
167
+ // Parse cookies
168
+ httpReq.cookies = this.parseCookies(req.headers.cookie || "");
169
+
170
+ return httpReq;
171
+ }
172
+
173
+ private parseCookies(cookieHeader: string): Record<string, string> {
174
+ const cookies: Record<string, string> = {};
175
+ if (!cookieHeader) return cookies;
176
+
177
+ cookieHeader.split(";").forEach((cookie) => {
178
+ const [name, value] = cookie.trim().split("=");
179
+ if (name && value) {
180
+ cookies[name] = decodeURIComponent(value);
181
+ }
182
+ });
183
+
184
+ return cookies;
185
+ }
186
+
187
+ private enhanceResponse(res: ServerResponse): HttpResponse {
188
+ const httpRes = res as HttpResponse;
189
+
190
+ httpRes.json = async (data: any) => {
191
+ if (httpRes.headersSent) return;
192
+
193
+ const jsonString = JSON.stringify(data);
194
+ const buffer = Buffer.from(jsonString);
195
+
196
+ httpRes.setHeader("Content-Type", "application/json; charset=utf-8");
197
+
198
+ // Compression
199
+ if (
200
+ this.compressionEnabled &&
201
+ buffer.length > this.compressionThreshold
202
+ ) {
203
+ const acceptEncoding = httpRes.req.headers["accept-encoding"] || "";
204
+
205
+ if (acceptEncoding.includes("gzip")) {
206
+ const compressed = await gzip(buffer);
207
+ httpRes.setHeader("Content-Encoding", "gzip");
208
+ httpRes.setHeader("Content-Length", compressed.length);
209
+ httpRes.end(compressed);
210
+ return;
211
+ } else if (acceptEncoding.includes("deflate")) {
212
+ const compressed = await deflate(buffer);
213
+ httpRes.setHeader("Content-Encoding", "deflate");
214
+ httpRes.setHeader("Content-Length", compressed.length);
215
+ httpRes.end(compressed);
216
+ return;
217
+ }
218
+ }
219
+
220
+ httpRes.setHeader("Content-Length", buffer.length);
221
+ httpRes.end(buffer);
222
+ };
223
+
224
+ httpRes.status = (code: number) => {
225
+ httpRes.statusCode = code;
226
+ return httpRes;
227
+ };
228
+
229
+ httpRes.send = (data: string | Buffer) => {
230
+ if (httpRes.headersSent) return;
231
+
232
+ // Auto-detect content type if not already set
233
+ if (!httpRes.getHeader("Content-Type")) {
234
+ if (typeof data === "string") {
235
+ // Check if it's JSON
236
+ try {
237
+ JSON.parse(data);
238
+ httpRes.setHeader(
239
+ "Content-Type",
240
+ "application/json; charset=utf-8",
241
+ );
242
+ } catch {
243
+ // Default to plain text
244
+ httpRes.setHeader("Content-Type", "text/plain; charset=utf-8");
245
+ }
246
+ } else {
247
+ // Buffer data - default to octet-stream
248
+ httpRes.setHeader("Content-Type", "application/octet-stream");
249
+ }
250
+ }
251
+
252
+ httpRes.end(data);
253
+ };
254
+
255
+ httpRes.cookie = (name: string, value: string, options: any = {}) => {
256
+ const cookieValue = encodeURIComponent(value);
257
+ let cookieString = `${name}=${cookieValue}`;
258
+
259
+ if (options.maxAge) cookieString += `; Max-Age=${options.maxAge}`;
260
+ if (options.expires)
261
+ cookieString += `; Expires=${options.expires.toUTCString()}`;
262
+ if (options.httpOnly) cookieString += "; HttpOnly";
263
+ if (options.secure) cookieString += "; Secure";
264
+ if (options.sameSite) cookieString += `; SameSite=${options.sameSite}`;
265
+ if (options.domain) cookieString += `; Domain=${options.domain}`;
266
+ if (options.path) cookieString += `; Path=${options.path}`;
267
+
268
+ const existingCookies = httpRes.getHeader("Set-Cookie") || [];
269
+ const cookies = Array.isArray(existingCookies)
270
+ ? [...existingCookies]
271
+ : [existingCookies as string];
272
+ cookies.push(cookieString);
273
+ httpRes.setHeader("Set-Cookie", cookies);
274
+
275
+ return httpRes;
276
+ };
277
+
278
+ httpRes.clearCookie = (name: string, options: any = {}) => {
279
+ const clearOptions = { ...options, expires: new Date(0), maxAge: 0 };
280
+ return httpRes.cookie(name, "", clearOptions);
281
+ };
282
+
283
+ httpRes.redirect = (url: string, status: number = 302) => {
284
+ if (httpRes.headersSent) return;
285
+ httpRes.statusCode = status;
286
+ httpRes.setHeader("Location", url);
287
+ httpRes.end();
288
+ };
289
+
290
+ httpRes.sendFile = async (filePath: string) => {
291
+ if (httpRes.headersSent) return;
292
+
293
+ try {
294
+ const fs = await import("fs/promises");
295
+ const path = await import("path");
296
+ const extension = path.extname(filePath);
297
+ const mime = await this.getMimeType(extension);
298
+
299
+ const stats = await fs.stat(filePath);
300
+ const data = await fs.readFile(filePath);
301
+
302
+ // Add charset for text-based files
303
+ const contentType = this.addCharsetIfNeeded(mime);
304
+ httpRes.setHeader("Content-Type", contentType);
305
+ httpRes.setHeader("Content-Length", stats.size);
306
+
307
+ // Add security headers for file downloads
308
+ httpRes.setHeader("X-Content-Type-Options", "nosniff");
309
+
310
+ // Add caching headers
311
+ httpRes.setHeader("Last-Modified", stats.mtime.toUTCString());
312
+ httpRes.setHeader("Cache-Control", "public, max-age=31536000"); // 1 year for static files
313
+
314
+ httpRes.end(data);
315
+ } catch (error) {
316
+ httpRes.status(404).json({ success: false, error: "File not found" });
317
+ }
318
+ };
319
+
320
+ return httpRes;
321
+ }
322
+
323
+ private async getMimeType(ext: string): Promise<string> {
324
+ const mimeTypes: Record<string, string> = {
325
+ ".html": "text/html",
326
+ ".css": "text/css",
327
+ ".js": "application/javascript",
328
+ ".json": "application/json",
329
+ ".png": "image/png",
330
+ ".jpg": "image/jpeg",
331
+ ".jpeg": "image/jpeg",
332
+ ".gif": "image/gif",
333
+ ".svg": "image/svg+xml",
334
+ ".ico": "image/x-icon",
335
+ ".pdf": "application/pdf",
336
+ ".txt": "text/plain",
337
+ ".xml": "application/xml",
338
+ };
339
+
340
+ return mimeTypes[ext.toLowerCase()] || "application/octet-stream";
341
+ }
342
+
343
+ private addCharsetIfNeeded(mimeType: string): string {
344
+ // Add charset for text-based content types
345
+ const textTypes = [
346
+ "text/",
347
+ "application/json",
348
+ "application/javascript",
349
+ "application/xml",
350
+ "image/svg+xml",
351
+ ];
352
+
353
+ const needsCharset = textTypes.some((type) => mimeType.startsWith(type));
354
+
355
+ if (needsCharset && !mimeType.includes("charset")) {
356
+ return `${mimeType}; charset=utf-8`;
357
+ }
358
+
359
+ return mimeType;
360
+ }
361
+
362
+ private async parseBody(req: IncomingMessage): Promise<any> {
363
+ return new Promise((resolve, reject) => {
364
+ const chunks: Buffer[] = [];
365
+ let totalLength = 0;
366
+ const maxSize = 10 * 1024 * 1024; // 10MB limit
367
+
368
+ req.on("data", (chunk: Buffer) => {
369
+ totalLength += chunk.length;
370
+ if (totalLength > maxSize) {
371
+ reject(new Error("Request body too large"));
372
+ return;
373
+ }
374
+ chunks.push(chunk);
375
+ });
376
+
377
+ req.on("end", () => {
378
+ try {
379
+ const body = Buffer.concat(chunks);
380
+ const contentType = req.headers["content-type"] || "";
381
+
382
+ if (contentType.includes("application/json")) {
383
+ resolve(JSON.parse(body.toString()));
384
+ } else if (
385
+ contentType.includes("application/x-www-form-urlencoded")
386
+ ) {
387
+ resolve(this.parseUrlEncoded(body.toString()));
388
+ } else if (contentType.includes("multipart/form-data")) {
389
+ resolve(this.parseMultipart(body, contentType));
390
+ } else {
391
+ resolve(body.toString());
392
+ }
393
+ } catch (error) {
394
+ reject(error);
395
+ }
396
+ });
397
+
398
+ req.on("error", reject);
399
+ });
400
+ }
401
+
402
+ private parseMultipart(
403
+ buffer: Buffer,
404
+ contentType: string,
405
+ ): { fields: Record<string, string>; files: Record<string, any> } {
406
+ const boundary = contentType.split("boundary=")[1];
407
+ if (!boundary) {
408
+ throw new Error("Invalid multipart boundary");
409
+ }
410
+
411
+ const parts = buffer.toString("binary").split("--" + boundary);
412
+ const fields: Record<string, string> = {};
413
+ const files: Record<string, any> = {};
414
+
415
+ for (let i = 1; i < parts.length - 1; i++) {
416
+ const part = parts[i];
417
+ const [headers, content] = part.split("\r\n\r\n");
418
+
419
+ if (!headers || content === undefined) continue;
420
+
421
+ const nameMatch = headers.match(/name="([^"]+)"/);
422
+ const filenameMatch = headers.match(/filename="([^"]+)"/);
423
+ const contentTypeMatch = headers.match(/Content-Type: ([^\r\n]+)/);
424
+
425
+ if (nameMatch) {
426
+ const name = nameMatch[1];
427
+
428
+ if (filenameMatch) {
429
+ // This is a file
430
+ const filename = filenameMatch[1];
431
+ const mimeType = contentTypeMatch
432
+ ? contentTypeMatch[1]
433
+ : "application/octet-stream";
434
+ const fileContent = content.substring(0, content.length - 2); // Remove trailing \r\n
435
+
436
+ files[name] = {
437
+ filename,
438
+ mimetype: mimeType,
439
+ data: Buffer.from(fileContent, "binary"),
440
+ size: Buffer.byteLength(fileContent, "binary"),
441
+ };
442
+ } else {
443
+ // This is a regular field
444
+ fields[name] = content.substring(0, content.length - 2); // Remove trailing \r\n
445
+ }
446
+ }
447
+ }
448
+
449
+ return { fields, files };
450
+ }
451
+
452
+ private parseUrlEncoded(body: string): Record<string, string> {
453
+ const params = new URLSearchParams(body);
454
+ const result: Record<string, string> = {};
455
+ for (const [key, value] of params) {
456
+ result[key] = value;
457
+ }
458
+ return result;
459
+ }
460
+
461
+ private findRoute(method: string, path: string): RouteEntry | null {
462
+ return (
463
+ this.routes.find(
464
+ (route) => route.method === method && route.pattern.test(path),
465
+ ) || null
466
+ );
467
+ }
468
+
469
+ private async executeMiddleware(
470
+ middleware: Middleware[],
471
+ req: HttpRequest,
472
+ res: HttpResponse,
473
+ ): Promise<void> {
474
+ for (const mw of middleware) {
475
+ await new Promise<void>((resolve, reject) => {
476
+ let nextCalled = false;
477
+
478
+ const next = () => {
479
+ if (nextCalled) return;
480
+ nextCalled = true;
481
+ resolve();
482
+ };
483
+
484
+ try {
485
+ const result = mw(req, res, next);
486
+
487
+ // Handle async middleware
488
+ if (result instanceof Promise) {
489
+ result
490
+ .then(() => {
491
+ if (!nextCalled) next();
492
+ })
493
+ .catch(reject);
494
+ }
495
+ } catch (error) {
496
+ reject(error);
497
+ }
498
+ });
499
+ }
500
+ }
501
+
502
+ listen(port: number, callback?: () => void): void;
503
+ listen(port: number, host: string, callback?: () => void): void;
504
+ listen(
505
+ port: number,
506
+ host?: string | (() => void),
507
+ callback?: () => void,
508
+ ): void {
509
+ // Handle overloaded parameters (port, callback) or (port, host, callback)
510
+ if (typeof host === "function") {
511
+ callback = host;
512
+ host = undefined;
513
+ }
514
+
515
+ if (host) {
516
+ this.server.listen(port, host, callback);
517
+ } else {
518
+ this.server.listen(port, callback);
519
+ }
520
+ }
521
+
522
+ close(): Promise<void> {
523
+ return new Promise((resolve) => {
524
+ this.server.close(() => resolve());
525
+ });
526
+ }
527
+
528
+ getServer(): Server {
529
+ return this.server;
530
+ }
531
+ }
532
+
533
+ // Built-in middleware
534
+ export const middleware = {
535
+ cors: (
536
+ options: { origin?: string; credentials?: boolean } = {},
537
+ ): Middleware => {
538
+ return (req, res, next) => {
539
+ res.setHeader("Access-Control-Allow-Origin", options.origin || "*");
540
+ res.setHeader(
541
+ "Access-Control-Allow-Methods",
542
+ "GET, POST, PUT, DELETE, OPTIONS",
543
+ );
544
+ res.setHeader(
545
+ "Access-Control-Allow-Headers",
546
+ "Content-Type, Authorization",
547
+ );
548
+
549
+ if (options.credentials) {
550
+ res.setHeader("Access-Control-Allow-Credentials", "true");
551
+ }
552
+
553
+ if (req.method === "OPTIONS") {
554
+ res.status(200).send("");
555
+ return;
556
+ }
557
+
558
+ next();
559
+ };
560
+ },
561
+
562
+ helmet: (): Middleware => {
563
+ return (req, res, next) => {
564
+ res.setHeader("X-Content-Type-Options", "nosniff");
565
+ res.setHeader("X-Frame-Options", "DENY");
566
+ res.setHeader("X-XSS-Protection", "1; mode=block");
567
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
568
+ res.setHeader(
569
+ "Strict-Transport-Security",
570
+ "max-age=31536000; includeSubDomains",
571
+ );
572
+ res.setHeader("Content-Security-Policy", "default-src 'self'");
573
+ next();
574
+ };
575
+ },
576
+
577
+ compression: (
578
+ options: { threshold?: number; level?: number } = {},
579
+ ): Middleware => {
580
+ const zlib = require("zlib");
581
+ const threshold = options.threshold || 1024;
582
+ const level = options.level || 6;
583
+
584
+ return (req, res, next) => {
585
+ const acceptEncoding = req.headers["accept-encoding"] || "";
586
+
587
+ // Override res.json to compress responses
588
+ const originalJson = res.json;
589
+ const originalSend = res.send;
590
+
591
+ const compressResponse = (data: any, isJson = false) => {
592
+ const content = isJson ? JSON.stringify(data) : data;
593
+ const buffer = Buffer.from(content);
594
+
595
+ if (buffer.length < threshold) {
596
+ return isJson
597
+ ? originalJson.call(res, data)
598
+ : originalSend.call(res, data);
599
+ }
600
+
601
+ if (acceptEncoding.includes("gzip")) {
602
+ res.setHeader("Content-Encoding", "gzip");
603
+ zlib.gzip(buffer, { level }, (err: any, compressed: Buffer) => {
604
+ if (err) {
605
+ return isJson
606
+ ? originalJson.call(res, data)
607
+ : originalSend.call(res, data);
608
+ }
609
+ res.setHeader("Content-Length", compressed.length);
610
+ res.writeHead(res.statusCode || 200, res.getHeaders());
611
+ res.end(compressed);
612
+ });
613
+ } else if (acceptEncoding.includes("deflate")) {
614
+ res.setHeader("Content-Encoding", "deflate");
615
+ zlib.deflate(buffer, { level }, (err: any, compressed: Buffer) => {
616
+ if (err) {
617
+ return isJson
618
+ ? originalJson.call(res, data)
619
+ : originalSend.call(res, data);
620
+ }
621
+ res.setHeader("Content-Length", compressed.length);
622
+ res.writeHead(res.statusCode || 200, res.getHeaders());
623
+ res.end(compressed);
624
+ });
625
+ } else {
626
+ return isJson
627
+ ? originalJson.call(res, data)
628
+ : originalSend.call(res, data);
629
+ }
630
+ };
631
+
632
+ res.json = function (data: any) {
633
+ // Ensure charset is set for Safari compatibility
634
+ this.setHeader("Content-Type", "application/json; charset=utf-8");
635
+ compressResponse(data, true);
636
+ return this;
637
+ };
638
+
639
+ res.send = function (data: any) {
640
+ compressResponse(data, false);
641
+ return this;
642
+ };
643
+
644
+ next();
645
+ };
646
+ },
647
+
648
+ requestLogger: (): Middleware => {
649
+ return (req, res, next) => {
650
+ const start = Date.now();
651
+ res.on("finish", () => {
652
+ const duration = Date.now() - start;
653
+ // Request completed - logged by framework
654
+ });
655
+
656
+ next();
657
+ };
658
+ },
659
+
660
+ bodySize: (options: { limit?: string } = {}): Middleware => {
661
+ const limit = options.limit || "10mb";
662
+ const limitBytes = parseSize(limit);
663
+
664
+ return (req, res, next) => {
665
+ const contentLength = parseInt(req.headers["content-length"] || "0");
666
+
667
+ if (contentLength > limitBytes) {
668
+ res.status(413).json({
669
+ success: false,
670
+ error: "Request entity too large",
671
+ limit: limit,
672
+ });
673
+ return;
674
+ }
675
+
676
+ next();
677
+ };
678
+ },
679
+
680
+ static: (options: {
681
+ root: string;
682
+ maxAge?: number;
683
+ index?: string[];
684
+ dotfiles?: "allow" | "deny" | "ignore";
685
+ etag?: boolean;
686
+ }): Middleware => {
687
+ return async (req, res, next) => {
688
+ // Only handle GET and HEAD requests
689
+ if (req.method !== "GET" && req.method !== "HEAD") {
690
+ next();
691
+ return;
692
+ }
693
+
694
+ try {
695
+ const fs = await import("fs/promises");
696
+ const path = await import("path");
697
+ const crypto = await import("crypto");
698
+
699
+ let filePath = path.join(options.root, req.path);
700
+
701
+ // Security: prevent directory traversal
702
+ if (!filePath.startsWith(path.resolve(options.root))) {
703
+ res.status(403).json({ success: false, error: "Forbidden" });
704
+ return;
705
+ }
706
+
707
+ // Handle dotfiles
708
+ const basename = path.basename(filePath);
709
+ if (basename.startsWith(".")) {
710
+ if (options.dotfiles === "deny") {
711
+ res.status(403).json({ success: false, error: "Forbidden" });
712
+ return;
713
+ } else if (options.dotfiles === "ignore") {
714
+ next();
715
+ return;
716
+ }
717
+ }
718
+
719
+ let stats;
720
+ try {
721
+ stats = await fs.stat(filePath);
722
+ } catch (error) {
723
+ next(); // File not found, let other middleware handle
724
+ return;
725
+ }
726
+
727
+ // Handle directories
728
+ if (stats.isDirectory()) {
729
+ const indexFiles = options.index || ["index.html", "index.htm"];
730
+ let indexFound = false;
731
+
732
+ for (const indexFile of indexFiles) {
733
+ const indexPath = path.join(filePath, indexFile);
734
+ try {
735
+ const indexStats = await fs.stat(indexPath);
736
+ if (indexStats.isFile()) {
737
+ filePath = indexPath;
738
+ stats = indexStats;
739
+ indexFound = true;
740
+ break;
741
+ }
742
+ } catch (error) {
743
+ // Continue to next index file
744
+ }
745
+ }
746
+
747
+ if (!indexFound) {
748
+ next();
749
+ return;
750
+ }
751
+ }
752
+
753
+ // Set headers with proper mime type and charset
754
+ const ext = path.extname(filePath);
755
+ const mimeTypes: Record<string, string> = {
756
+ ".html": "text/html",
757
+ ".css": "text/css",
758
+ ".js": "application/javascript",
759
+ ".json": "application/json",
760
+ ".png": "image/png",
761
+ ".jpg": "image/jpeg",
762
+ ".jpeg": "image/jpeg",
763
+ ".gif": "image/gif",
764
+ ".svg": "image/svg+xml",
765
+ ".ico": "image/x-icon",
766
+ ".pdf": "application/pdf",
767
+ ".txt": "text/plain",
768
+ ".xml": "application/xml",
769
+ };
770
+
771
+ const baseMimeType =
772
+ mimeTypes[ext.toLowerCase()] || "application/octet-stream";
773
+
774
+ // Add charset for text-based files
775
+ const textTypes = [
776
+ "text/",
777
+ "application/json",
778
+ "application/javascript",
779
+ "application/xml",
780
+ "image/svg+xml",
781
+ ];
782
+ const needsCharset = textTypes.some((type) =>
783
+ baseMimeType.startsWith(type),
784
+ );
785
+ const contentType = needsCharset
786
+ ? `${baseMimeType}; charset=utf-8`
787
+ : baseMimeType;
788
+
789
+ res.setHeader("Content-Type", contentType);
790
+ res.setHeader("Content-Length", stats.size);
791
+
792
+ // Cache headers
793
+ if (options.maxAge) {
794
+ res.setHeader("Cache-Control", `public, max-age=${options.maxAge}`);
795
+ }
796
+
797
+ // ETag support
798
+ if (options.etag !== false) {
799
+ const etag = crypto
800
+ .createHash("md5")
801
+ .update(`${stats.mtime.getTime()}-${stats.size}`)
802
+ .digest("hex");
803
+ res.setHeader("ETag", `"${etag}"`);
804
+
805
+ // Handle conditional requests
806
+ const ifNoneMatch = req.headers["if-none-match"];
807
+ if (ifNoneMatch === `"${etag}"`) {
808
+ res.statusCode = 304;
809
+ res.end();
810
+ return;
811
+ }
812
+ }
813
+
814
+ // Handle HEAD requests
815
+ if (req.method === "HEAD") {
816
+ res.end();
817
+ return;
818
+ }
819
+
820
+ // Send file
821
+ const data = await fs.readFile(filePath);
822
+ res.end(data);
823
+ } catch (error) {
824
+ res
825
+ .status(500)
826
+ .json({ success: false, error: "Internal server error" });
827
+ }
828
+ };
829
+ },
830
+
831
+ upload: (
832
+ options: {
833
+ dest?: string;
834
+ maxFileSize?: number;
835
+ maxFiles?: number;
836
+ allowedTypes?: string[];
837
+ } = {},
838
+ ): Middleware => {
839
+ return (req, res, next) => {
840
+ const contentType = req.headers["content-type"] || "";
841
+
842
+ if (!contentType.includes("multipart/form-data")) {
843
+ next();
844
+ return;
845
+ }
846
+
847
+ // File upload handling is now built into parseBody method
848
+ // This middleware can add additional validation
849
+ if (req.body && req.body.files) {
850
+ const files = req.body.files;
851
+ const maxFileSize = options.maxFileSize || 5 * 1024 * 1024; // 5MB default
852
+ const maxFiles = options.maxFiles || 10;
853
+ const allowedTypes = options.allowedTypes;
854
+
855
+ // Validate file count
856
+ if (Object.keys(files).length > maxFiles) {
857
+ res.status(400).json({
858
+ success: false,
859
+ error: `Too many files. Maximum ${maxFiles} allowed.`,
860
+ });
861
+ return;
862
+ }
863
+
864
+ // Validate each file
865
+ for (const [fieldName, file] of Object.entries(files)) {
866
+ const fileData = file as any;
867
+
868
+ // Validate file size
869
+ if (fileData.size > maxFileSize) {
870
+ res.status(400).json({
871
+ success: false,
872
+ error: `File ${fileData.filename} is too large. Maximum ${maxFileSize} bytes allowed.`,
873
+ });
874
+ return;
875
+ }
876
+
877
+ // Validate file type
878
+ if (allowedTypes && !allowedTypes.includes(fileData.mimetype)) {
879
+ res.status(400).json({
880
+ success: false,
881
+ error: `File type ${fileData.mimetype} not allowed.`,
882
+ });
883
+ return;
884
+ }
885
+ }
886
+
887
+ // Store files in request for easy access
888
+ req.files = files;
889
+ }
890
+
891
+ next();
892
+ };
893
+ },
894
+
895
+ template: (options: {
896
+ views: string;
897
+ engine?: "moro" | "handlebars" | "ejs";
898
+ cache?: boolean;
899
+ defaultLayout?: string;
900
+ }): Middleware => {
901
+ const templateCache = new Map<string, string>();
902
+
903
+ return async (req, res, next) => {
904
+ // Add render method to response
905
+ res.render = async (template: string, data: any = {}) => {
906
+ try {
907
+ const fs = await import("fs/promises");
908
+ const path = await import("path");
909
+
910
+ const templatePath = path.join(options.views, `${template}.html`);
911
+
912
+ let templateContent: string;
913
+
914
+ // Check cache first
915
+ if (options.cache && templateCache.has(templatePath)) {
916
+ templateContent = templateCache.get(templatePath)!;
917
+ } else {
918
+ templateContent = await fs.readFile(templatePath, "utf-8");
919
+ if (options.cache) {
920
+ templateCache.set(templatePath, templateContent);
921
+ }
922
+ }
923
+
924
+ // Simple template engine - replace {{variable}} with values
925
+ let rendered = templateContent;
926
+
927
+ // Handle basic variable substitution
928
+ rendered = rendered.replace(
929
+ /\{\{(\w+)\}\}/g,
930
+ (match: string, key: string) => {
931
+ return data[key] !== undefined ? String(data[key]) : match;
932
+ },
933
+ );
934
+
935
+ // Handle nested object properties like {{user.name}}
936
+ rendered = rendered.replace(
937
+ /\{\{([\w.]+)\}\}/g,
938
+ (match: string, key: string) => {
939
+ const value = key
940
+ .split(".")
941
+ .reduce((obj: any, prop: string) => obj?.[prop], data);
942
+ return value !== undefined ? String(value) : match;
943
+ },
944
+ );
945
+
946
+ // Handle loops: {{#each items}}{{name}}{{/each}}
947
+ rendered = rendered.replace(
948
+ /\{\{#each (\w+)\}\}(.*?)\{\{\/each\}\}/gs,
949
+ (match, arrayKey, template) => {
950
+ const array = data[arrayKey];
951
+ if (!Array.isArray(array)) return "";
952
+
953
+ return array
954
+ .map((item) => {
955
+ let itemTemplate = template;
956
+ // Replace variables in the loop template
957
+ itemTemplate = itemTemplate.replace(
958
+ /\{\{(\w+)\}\}/g,
959
+ (match: string, key: string) => {
960
+ return item[key] !== undefined
961
+ ? String(item[key])
962
+ : match;
963
+ },
964
+ );
965
+ return itemTemplate;
966
+ })
967
+ .join("");
968
+ },
969
+ );
970
+
971
+ // Handle conditionals: {{#if condition}}content{{/if}}
972
+ rendered = rendered.replace(
973
+ /\{\{#if (\w+)\}\}(.*?)\{\{\/if\}\}/gs,
974
+ (match, conditionKey, content) => {
975
+ const condition = data[conditionKey];
976
+ return condition ? content : "";
977
+ },
978
+ );
979
+
980
+ // Handle layout
981
+ if (options.defaultLayout) {
982
+ const layoutPath = path.join(
983
+ options.views,
984
+ "layouts",
985
+ `${options.defaultLayout}.html`,
986
+ );
987
+ try {
988
+ let layoutContent: string;
989
+
990
+ if (options.cache && templateCache.has(layoutPath)) {
991
+ layoutContent = templateCache.get(layoutPath)!;
992
+ } else {
993
+ layoutContent = await fs.readFile(layoutPath, "utf-8");
994
+ if (options.cache) {
995
+ templateCache.set(layoutPath, layoutContent);
996
+ }
997
+ }
998
+
999
+ rendered = layoutContent.replace(/\{\{body\}\}/, rendered);
1000
+ } catch (error) {
1001
+ // Layout not found, use template as-is
1002
+ }
1003
+ }
1004
+
1005
+ res.setHeader("Content-Type", "text/html");
1006
+ res.end(rendered);
1007
+ } catch (error) {
1008
+ res
1009
+ .status(500)
1010
+ .json({ success: false, error: "Template rendering failed" });
1011
+ }
1012
+ };
1013
+
1014
+ next();
1015
+ };
1016
+ },
1017
+
1018
+ // HTTP/2 Server Push middleware
1019
+ http2Push: (
1020
+ options: {
1021
+ resources?: Array<{ path: string; as: string; type?: string }>;
1022
+ condition?: (req: any) => boolean;
1023
+ } = {},
1024
+ ): Middleware => {
1025
+ return (req, res, next) => {
1026
+ // Add HTTP/2 push capability to response
1027
+ (res as any).push = (path: string, options: any = {}) => {
1028
+ // Check if HTTP/2 is supported
1029
+ if (
1030
+ req.httpVersion === "2.0" &&
1031
+ (res as any).stream &&
1032
+ (res as any).stream.pushAllowed
1033
+ ) {
1034
+ try {
1035
+ const pushStream = (res as any).stream.pushStream({
1036
+ ":method": "GET",
1037
+ ":path": path,
1038
+ ...options.headers,
1039
+ });
1040
+
1041
+ if (pushStream) {
1042
+ // Handle push stream
1043
+ return pushStream;
1044
+ }
1045
+ } catch (error) {
1046
+ // Push failed, continue normally
1047
+ }
1048
+ }
1049
+ return null;
1050
+ };
1051
+
1052
+ // Auto-push configured resources
1053
+ if (options.resources && (!options.condition || options.condition(req))) {
1054
+ for (const resource of options.resources) {
1055
+ (res as any).push?.(resource.path, {
1056
+ headers: {
1057
+ "content-type": resource.type || "text/plain",
1058
+ },
1059
+ });
1060
+ }
1061
+ }
1062
+
1063
+ next();
1064
+ };
1065
+ },
1066
+
1067
+ // Server-Sent Events middleware
1068
+ sse: (
1069
+ options: {
1070
+ heartbeat?: number;
1071
+ retry?: number;
1072
+ cors?: boolean;
1073
+ } = {},
1074
+ ): Middleware => {
1075
+ return (req, res, next) => {
1076
+ // Only handle SSE requests
1077
+ if (req.headers.accept?.includes("text/event-stream")) {
1078
+ // Set SSE headers
1079
+ res.writeHead(200, {
1080
+ "Content-Type": "text/event-stream",
1081
+ "Cache-Control": "no-cache",
1082
+ Connection: "keep-alive",
1083
+ "Access-Control-Allow-Origin": options.cors ? "*" : undefined,
1084
+ "Access-Control-Allow-Headers": options.cors
1085
+ ? "Cache-Control"
1086
+ : undefined,
1087
+ });
1088
+
1089
+ // Add SSE methods to response
1090
+ (res as any).sendEvent = (data: any, event?: string, id?: string) => {
1091
+ if (id) res.write(`id: ${id}\n`);
1092
+ if (event) res.write(`event: ${event}\n`);
1093
+ res.write(
1094
+ `data: ${typeof data === "string" ? data : JSON.stringify(data)}\n\n`,
1095
+ );
1096
+ };
1097
+
1098
+ (res as any).sendComment = (comment: string) => {
1099
+ res.write(`: ${comment}\n\n`);
1100
+ };
1101
+
1102
+ (res as any).sendRetry = (ms: number) => {
1103
+ res.write(`retry: ${ms}\n\n`);
1104
+ };
1105
+
1106
+ // Set up heartbeat if configured
1107
+ let heartbeatInterval: NodeJS.Timeout | null = null;
1108
+ if (options.heartbeat) {
1109
+ heartbeatInterval = setInterval(() => {
1110
+ (res as any).sendComment("heartbeat");
1111
+ }, options.heartbeat);
1112
+ }
1113
+
1114
+ // Set retry if configured
1115
+ if (options.retry) {
1116
+ (res as any).sendRetry(options.retry);
1117
+ }
1118
+
1119
+ // Clean up on close
1120
+ req.on("close", () => {
1121
+ if (heartbeatInterval) {
1122
+ clearInterval(heartbeatInterval);
1123
+ }
1124
+ });
1125
+
1126
+ // Don't call next() - this middleware handles the response
1127
+ return;
1128
+ }
1129
+
1130
+ next();
1131
+ };
1132
+ },
1133
+
1134
+ // Range request middleware for streaming
1135
+ range: (
1136
+ options: {
1137
+ acceptRanges?: string;
1138
+ maxRanges?: number;
1139
+ } = {},
1140
+ ): Middleware => {
1141
+ return async (req, res, next) => {
1142
+ // Add range support to response
1143
+ (res as any).sendRange = async (filePath: string, stats?: any) => {
1144
+ try {
1145
+ const fs = await import("fs/promises");
1146
+ const path = await import("path");
1147
+
1148
+ if (!stats) {
1149
+ stats = await fs.stat(filePath);
1150
+ }
1151
+
1152
+ const fileSize = stats.size;
1153
+ const range = req.headers.range;
1154
+
1155
+ // Set Accept-Ranges header
1156
+ res.setHeader("Accept-Ranges", options.acceptRanges || "bytes");
1157
+
1158
+ if (!range) {
1159
+ // No range requested, send entire file
1160
+ res.setHeader("Content-Length", fileSize);
1161
+ const data = await fs.readFile(filePath);
1162
+ res.end(data);
1163
+ return;
1164
+ }
1165
+
1166
+ // Parse range header
1167
+ const ranges = range
1168
+ .replace(/bytes=/, "")
1169
+ .split(",")
1170
+ .map((r) => {
1171
+ const [start, end] = r.split("-");
1172
+ return {
1173
+ start: start ? parseInt(start) : 0,
1174
+ end: end ? parseInt(end) : fileSize - 1,
1175
+ };
1176
+ });
1177
+
1178
+ // Validate ranges
1179
+ if (options.maxRanges && ranges.length > options.maxRanges) {
1180
+ res.status(416).json({ success: false, error: "Too many ranges" });
1181
+ return;
1182
+ }
1183
+
1184
+ if (ranges.length === 1) {
1185
+ // Single range
1186
+ const { start, end } = ranges[0];
1187
+ const chunkSize = end - start + 1;
1188
+
1189
+ if (start >= fileSize || end >= fileSize) {
1190
+ res.status(416).setHeader("Content-Range", `bytes */${fileSize}`);
1191
+ res.json({ success: false, error: "Range not satisfiable" });
1192
+ return;
1193
+ }
1194
+
1195
+ res.status(206);
1196
+ res.setHeader("Content-Range", `bytes ${start}-${end}/${fileSize}`);
1197
+ res.setHeader("Content-Length", chunkSize);
1198
+
1199
+ // Stream the range
1200
+ const stream = require("fs").createReadStream(filePath, {
1201
+ start,
1202
+ end,
1203
+ });
1204
+ stream.pipe(res);
1205
+ } else {
1206
+ // Multiple ranges - multipart response
1207
+ const boundary = "MULTIPART_BYTERANGES";
1208
+ res.status(206);
1209
+ res.setHeader(
1210
+ "Content-Type",
1211
+ `multipart/byteranges; boundary=${boundary}`,
1212
+ );
1213
+
1214
+ for (const { start, end } of ranges) {
1215
+ if (start >= fileSize || end >= fileSize) continue;
1216
+
1217
+ const chunkSize = end - start + 1;
1218
+ res.write(`\r\n--${boundary}\r\n`);
1219
+ res.write(
1220
+ `Content-Range: bytes ${start}-${end}/${fileSize}\r\n\r\n`,
1221
+ );
1222
+
1223
+ const stream = require("fs").createReadStream(filePath, {
1224
+ start,
1225
+ end,
1226
+ });
1227
+ await new Promise((resolve) => {
1228
+ stream.on("end", resolve);
1229
+ stream.pipe(res, { end: false });
1230
+ });
1231
+ }
1232
+ res.write(`\r\n--${boundary}--\r\n`);
1233
+ res.end();
1234
+ }
1235
+ } catch (error) {
1236
+ res
1237
+ .status(500)
1238
+ .json({ success: false, error: "Range request failed" });
1239
+ }
1240
+ };
1241
+
1242
+ next();
1243
+ };
1244
+ },
1245
+
1246
+ // CSRF Protection middleware
1247
+ csrf: (
1248
+ options: {
1249
+ secret?: string;
1250
+ tokenLength?: number;
1251
+ cookieName?: string;
1252
+ headerName?: string;
1253
+ ignoreMethods?: string[];
1254
+ sameSite?: boolean;
1255
+ } = {},
1256
+ ): Middleware => {
1257
+ const secret = options.secret || "moro-csrf-secret";
1258
+ const tokenLength = options.tokenLength || 32;
1259
+ const cookieName = options.cookieName || "_csrf";
1260
+ const headerName = options.headerName || "x-csrf-token";
1261
+ const ignoreMethods = options.ignoreMethods || ["GET", "HEAD", "OPTIONS"];
1262
+
1263
+ const generateToken = () => {
1264
+ const crypto = require("crypto");
1265
+ return crypto.randomBytes(tokenLength).toString("hex");
1266
+ };
1267
+
1268
+ const verifyToken = (token: string, sessionToken: string) => {
1269
+ return token && sessionToken && token === sessionToken;
1270
+ };
1271
+
1272
+ return (req, res, next) => {
1273
+ // Add CSRF token generation method
1274
+ (req as any).csrfToken = () => {
1275
+ if (!(req as any)._csrfToken) {
1276
+ (req as any)._csrfToken = generateToken();
1277
+ // Set token in cookie
1278
+ res.cookie(cookieName, (req as any)._csrfToken, {
1279
+ httpOnly: true,
1280
+ sameSite: options.sameSite !== false ? "strict" : undefined,
1281
+ secure:
1282
+ req.headers["x-forwarded-proto"] === "https" ||
1283
+ (req.socket as any).encrypted,
1284
+ });
1285
+ }
1286
+ return (req as any)._csrfToken;
1287
+ };
1288
+
1289
+ // Skip verification for safe methods
1290
+ if (ignoreMethods.includes(req.method!)) {
1291
+ next();
1292
+ return;
1293
+ }
1294
+
1295
+ // Get token from header or body
1296
+ const token =
1297
+ req.headers[headerName] ||
1298
+ (req.body && req.body._csrf) ||
1299
+ (req.query && req.query._csrf);
1300
+
1301
+ // Get session token from cookie
1302
+ const sessionToken = req.cookies?.[cookieName];
1303
+
1304
+ if (!verifyToken(token as string, sessionToken || "")) {
1305
+ res.status(403).json({
1306
+ success: false,
1307
+ error: "Invalid CSRF token",
1308
+ code: "CSRF_TOKEN_MISMATCH",
1309
+ });
1310
+ return;
1311
+ }
1312
+
1313
+ next();
1314
+ };
1315
+ },
1316
+
1317
+ // Content Security Policy middleware
1318
+ csp: (
1319
+ options: {
1320
+ directives?: {
1321
+ defaultSrc?: string[];
1322
+ scriptSrc?: string[];
1323
+ styleSrc?: string[];
1324
+ imgSrc?: string[];
1325
+ connectSrc?: string[];
1326
+ fontSrc?: string[];
1327
+ objectSrc?: string[];
1328
+ mediaSrc?: string[];
1329
+ frameSrc?: string[];
1330
+ childSrc?: string[];
1331
+ workerSrc?: string[];
1332
+ formAction?: string[];
1333
+ upgradeInsecureRequests?: boolean;
1334
+ blockAllMixedContent?: boolean;
1335
+ };
1336
+ reportOnly?: boolean;
1337
+ reportUri?: string;
1338
+ nonce?: boolean;
1339
+ } = {},
1340
+ ): Middleware => {
1341
+ return (req, res, next) => {
1342
+ const directives = options.directives || {
1343
+ defaultSrc: ["'self'"],
1344
+ scriptSrc: ["'self'"],
1345
+ styleSrc: ["'self'", "'unsafe-inline'"],
1346
+ imgSrc: ["'self'", "data:", "https:"],
1347
+ connectSrc: ["'self'"],
1348
+ fontSrc: ["'self'"],
1349
+ objectSrc: ["'none'"],
1350
+ mediaSrc: ["'self'"],
1351
+ frameSrc: ["'none'"],
1352
+ };
1353
+
1354
+ // Generate nonce if requested
1355
+ let nonce: string | undefined;
1356
+ if (options.nonce) {
1357
+ const crypto = require("crypto");
1358
+ nonce = crypto.randomBytes(16).toString("base64");
1359
+ (req as any).cspNonce = nonce;
1360
+ }
1361
+
1362
+ // Build CSP header value
1363
+ const cspParts: string[] = [];
1364
+
1365
+ for (const [directive, sources] of Object.entries(directives)) {
1366
+ if (directive === "upgradeInsecureRequests" && sources === true) {
1367
+ cspParts.push("upgrade-insecure-requests");
1368
+ } else if (directive === "blockAllMixedContent" && sources === true) {
1369
+ cspParts.push("block-all-mixed-content");
1370
+ } else if (Array.isArray(sources)) {
1371
+ let sourceList = sources.join(" ");
1372
+
1373
+ // Add nonce to script-src and style-src if enabled
1374
+ if (
1375
+ nonce &&
1376
+ (directive === "scriptSrc" || directive === "styleSrc")
1377
+ ) {
1378
+ sourceList += ` 'nonce-${nonce}'`;
1379
+ }
1380
+
1381
+ // Convert camelCase to kebab-case
1382
+ const kebabDirective = directive
1383
+ .replace(/([A-Z])/g, "-$1")
1384
+ .toLowerCase();
1385
+ cspParts.push(`${kebabDirective} ${sourceList}`);
1386
+ }
1387
+ }
1388
+
1389
+ // Add report-uri if specified
1390
+ if (options.reportUri) {
1391
+ cspParts.push(`report-uri ${options.reportUri}`);
1392
+ }
1393
+
1394
+ const cspValue = cspParts.join("; ");
1395
+ const headerName = options.reportOnly
1396
+ ? "Content-Security-Policy-Report-Only"
1397
+ : "Content-Security-Policy";
1398
+
1399
+ res.setHeader(headerName, cspValue);
1400
+
1401
+ next();
1402
+ };
1403
+ },
1404
+ };
1405
+
1406
+ function parseSize(size: string): number {
1407
+ const units: { [key: string]: number } = {
1408
+ b: 1,
1409
+ kb: 1024,
1410
+ mb: 1024 * 1024,
1411
+ gb: 1024 * 1024 * 1024,
1412
+ };
1413
+
1414
+ const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
1415
+ if (!match) return 1024 * 1024; // Default 1MB
1416
+
1417
+ const value = parseFloat(match[1]);
1418
+ const unit = match[2] || "b";
1419
+
1420
+ return Math.round(value * units[unit]);
1421
+ }