@morojs/moro 1.6.8 → 1.7.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 (300) hide show
  1. package/README.md +3 -1
  2. package/dist/core/auth/morojs-adapter.js +16 -6
  3. package/dist/core/auth/morojs-adapter.js.map +1 -1
  4. package/dist/core/config/config-sources.js +27 -15
  5. package/dist/core/config/config-sources.js.map +1 -1
  6. package/dist/core/config/config-validator.js +201 -6
  7. package/dist/core/config/config-validator.js.map +1 -1
  8. package/dist/core/docs/openapi-generator.js +8 -9
  9. package/dist/core/docs/openapi-generator.js.map +1 -1
  10. package/dist/core/events/event-bus.js +1 -1
  11. package/dist/core/events/event-bus.js.map +1 -1
  12. package/dist/core/framework.d.ts +4 -2
  13. package/dist/core/framework.js +25 -24
  14. package/dist/core/framework.js.map +1 -1
  15. package/dist/core/graphql/core.js +34 -8
  16. package/dist/core/graphql/core.js.map +1 -1
  17. package/dist/core/grpc/adapters/grpc-js-adapter.d.ts +28 -0
  18. package/dist/core/grpc/adapters/grpc-js-adapter.js +449 -0
  19. package/dist/core/grpc/adapters/grpc-js-adapter.js.map +1 -0
  20. package/dist/core/grpc/adapters/index.d.ts +1 -0
  21. package/dist/core/grpc/adapters/index.js +6 -0
  22. package/dist/core/grpc/adapters/index.js.map +1 -0
  23. package/dist/core/grpc/grpc-adapter.d.ts +47 -0
  24. package/dist/core/grpc/grpc-adapter.js +4 -0
  25. package/dist/core/grpc/grpc-adapter.js.map +1 -0
  26. package/dist/core/grpc/grpc-manager.d.ts +59 -0
  27. package/dist/core/grpc/grpc-manager.js +218 -0
  28. package/dist/core/grpc/grpc-manager.js.map +1 -0
  29. package/dist/core/grpc/index.d.ts +7 -0
  30. package/dist/core/grpc/index.js +10 -0
  31. package/dist/core/grpc/index.js.map +1 -0
  32. package/dist/core/grpc/middleware/auth.d.ts +22 -0
  33. package/dist/core/grpc/middleware/auth.js +126 -0
  34. package/dist/core/grpc/middleware/auth.js.map +1 -0
  35. package/dist/core/grpc/middleware/logging.d.ts +19 -0
  36. package/dist/core/grpc/middleware/logging.js +57 -0
  37. package/dist/core/grpc/middleware/logging.js.map +1 -0
  38. package/dist/core/grpc/middleware/validation.d.ts +18 -0
  39. package/dist/core/grpc/middleware/validation.js +126 -0
  40. package/dist/core/grpc/middleware/validation.js.map +1 -0
  41. package/dist/core/grpc/types.d.ts +233 -0
  42. package/dist/core/grpc/types.js +36 -0
  43. package/dist/core/grpc/types.js.map +1 -0
  44. package/dist/core/http/http-server.d.ts +13 -84
  45. package/dist/core/http/http-server.js +205 -792
  46. package/dist/core/http/http-server.js.map +1 -1
  47. package/dist/core/http/http2-server.d.ts +131 -0
  48. package/dist/core/http/http2-server.js +803 -0
  49. package/dist/core/http/http2-server.js.map +1 -0
  50. package/dist/core/http/index.d.ts +3 -1
  51. package/dist/core/http/index.js +2 -1
  52. package/dist/core/http/index.js.map +1 -1
  53. package/dist/core/http/uws-http-server.js +21 -26
  54. package/dist/core/http/uws-http-server.js.map +1 -1
  55. package/dist/core/jobs/job-executor.js +6 -1
  56. package/dist/core/jobs/job-executor.js.map +1 -1
  57. package/dist/core/jobs/job-scheduler.js +4 -1
  58. package/dist/core/jobs/job-scheduler.js.map +1 -1
  59. package/dist/core/jobs/leader-election.js +2 -1
  60. package/dist/core/jobs/leader-election.js.map +1 -1
  61. package/dist/core/logger/logger.js +41 -18
  62. package/dist/core/logger/logger.js.map +1 -1
  63. package/dist/core/logger/outputs.js +9 -3
  64. package/dist/core/logger/outputs.js.map +1 -1
  65. package/dist/core/mail/adapters/console-adapter.d.ts +14 -0
  66. package/dist/core/mail/adapters/console-adapter.js +89 -0
  67. package/dist/core/mail/adapters/console-adapter.js.map +1 -0
  68. package/dist/core/mail/adapters/index.d.ts +5 -0
  69. package/dist/core/mail/adapters/index.js +8 -0
  70. package/dist/core/mail/adapters/index.js.map +1 -0
  71. package/dist/core/mail/adapters/nodemailer-adapter.d.ts +18 -0
  72. package/dist/core/mail/adapters/nodemailer-adapter.js +188 -0
  73. package/dist/core/mail/adapters/nodemailer-adapter.js.map +1 -0
  74. package/dist/core/mail/adapters/resend-adapter.d.ts +18 -0
  75. package/dist/core/mail/adapters/resend-adapter.js +169 -0
  76. package/dist/core/mail/adapters/resend-adapter.js.map +1 -0
  77. package/dist/core/mail/adapters/sendgrid-adapter.d.ts +19 -0
  78. package/dist/core/mail/adapters/sendgrid-adapter.js +186 -0
  79. package/dist/core/mail/adapters/sendgrid-adapter.js.map +1 -0
  80. package/dist/core/mail/adapters/ses-adapter.d.ts +18 -0
  81. package/dist/core/mail/adapters/ses-adapter.js +167 -0
  82. package/dist/core/mail/adapters/ses-adapter.js.map +1 -0
  83. package/dist/core/mail/index.d.ts +5 -0
  84. package/dist/core/mail/index.js +8 -0
  85. package/dist/core/mail/index.js.map +1 -0
  86. package/dist/core/mail/mail-adapter.d.ts +62 -0
  87. package/dist/core/mail/mail-adapter.js +83 -0
  88. package/dist/core/mail/mail-adapter.js.map +1 -0
  89. package/dist/core/mail/mail-manager.d.ts +63 -0
  90. package/dist/core/mail/mail-manager.js +302 -0
  91. package/dist/core/mail/mail-manager.js.map +1 -0
  92. package/dist/core/mail/template-engine.d.ts +43 -0
  93. package/dist/core/mail/template-engine.js +239 -0
  94. package/dist/core/mail/template-engine.js.map +1 -0
  95. package/dist/core/mail/types.d.ts +237 -0
  96. package/dist/core/mail/types.js +4 -0
  97. package/dist/core/mail/types.js.map +1 -0
  98. package/dist/core/middleware/built-in/body-size/core.d.ts +12 -0
  99. package/dist/core/middleware/built-in/body-size/core.js +52 -0
  100. package/dist/core/middleware/built-in/body-size/core.js.map +1 -0
  101. package/dist/core/middleware/built-in/body-size/hook.d.ts +2 -0
  102. package/dist/core/middleware/built-in/body-size/hook.js +12 -0
  103. package/dist/core/middleware/built-in/body-size/hook.js.map +1 -0
  104. package/dist/core/middleware/built-in/body-size/index.d.ts +6 -0
  105. package/dist/core/middleware/built-in/body-size/index.js +7 -0
  106. package/dist/core/middleware/built-in/body-size/index.js.map +1 -0
  107. package/dist/core/middleware/built-in/body-size/middleware.d.ts +14 -0
  108. package/dist/core/middleware/built-in/body-size/middleware.js +22 -0
  109. package/dist/core/middleware/built-in/body-size/middleware.js.map +1 -0
  110. package/dist/core/middleware/built-in/cache/core.d.ts +20 -1
  111. package/dist/core/middleware/built-in/cache/core.js.map +1 -1
  112. package/dist/core/middleware/built-in/cache/hook.d.ts +38 -1
  113. package/dist/core/middleware/built-in/cache/hook.js +202 -16
  114. package/dist/core/middleware/built-in/cache/hook.js.map +1 -1
  115. package/dist/core/middleware/built-in/cache/index.js +1 -1
  116. package/dist/core/middleware/built-in/cache/index.js.map +1 -1
  117. package/dist/core/middleware/built-in/compression/core.d.ts +16 -0
  118. package/dist/core/middleware/built-in/compression/core.js +75 -0
  119. package/dist/core/middleware/built-in/compression/core.js.map +1 -0
  120. package/dist/core/middleware/built-in/compression/hook.d.ts +2 -0
  121. package/dist/core/middleware/built-in/compression/hook.js +14 -0
  122. package/dist/core/middleware/built-in/compression/hook.js.map +1 -0
  123. package/dist/core/middleware/built-in/compression/index.d.ts +6 -0
  124. package/dist/core/middleware/built-in/compression/index.js +7 -0
  125. package/dist/core/middleware/built-in/compression/index.js.map +1 -0
  126. package/dist/core/middleware/built-in/compression/middleware.d.ts +20 -0
  127. package/dist/core/middleware/built-in/compression/middleware.js +28 -0
  128. package/dist/core/middleware/built-in/compression/middleware.js.map +1 -0
  129. package/dist/core/middleware/built-in/cookie/core.js +37 -9
  130. package/dist/core/middleware/built-in/cookie/core.js.map +1 -1
  131. package/dist/core/middleware/built-in/helmet/core.d.ts +19 -0
  132. package/dist/core/middleware/built-in/helmet/core.js +70 -0
  133. package/dist/core/middleware/built-in/helmet/core.js.map +1 -0
  134. package/dist/core/middleware/built-in/helmet/hook.d.ts +2 -0
  135. package/dist/core/middleware/built-in/helmet/hook.js +12 -0
  136. package/dist/core/middleware/built-in/helmet/hook.js.map +1 -0
  137. package/dist/core/middleware/built-in/helmet/index.d.ts +6 -0
  138. package/dist/core/middleware/built-in/helmet/index.js +7 -0
  139. package/dist/core/middleware/built-in/helmet/index.js.map +1 -0
  140. package/dist/core/middleware/built-in/helmet/middleware.d.ts +22 -0
  141. package/dist/core/middleware/built-in/helmet/middleware.js +28 -0
  142. package/dist/core/middleware/built-in/helmet/middleware.js.map +1 -0
  143. package/dist/core/middleware/built-in/http2/core.d.ts +35 -0
  144. package/dist/core/middleware/built-in/http2/core.js +128 -0
  145. package/dist/core/middleware/built-in/http2/core.js.map +1 -0
  146. package/dist/core/middleware/built-in/http2/hook.d.ts +5 -0
  147. package/dist/core/middleware/built-in/http2/hook.js +34 -0
  148. package/dist/core/middleware/built-in/http2/hook.js.map +1 -0
  149. package/dist/core/middleware/built-in/http2/index.d.ts +8 -0
  150. package/dist/core/middleware/built-in/http2/index.js +10 -0
  151. package/dist/core/middleware/built-in/http2/index.js.map +1 -0
  152. package/dist/core/middleware/built-in/http2/middleware.d.ts +20 -0
  153. package/dist/core/middleware/built-in/http2/middleware.js +31 -0
  154. package/dist/core/middleware/built-in/http2/middleware.js.map +1 -0
  155. package/dist/core/middleware/built-in/index.d.ts +18 -0
  156. package/dist/core/middleware/built-in/index.js +28 -0
  157. package/dist/core/middleware/built-in/index.js.map +1 -1
  158. package/dist/core/middleware/built-in/range/core.d.ts +16 -0
  159. package/dist/core/middleware/built-in/range/core.js +112 -0
  160. package/dist/core/middleware/built-in/range/core.js.map +1 -0
  161. package/dist/core/middleware/built-in/range/hook.d.ts +2 -0
  162. package/dist/core/middleware/built-in/range/hook.js +12 -0
  163. package/dist/core/middleware/built-in/range/hook.js.map +1 -0
  164. package/dist/core/middleware/built-in/range/index.d.ts +6 -0
  165. package/dist/core/middleware/built-in/range/index.js +7 -0
  166. package/dist/core/middleware/built-in/range/index.js.map +1 -0
  167. package/dist/core/middleware/built-in/range/middleware.d.ts +21 -0
  168. package/dist/core/middleware/built-in/range/middleware.js +27 -0
  169. package/dist/core/middleware/built-in/range/middleware.js.map +1 -0
  170. package/dist/core/middleware/built-in/session/core.js +14 -1
  171. package/dist/core/middleware/built-in/session/core.js.map +1 -1
  172. package/dist/core/middleware/built-in/static/core.d.ts +20 -0
  173. package/dist/core/middleware/built-in/static/core.js +143 -0
  174. package/dist/core/middleware/built-in/static/core.js.map +1 -0
  175. package/dist/core/middleware/built-in/static/hook.d.ts +2 -0
  176. package/dist/core/middleware/built-in/static/hook.js +12 -0
  177. package/dist/core/middleware/built-in/static/hook.js.map +1 -0
  178. package/dist/core/middleware/built-in/static/index.d.ts +6 -0
  179. package/dist/core/middleware/built-in/static/index.js +7 -0
  180. package/dist/core/middleware/built-in/static/index.js.map +1 -0
  181. package/dist/core/middleware/built-in/static/middleware.d.ts +18 -0
  182. package/dist/core/middleware/built-in/static/middleware.js +26 -0
  183. package/dist/core/middleware/built-in/static/middleware.js.map +1 -0
  184. package/dist/core/middleware/built-in/template/core.d.ts +19 -0
  185. package/dist/core/middleware/built-in/template/core.js +108 -0
  186. package/dist/core/middleware/built-in/template/core.js.map +1 -0
  187. package/dist/core/middleware/built-in/template/hook.d.ts +2 -0
  188. package/dist/core/middleware/built-in/template/hook.js +12 -0
  189. package/dist/core/middleware/built-in/template/hook.js.map +1 -0
  190. package/dist/core/middleware/built-in/template/index.d.ts +6 -0
  191. package/dist/core/middleware/built-in/template/index.js +7 -0
  192. package/dist/core/middleware/built-in/template/index.js.map +1 -0
  193. package/dist/core/middleware/built-in/template/middleware.d.ts +21 -0
  194. package/dist/core/middleware/built-in/template/middleware.js +27 -0
  195. package/dist/core/middleware/built-in/template/middleware.js.map +1 -0
  196. package/dist/core/middleware/built-in/upload/core.d.ts +29 -0
  197. package/dist/core/middleware/built-in/upload/core.js +66 -0
  198. package/dist/core/middleware/built-in/upload/core.js.map +1 -0
  199. package/dist/core/middleware/built-in/upload/hook.d.ts +2 -0
  200. package/dist/core/middleware/built-in/upload/hook.js +25 -0
  201. package/dist/core/middleware/built-in/upload/hook.js.map +1 -0
  202. package/dist/core/middleware/built-in/upload/index.d.ts +6 -0
  203. package/dist/core/middleware/built-in/upload/index.js +7 -0
  204. package/dist/core/middleware/built-in/upload/index.js.map +1 -0
  205. package/dist/core/middleware/built-in/upload/middleware.d.ts +18 -0
  206. package/dist/core/middleware/built-in/upload/middleware.js +41 -0
  207. package/dist/core/middleware/built-in/upload/middleware.js.map +1 -0
  208. package/dist/core/middleware/built-in/validation/middleware.js +2 -1
  209. package/dist/core/middleware/built-in/validation/middleware.js.map +1 -1
  210. package/dist/core/networking/adapters/uws-adapter.js +54 -6
  211. package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
  212. package/dist/core/networking/adapters/ws-adapter.js +56 -17
  213. package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
  214. package/dist/core/pooling/object-pool-manager.js +9 -1
  215. package/dist/core/pooling/object-pool-manager.js.map +1 -1
  216. package/dist/core/queue/adapters/bull-adapter.d.ts +86 -0
  217. package/dist/core/queue/adapters/bull-adapter.js +330 -0
  218. package/dist/core/queue/adapters/bull-adapter.js.map +1 -0
  219. package/dist/core/queue/adapters/index.d.ts +9 -0
  220. package/dist/core/queue/adapters/index.js +10 -0
  221. package/dist/core/queue/adapters/index.js.map +1 -0
  222. package/dist/core/queue/adapters/kafka-adapter.d.ts +86 -0
  223. package/dist/core/queue/adapters/kafka-adapter.js +462 -0
  224. package/dist/core/queue/adapters/kafka-adapter.js.map +1 -0
  225. package/dist/core/queue/adapters/memory-adapter.d.ts +87 -0
  226. package/dist/core/queue/adapters/memory-adapter.js +415 -0
  227. package/dist/core/queue/adapters/memory-adapter.js.map +1 -0
  228. package/dist/core/queue/adapters/rabbitmq-adapter.d.ts +86 -0
  229. package/dist/core/queue/adapters/rabbitmq-adapter.js +436 -0
  230. package/dist/core/queue/adapters/rabbitmq-adapter.js.map +1 -0
  231. package/dist/core/queue/adapters/sqs-adapter.d.ts +102 -0
  232. package/dist/core/queue/adapters/sqs-adapter.js +522 -0
  233. package/dist/core/queue/adapters/sqs-adapter.js.map +1 -0
  234. package/dist/core/queue/index.d.ts +11 -0
  235. package/dist/core/queue/index.js +14 -0
  236. package/dist/core/queue/index.js.map +1 -0
  237. package/dist/core/queue/middleware/index.d.ts +7 -0
  238. package/dist/core/queue/middleware/index.js +8 -0
  239. package/dist/core/queue/middleware/index.js.map +1 -0
  240. package/dist/core/queue/middleware/monitoring.d.ts +84 -0
  241. package/dist/core/queue/middleware/monitoring.js +145 -0
  242. package/dist/core/queue/middleware/monitoring.js.map +1 -0
  243. package/dist/core/queue/middleware/priority.d.ts +61 -0
  244. package/dist/core/queue/middleware/priority.js +90 -0
  245. package/dist/core/queue/middleware/priority.js.map +1 -0
  246. package/dist/core/queue/middleware/rate-limit.d.ts +34 -0
  247. package/dist/core/queue/middleware/rate-limit.js +109 -0
  248. package/dist/core/queue/middleware/rate-limit.js.map +1 -0
  249. package/dist/core/queue/queue-adapter.d.ts +73 -0
  250. package/dist/core/queue/queue-adapter.js +20 -0
  251. package/dist/core/queue/queue-adapter.js.map +1 -0
  252. package/dist/core/queue/queue-manager.d.ts +92 -0
  253. package/dist/core/queue/queue-manager.js +327 -0
  254. package/dist/core/queue/queue-manager.js.map +1 -0
  255. package/dist/core/queue/types.d.ts +205 -0
  256. package/dist/core/queue/types.js +6 -0
  257. package/dist/core/queue/types.js.map +1 -0
  258. package/dist/core/routing/index.js +41 -10
  259. package/dist/core/routing/index.js.map +1 -1
  260. package/dist/core/routing/radix-tree.d.ts +48 -0
  261. package/dist/core/routing/radix-tree.js +211 -0
  262. package/dist/core/routing/radix-tree.js.map +1 -0
  263. package/dist/core/routing/router.d.ts +10 -9
  264. package/dist/core/routing/router.js +3 -1
  265. package/dist/core/routing/router.js.map +1 -1
  266. package/dist/core/routing/unified-router.d.ts +18 -12
  267. package/dist/core/routing/unified-router.js +220 -163
  268. package/dist/core/routing/unified-router.js.map +1 -1
  269. package/dist/core/runtime/aws-lambda-adapter.js +21 -10
  270. package/dist/core/runtime/aws-lambda-adapter.js.map +1 -1
  271. package/dist/core/runtime/base-adapter.js +15 -5
  272. package/dist/core/runtime/base-adapter.js.map +1 -1
  273. package/dist/core/runtime/cloudflare-workers-adapter.js +35 -12
  274. package/dist/core/runtime/cloudflare-workers-adapter.js.map +1 -1
  275. package/dist/core/runtime/vercel-edge-adapter.js +16 -6
  276. package/dist/core/runtime/vercel-edge-adapter.js.map +1 -1
  277. package/dist/core/utilities/container.js +3 -1
  278. package/dist/core/utilities/container.js.map +1 -1
  279. package/dist/core/workers/facade.d.ts +74 -0
  280. package/dist/core/workers/facade.js +98 -0
  281. package/dist/core/workers/facade.js.map +1 -0
  282. package/dist/core/workers/index.d.ts +2 -0
  283. package/dist/core/workers/index.js +6 -0
  284. package/dist/core/workers/index.js.map +1 -0
  285. package/dist/core/workers/worker-manager.d.ts +124 -0
  286. package/dist/core/workers/worker-manager.js +299 -0
  287. package/dist/core/workers/worker-manager.js.map +1 -0
  288. package/dist/core/workers/worker.d.ts +1 -0
  289. package/dist/core/workers/worker.js +225 -0
  290. package/dist/core/workers/worker.js.map +1 -0
  291. package/dist/index.d.ts +8 -1
  292. package/dist/index.js +6 -1
  293. package/dist/index.js.map +1 -1
  294. package/dist/moro.d.ts +345 -13
  295. package/dist/moro.js +803 -214
  296. package/dist/moro.js.map +1 -1
  297. package/dist/types/cache.d.ts +4 -0
  298. package/dist/types/config.d.ts +42 -0
  299. package/dist/types/core.d.ts +18 -1
  300. package/package.json +94 -20
@@ -1,8 +1,6 @@
1
1
  // src/core/http-server.ts
2
2
  import { createServer } from 'http';
3
3
  import * as zlib from 'zlib';
4
- import { createReadStream } from 'fs';
5
- import * as crypto from 'crypto';
6
4
  import { promisify } from 'util';
7
5
  import { createFrameworkLogger } from '../logger/index.js';
8
6
  import { PathMatcher } from '../routing/path-matcher.js';
@@ -38,44 +36,6 @@ export class MoroHttpServer {
38
36
  methodNotAllowed: Buffer.from('{"success":false,"error":"Method not allowed"}'),
39
37
  rateLimited: Buffer.from('{"success":false,"error":"Rate limit exceeded"}'),
40
38
  };
41
- // Buffer pool for zero-copy operations
42
- static BUFFER_SIZES = [64, 256, 1024, 4096, 16384];
43
- static BUFFER_POOLS = new Map();
44
- static {
45
- // Pre-allocate buffer pools for zero-allocation responses
46
- for (const size of MoroHttpServer.BUFFER_SIZES) {
47
- MoroHttpServer.BUFFER_POOLS.set(size, []);
48
- for (let i = 0; i < 50; i++) {
49
- // 50 buffers per size
50
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
51
- MoroHttpServer.BUFFER_POOLS.get(size).push(Buffer.allocUnsafe(size));
52
- }
53
- }
54
- }
55
- static getOptimalBuffer(size) {
56
- // Find the smallest buffer that fits
57
- for (const poolSize of MoroHttpServer.BUFFER_SIZES) {
58
- if (size <= poolSize) {
59
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
60
- const pool = MoroHttpServer.BUFFER_POOLS.get(poolSize);
61
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
62
- return pool.length > 0 ? pool.pop() : Buffer.allocUnsafe(poolSize);
63
- }
64
- }
65
- return Buffer.allocUnsafe(size);
66
- }
67
- static returnBuffer(buffer) {
68
- // Return buffer to appropriate pool
69
- const size = buffer.length;
70
- if (MoroHttpServer.BUFFER_POOLS.has(size)) {
71
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
72
- const pool = MoroHttpServer.BUFFER_POOLS.get(size);
73
- if (pool.length < 50) {
74
- // Don't let pools grow too large
75
- pool.push(buffer);
76
- }
77
- }
78
- }
79
39
  constructor() {
80
40
  this.server = createServer(this.handleRequest.bind(this));
81
41
  // Optimize server for high performance (conservative settings for compatibility)
@@ -289,9 +249,9 @@ export class MoroHttpServer {
289
249
  }
290
250
  }
291
251
  finally {
292
- // CRITICAL: Always release pooled objects back to the pool
252
+ // Always release pooled objects back to the pool
293
253
  // This prevents memory leaks and ensures consistent performance
294
- // Optimized: Check if object is empty without Object.keys()
254
+ // Check if object is empty without Object.keys()
295
255
  if (originalParams) {
296
256
  let isEmpty = true;
297
257
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -317,7 +277,7 @@ export class MoroHttpServer {
317
277
  }
318
278
  // Additional cleanup on response completion to ensure objects are returned to pool
319
279
  res.once('finish', () => {
320
- // Optimized: Check if object is empty without Object.keys()
280
+ // Check if object is empty without Object.keys()
321
281
  if (originalParams) {
322
282
  let isEmpty = true;
323
283
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -367,14 +327,40 @@ export class MoroHttpServer {
367
327
  streamLargeResponse(res, data) {
368
328
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
369
329
  res.setHeader('Transfer-Encoding', 'chunked');
370
- // Stream the response in chunks
371
- const jsonString = JSON.stringify(data);
372
- const chunkSize = 8192; // 8KB chunks
373
- for (let i = 0; i < jsonString.length; i += chunkSize) {
374
- const chunk = jsonString.substring(i, i + chunkSize);
375
- res.write(chunk);
330
+ // Stream large JSON responses to prevent memory issues
331
+ if (Array.isArray(data) && data.length > 100) {
332
+ // Stream large arrays element by element
333
+ res.write('[');
334
+ // Stream each array element
335
+ let first = true;
336
+ for (const item of data) {
337
+ if (!first)
338
+ res.write(',');
339
+ res.write(JSON.stringify(item));
340
+ first = false;
341
+ }
342
+ // Write closing bracket and end
343
+ res.end(']');
344
+ }
345
+ else if (typeof data === 'object' && data !== null && Object.keys(data).length > 50) {
346
+ // For large objects, stream key-value pairs
347
+ res.write('{');
348
+ const keys = Object.keys(data);
349
+ let first = true;
350
+ for (const key of keys) {
351
+ if (!first)
352
+ res.write(',');
353
+ // Properly escape the key using JSON.stringify
354
+ res.write(`${JSON.stringify(key)}:${JSON.stringify(data[key])}`);
355
+ first = false;
356
+ }
357
+ res.end('}');
358
+ }
359
+ else {
360
+ // For smaller data, still avoid the old chunking approach
361
+ const jsonString = JSON.stringify(data);
362
+ res.end(jsonString);
376
363
  }
377
- res.end();
378
364
  }
379
365
  normalizePath(path) {
380
366
  // Check cache first
@@ -472,76 +458,21 @@ export class MoroHttpServer {
472
458
  httpRes.json = async (data) => {
473
459
  if (httpRes.headersSent)
474
460
  return;
475
- // JSON serialization with zero-copy buffers
476
- let jsonString;
477
- // Enhanced JSON optimization for common API patterns
478
- // Fast path for common 2-3 key objects without Object.keys() overhead
479
- if (data && typeof data === 'object' && 'success' in data) {
480
- // Check for common patterns using 'in' operator (faster than Object.keys for small objects)
481
- const hasData = 'data' in data;
482
- const hasError = 'error' in data;
483
- const hasTotal = 'total' in data;
484
- // Fast path: {success, data} - most common pattern
485
- if (hasData && !hasError && !hasTotal) {
486
- // Verify it's exactly 2 keys by checking no other common keys exist
487
- if (!('message' in data) && !('code' in data) && !('status' in data)) {
488
- jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)}}`;
489
- }
490
- else {
491
- jsonString = JSON.stringify(data);
492
- }
493
- }
494
- else if (hasError && !hasData && !hasTotal) {
495
- // Fast path: {success, error}
496
- if (!('message' in data) && !('code' in data) && !('status' in data)) {
497
- jsonString = `{"success":${data.success},"error":${JSON.stringify(data.error)}}`;
498
- }
499
- else {
500
- jsonString = JSON.stringify(data);
501
- }
502
- }
503
- else if (hasData && hasError && !hasTotal) {
504
- // Fast path: {success, data, error}
505
- if (!('message' in data) && !('code' in data) && !('status' in data)) {
506
- jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)},"error":${JSON.stringify(data.error)}}`;
507
- }
508
- else {
509
- jsonString = JSON.stringify(data);
510
- }
511
- }
512
- else if (hasData && hasTotal && !hasError) {
513
- // Fast path: {success, data, total}
514
- if (!('message' in data) && !('code' in data) && !('status' in data)) {
515
- jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)},"total":${data.total}}`;
516
- }
517
- else {
518
- jsonString = JSON.stringify(data);
519
- }
520
- }
521
- else {
522
- // Complex object - use standard JSON.stringify
523
- jsonString = JSON.stringify(data);
524
- }
525
- }
526
- else {
527
- jsonString = JSON.stringify(data);
528
- }
529
- // Use buffer pool for zero-allocation responses
530
- const estimatedSize = jsonString.length;
531
- if (estimatedSize > 32768) {
461
+ // Simple, optimized JSON serialization - let V8 handle the optimization
462
+ const jsonString = JSON.stringify(data);
463
+ // Large response check - stream if needed
464
+ if (jsonString.length > 32768) {
532
465
  // Large response - stream it
533
466
  return this.streamLargeResponse(httpRes, data);
534
467
  }
535
- const buffer = MoroHttpServer.getOptimalBuffer(estimatedSize);
536
- const actualLength = buffer.write(jsonString, 0, 'utf8');
537
- // Slice to actual size to avoid sending extra bytes
538
- const finalBuffer = actualLength === buffer.length ? buffer : buffer.subarray(0, actualLength);
468
+ // Use efficient buffer allocation - let Node.js handle optimization
469
+ const finalBuffer = Buffer.from(jsonString, 'utf8');
539
470
  // Optimized header setting - set multiple headers at once when possible
540
471
  const headers = {
541
472
  'Content-Type': 'application/json; charset=utf-8',
542
473
  };
543
474
  // Compression with buffer pool - EARLY EXIT if disabled or below threshold
544
- // CRITICAL: Only make this async if compression is actually happening
475
+ // Only make this async if compression is actually happening
545
476
  if (this.compressionEnabled && finalBuffer.length > this.compressionThreshold) {
546
477
  const acceptEncoding = httpRes.req.headers['accept-encoding'];
547
478
  if (acceptEncoding && acceptEncoding.includes('gzip')) {
@@ -552,8 +483,6 @@ export class MoroHttpServer {
552
483
  // Batch write all headers at once (50-100% faster)
553
484
  httpRes.writeHead(httpRes.statusCode || 200, headers);
554
485
  httpRes.end(compressed);
555
- // Return buffer to pool after response
556
- process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
557
486
  });
558
487
  return;
559
488
  }
@@ -565,8 +494,6 @@ export class MoroHttpServer {
565
494
  // Batch write all headers at once
566
495
  httpRes.writeHead(httpRes.statusCode || 200, headers);
567
496
  httpRes.end(compressed);
568
- // Return buffer to pool after response
569
- process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
570
497
  });
571
498
  return;
572
499
  }
@@ -576,8 +503,6 @@ export class MoroHttpServer {
576
503
  // Batch write all headers at once
577
504
  httpRes.writeHead(httpRes.statusCode || 200, headers);
578
505
  httpRes.end(finalBuffer);
579
- // Return buffer to pool after response (zero-copy achievement!)
580
- process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
581
506
  };
582
507
  httpRes.send = (data) => {
583
508
  if (httpRes.headersSent)
@@ -638,15 +563,31 @@ export class MoroHttpServer {
638
563
  if (options.path)
639
564
  cookieString += `; Path=${options.path}`;
640
565
  const existingCookies = httpRes.getHeader('Set-Cookie') || [];
566
+ // Avoid spread operator - direct array manipulation
641
567
  const cookies = Array.isArray(existingCookies)
642
- ? [...existingCookies]
568
+ ? existingCookies
643
569
  : [existingCookies];
644
570
  cookies.push(cookieString);
645
571
  httpRes.setHeader('Set-Cookie', cookies);
646
572
  return httpRes;
647
573
  };
648
574
  httpRes.clearCookie = (name, options = {}) => {
649
- const clearOptions = { ...options, expires: new Date(0), maxAge: 0 };
575
+ // Avoid spread operator - manually set properties
576
+ const clearOptions = {
577
+ expires: new Date(0),
578
+ maxAge: 0,
579
+ };
580
+ // Copy other options manually
581
+ if (options.path !== undefined)
582
+ clearOptions.path = options.path;
583
+ if (options.domain !== undefined)
584
+ clearOptions.domain = options.domain;
585
+ if (options.httpOnly !== undefined)
586
+ clearOptions.httpOnly = options.httpOnly;
587
+ if (options.secure !== undefined)
588
+ clearOptions.secure = options.secure;
589
+ if (options.sameSite !== undefined)
590
+ clearOptions.sameSite = options.sameSite;
650
591
  return httpRes.cookie(name, '', clearOptions);
651
592
  };
652
593
  httpRes.redirect = (url, status = 302) => {
@@ -688,15 +629,17 @@ export class MoroHttpServer {
688
629
  // Note: removeHeader is inherited from ServerResponse, we don't override it
689
630
  httpRes.setBulkHeaders = (headers) => {
690
631
  if (httpRes.headersSent) {
632
+ // Only enumerate keys for warning if headers were already sent
633
+ const attemptedHeaderKeys = [];
634
+ for (const key in headers) {
635
+ attemptedHeaderKeys.push(key);
636
+ }
691
637
  this.logger.warn('Cannot set headers - headers already sent', 'HeaderWarning', {
692
- attemptedHeaders: Object.keys(headers),
638
+ attemptedHeaders: attemptedHeaderKeys,
693
639
  });
694
640
  return httpRes;
695
641
  }
696
- const headerKeys = Object.keys(headers);
697
- const headerKeysLen = headerKeys.length;
698
- for (let i = 0; i < headerKeysLen; i++) {
699
- const key = headerKeys[i];
642
+ for (const key in headers) {
700
643
  httpRes.setHeader(key, headers[key]);
701
644
  }
702
645
  return httpRes;
@@ -765,10 +708,18 @@ export class MoroHttpServer {
765
708
  return mimeType;
766
709
  }
767
710
  async parseBody(req) {
711
+ const contentType = req.headers['content-type'] || '';
712
+ const contentLength = parseInt(req.headers['content-length'] || '0');
713
+ const maxSize = 10 * 1024 * 1024; // 10MB limit
714
+ // For very large payloads, return a streaming interface instead of buffering
715
+ if (contentLength > maxSize / 2) {
716
+ // Stream for payloads > 5MB
717
+ return this.createStreamingBodyParser(req, contentType, maxSize);
718
+ }
719
+ // Standard buffered parsing for smaller payloads
768
720
  return new Promise((resolve, reject) => {
769
721
  const chunks = [];
770
722
  let totalLength = 0;
771
- const maxSize = 10 * 1024 * 1024; // 10MB limit
772
723
  req.on('data', (chunk) => {
773
724
  totalLength += chunk.length;
774
725
  if (totalLength > maxSize) {
@@ -780,7 +731,6 @@ export class MoroHttpServer {
780
731
  req.on('end', () => {
781
732
  try {
782
733
  const body = Buffer.concat(chunks);
783
- const contentType = req.headers['content-type'] || '';
784
734
  if (contentType.includes('application/json')) {
785
735
  resolve(JSON.parse(body.toString()));
786
736
  }
@@ -801,6 +751,132 @@ export class MoroHttpServer {
801
751
  req.on('error', reject);
802
752
  });
803
753
  }
754
+ /**
755
+ * Create a streaming body parser for large payloads
756
+ * Returns a streaming interface instead of buffering
757
+ */
758
+ createStreamingBodyParser(req, contentType, maxSize) {
759
+ let totalLength = 0;
760
+ const chunks = [];
761
+ return new Promise((resolve, reject) => {
762
+ const streamParser = {
763
+ // Streaming JSON parser for large JSON payloads
764
+ json: () => this.streamJsonParse(req, maxSize),
765
+ // Streaming form data parser
766
+ form: () => this.streamFormParse(req, maxSize),
767
+ // Raw stream access
768
+ stream: () => ({
769
+ onData: (callback) => {
770
+ req.on('data', (chunk) => {
771
+ totalLength += chunk.length;
772
+ if (totalLength > maxSize) {
773
+ reject(new Error('Request body too large'));
774
+ return;
775
+ }
776
+ callback(chunk);
777
+ });
778
+ },
779
+ onEnd: (callback) => {
780
+ req.on('end', callback);
781
+ },
782
+ onError: (callback) => {
783
+ req.on('error', callback);
784
+ },
785
+ }),
786
+ // Traditional buffered parsing (fallback)
787
+ buffer: async () => {
788
+ return new Promise((resolveBuffer, rejectBuffer) => {
789
+ req.on('data', (chunk) => {
790
+ totalLength += chunk.length;
791
+ if (totalLength > maxSize) {
792
+ rejectBuffer(new Error('Request body too large'));
793
+ return;
794
+ }
795
+ chunks.push(chunk);
796
+ });
797
+ req.on('end', () => {
798
+ try {
799
+ const body = Buffer.concat(chunks);
800
+ if (contentType.includes('application/json')) {
801
+ resolveBuffer(JSON.parse(body.toString()));
802
+ }
803
+ else {
804
+ resolveBuffer(body.toString());
805
+ }
806
+ }
807
+ catch (error) {
808
+ rejectBuffer(error);
809
+ }
810
+ });
811
+ req.on('error', rejectBuffer);
812
+ });
813
+ },
814
+ };
815
+ // Auto-detect and return appropriate parser
816
+ if (contentType.includes('application/json')) {
817
+ resolve({ type: 'json', parser: streamParser.json });
818
+ }
819
+ else if (contentType.includes('application/x-www-form-urlencoded')) {
820
+ resolve({ type: 'form', parser: streamParser.form });
821
+ }
822
+ else {
823
+ resolve({ type: 'stream', parser: streamParser.stream });
824
+ }
825
+ });
826
+ }
827
+ /**
828
+ * Streaming JSON parser for large payloads
829
+ */
830
+ async streamJsonParse(req, maxSize) {
831
+ return new Promise((resolve, reject) => {
832
+ let jsonString = '';
833
+ let totalLength = 0;
834
+ req.on('data', (chunk) => {
835
+ totalLength += chunk.length;
836
+ if (totalLength > maxSize) {
837
+ reject(new Error('Request body too large'));
838
+ return;
839
+ }
840
+ jsonString += chunk.toString();
841
+ });
842
+ req.on('end', () => {
843
+ try {
844
+ // For very large JSON, consider streaming JSON parsing in the future
845
+ resolve(JSON.parse(jsonString));
846
+ }
847
+ catch (error) {
848
+ reject(error);
849
+ }
850
+ });
851
+ req.on('error', reject);
852
+ });
853
+ }
854
+ /**
855
+ * Streaming form data parser
856
+ */
857
+ async streamFormParse(req, maxSize) {
858
+ return new Promise((resolve, reject) => {
859
+ let formData = '';
860
+ let totalLength = 0;
861
+ req.on('data', (chunk) => {
862
+ totalLength += chunk.length;
863
+ if (totalLength > maxSize) {
864
+ reject(new Error('Request body too large'));
865
+ return;
866
+ }
867
+ formData += chunk.toString();
868
+ });
869
+ req.on('end', () => {
870
+ try {
871
+ resolve(this.parseUrlEncoded(formData));
872
+ }
873
+ catch (error) {
874
+ reject(error);
875
+ }
876
+ });
877
+ req.on('error', reject);
878
+ });
879
+ }
804
880
  parseMultipart(buffer, contentType) {
805
881
  const boundary = contentType.split('boundary=')[1];
806
882
  if (!boundary) {
@@ -898,7 +974,7 @@ export class MoroHttpServer {
898
974
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
899
975
  return this.routeCache.get(normalizedCacheKey);
900
976
  }
901
- // Phase 1: O(1) static route lookup
977
+ // O(1) static route lookup
902
978
  const staticRoute = this.staticRoutes.get(normalizedCacheKey);
903
979
  if (staticRoute) {
904
980
  this.routeCache.set(normalizedCacheKey, staticRoute);
@@ -907,7 +983,7 @@ export class MoroHttpServer {
907
983
  }
908
984
  return staticRoute;
909
985
  }
910
- // Phase 2: Optimized dynamic route matching by segment count
986
+ // Dynamic route matching by segment count
911
987
  let route = null;
912
988
  const dynamicRoutesLen = this.dynamicRoutes.length;
913
989
  if (dynamicRoutesLen > 0) {
@@ -1011,667 +1087,4 @@ export class MoroHttpServer {
1011
1087
  };
1012
1088
  }
1013
1089
  }
1014
- // Built-in middleware
1015
- export const middleware = {
1016
- cors: (options = {}) => {
1017
- return (req, res, next) => {
1018
- res.setHeader('Access-Control-Allow-Origin', options.origin || '*');
1019
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
1020
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
1021
- if (options.credentials) {
1022
- res.setHeader('Access-Control-Allow-Credentials', 'true');
1023
- }
1024
- if (req.method === 'OPTIONS') {
1025
- res.status(200).send('');
1026
- return;
1027
- }
1028
- next();
1029
- };
1030
- },
1031
- helmet: () => {
1032
- return (req, res, next) => {
1033
- res.setHeader('X-Content-Type-Options', 'nosniff');
1034
- res.setHeader('X-Frame-Options', 'DENY');
1035
- res.setHeader('X-XSS-Protection', '1; mode=block');
1036
- res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
1037
- res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
1038
- res.setHeader('Content-Security-Policy', "default-src 'self'");
1039
- next();
1040
- };
1041
- },
1042
- compression: (options = {}) => {
1043
- const threshold = options.threshold || 1024;
1044
- const level = options.level || 6;
1045
- return (req, res, next) => {
1046
- const acceptEncoding = req.headers['accept-encoding'] || '';
1047
- // Override res.json to compress responses
1048
- const originalJson = res.json;
1049
- const originalSend = res.send;
1050
- const compressResponse = (data, isJson = false) => {
1051
- const content = isJson ? JSON.stringify(data) : data;
1052
- const buffer = Buffer.from(content);
1053
- if (buffer.length < threshold) {
1054
- return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
1055
- }
1056
- if (acceptEncoding.includes('gzip')) {
1057
- zlib.gzip(buffer, { level }, (err, compressed) => {
1058
- if (err) {
1059
- return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
1060
- }
1061
- if (!res.headersSent) {
1062
- res.setHeader('Content-Encoding', 'gzip');
1063
- res.setHeader('Content-Length', compressed.length);
1064
- }
1065
- res.end(compressed);
1066
- });
1067
- }
1068
- else if (acceptEncoding.includes('deflate')) {
1069
- zlib.deflate(buffer, { level }, (err, compressed) => {
1070
- if (err) {
1071
- return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
1072
- }
1073
- if (!res.headersSent) {
1074
- res.setHeader('Content-Encoding', 'deflate');
1075
- res.setHeader('Content-Length', compressed.length);
1076
- }
1077
- res.end(compressed);
1078
- });
1079
- }
1080
- else {
1081
- return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
1082
- }
1083
- };
1084
- res.json = function (data) {
1085
- // Ensure charset is set for Safari compatibility
1086
- this.setHeader('Content-Type', 'application/json; charset=utf-8');
1087
- compressResponse(data, true);
1088
- return this;
1089
- };
1090
- res.send = function (data) {
1091
- compressResponse(data, false);
1092
- return this;
1093
- };
1094
- next();
1095
- };
1096
- },
1097
- requestLogger: () => {
1098
- return (req, res, next) => {
1099
- const start = Date.now();
1100
- res.on('finish', () => {
1101
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1102
- const duration = Date.now() - start;
1103
- // Request completed - logged by framework
1104
- });
1105
- next();
1106
- };
1107
- },
1108
- bodySize: (options = {}) => {
1109
- const limit = options.limit || '10mb';
1110
- const limitBytes = parseSize(limit);
1111
- return (req, res, next) => {
1112
- const contentLength = parseInt(req.headers['content-length'] || '0');
1113
- if (contentLength > limitBytes) {
1114
- res.status(413).json({
1115
- success: false,
1116
- error: 'Request entity too large',
1117
- limit: limit,
1118
- });
1119
- return;
1120
- }
1121
- next();
1122
- };
1123
- },
1124
- static: (options) => {
1125
- return async (req, res, next) => {
1126
- // Only handle GET and HEAD requests
1127
- if (req.method !== 'GET' && req.method !== 'HEAD') {
1128
- next();
1129
- return;
1130
- }
1131
- try {
1132
- const fs = await import('fs/promises');
1133
- const path = await import('path');
1134
- const crypto = await import('crypto');
1135
- let filePath = path.join(options.root, req.path);
1136
- // Security: prevent directory traversal
1137
- if (!filePath.startsWith(path.resolve(options.root))) {
1138
- res.status(403).json({ success: false, error: 'Forbidden' });
1139
- return;
1140
- }
1141
- // Handle dotfiles
1142
- const basename = path.basename(filePath);
1143
- if (basename.startsWith('.')) {
1144
- if (options.dotfiles === 'deny') {
1145
- res.status(403).json({ success: false, error: 'Forbidden' });
1146
- return;
1147
- }
1148
- else if (options.dotfiles === 'ignore') {
1149
- next();
1150
- return;
1151
- }
1152
- }
1153
- let stats;
1154
- try {
1155
- stats = await fs.stat(filePath);
1156
- }
1157
- catch {
1158
- next(); // File not found, let other middleware handle
1159
- return;
1160
- }
1161
- // Handle directories
1162
- if (stats.isDirectory()) {
1163
- const indexFiles = options.index || ['index.html', 'index.htm'];
1164
- let indexFound = false;
1165
- for (const indexFile of indexFiles) {
1166
- const indexPath = path.join(filePath, indexFile);
1167
- try {
1168
- const indexStats = await fs.stat(indexPath);
1169
- if (indexStats.isFile()) {
1170
- filePath = indexPath;
1171
- stats = indexStats;
1172
- indexFound = true;
1173
- break;
1174
- }
1175
- }
1176
- catch {
1177
- // Continue to next index file
1178
- }
1179
- }
1180
- if (!indexFound) {
1181
- next();
1182
- return;
1183
- }
1184
- }
1185
- // Set headers with proper mime type and charset
1186
- const ext = path.extname(filePath);
1187
- const mimeTypes = {
1188
- '.html': 'text/html',
1189
- '.css': 'text/css',
1190
- '.js': 'application/javascript',
1191
- '.json': 'application/json',
1192
- '.png': 'image/png',
1193
- '.jpg': 'image/jpeg',
1194
- '.jpeg': 'image/jpeg',
1195
- '.gif': 'image/gif',
1196
- '.svg': 'image/svg+xml',
1197
- '.ico': 'image/x-icon',
1198
- '.pdf': 'application/pdf',
1199
- '.txt': 'text/plain',
1200
- '.xml': 'application/xml',
1201
- };
1202
- const baseMimeType = mimeTypes[ext.toLowerCase()] || 'application/octet-stream';
1203
- // Add charset for text-based files
1204
- const textTypes = [
1205
- 'text/',
1206
- 'application/json',
1207
- 'application/javascript',
1208
- 'application/xml',
1209
- 'image/svg+xml',
1210
- ];
1211
- const needsCharset = textTypes.some(type => baseMimeType.startsWith(type));
1212
- const contentType = needsCharset ? `${baseMimeType}; charset=utf-8` : baseMimeType;
1213
- res.setHeader('Content-Type', contentType);
1214
- res.setHeader('Content-Length', stats.size);
1215
- // Cache headers
1216
- if (options.maxAge) {
1217
- res.setHeader('Cache-Control', `public, max-age=${options.maxAge}`);
1218
- }
1219
- // ETag support
1220
- if (options.etag !== false) {
1221
- const etag = crypto
1222
- .createHash('md5')
1223
- .update(`${stats.mtime.getTime()}-${stats.size}`)
1224
- .digest('hex');
1225
- res.setHeader('ETag', `"${etag}"`);
1226
- // Handle conditional requests
1227
- const ifNoneMatch = req.headers['if-none-match'];
1228
- if (ifNoneMatch === `"${etag}"`) {
1229
- res.statusCode = 304;
1230
- res.end();
1231
- return;
1232
- }
1233
- }
1234
- // Handle HEAD requests
1235
- if (req.method === 'HEAD') {
1236
- res.end();
1237
- return;
1238
- }
1239
- // Send file
1240
- const data = await fs.readFile(filePath);
1241
- res.end(data);
1242
- }
1243
- catch {
1244
- res.status(500).json({ success: false, error: 'Internal server error' });
1245
- }
1246
- };
1247
- },
1248
- upload: (options = {}) => {
1249
- return (req, res, next) => {
1250
- const contentType = req.headers['content-type'] || '';
1251
- if (!contentType.includes('multipart/form-data')) {
1252
- next();
1253
- return;
1254
- }
1255
- // File upload handling is now built into parseBody method
1256
- // This middleware can add additional validation
1257
- if (req.body && req.body.files) {
1258
- const files = req.body.files;
1259
- const maxFileSize = options.maxFileSize || 5 * 1024 * 1024; // 5MB default
1260
- const maxFiles = options.maxFiles || 10;
1261
- const allowedTypes = options.allowedTypes;
1262
- // Validate file count
1263
- if (Object.keys(files).length > maxFiles) {
1264
- res.status(400).json({
1265
- success: false,
1266
- error: `Too many files. Maximum ${maxFiles} allowed.`,
1267
- });
1268
- return;
1269
- }
1270
- // Validate each file
1271
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1272
- for (const [fieldName, file] of Object.entries(files)) {
1273
- const fileData = file;
1274
- // Validate file size
1275
- if (fileData.size > maxFileSize) {
1276
- res.status(400).json({
1277
- success: false,
1278
- error: `File ${fileData.filename} is too large. Maximum ${maxFileSize} bytes allowed.`,
1279
- });
1280
- return;
1281
- }
1282
- // Validate file type
1283
- if (allowedTypes && !allowedTypes.includes(fileData.mimetype)) {
1284
- res.status(400).json({
1285
- success: false,
1286
- error: `File type ${fileData.mimetype} not allowed.`,
1287
- });
1288
- return;
1289
- }
1290
- }
1291
- // Store files in request for easy access
1292
- req.files = files;
1293
- }
1294
- next();
1295
- };
1296
- },
1297
- template: (options) => {
1298
- const templateCache = new Map();
1299
- return async (req, res, next) => {
1300
- // Add render method to response
1301
- res.render = async (template, data = {}) => {
1302
- try {
1303
- const fs = await import('fs/promises');
1304
- const path = await import('path');
1305
- const templatePath = path.join(options.views, `${template}.html`);
1306
- let templateContent;
1307
- // Check cache first
1308
- if (options.cache && templateCache.has(templatePath)) {
1309
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1310
- templateContent = templateCache.get(templatePath);
1311
- }
1312
- else {
1313
- templateContent = await fs.readFile(templatePath, 'utf-8');
1314
- if (options.cache) {
1315
- templateCache.set(templatePath, templateContent);
1316
- }
1317
- }
1318
- // Simple template engine - replace {{variable}} with values
1319
- let rendered = templateContent;
1320
- // Handle basic variable substitution
1321
- rendered = rendered.replace(/\{\{(\w+)\}\}/g, (match, key) => {
1322
- return data[key] !== undefined ? String(data[key]) : match;
1323
- });
1324
- // Handle nested object properties like {{user.name}}
1325
- rendered = rendered.replace(/\{\{([\w.]+)\}\}/g, (match, key) => {
1326
- const value = key.split('.').reduce((obj, prop) => obj?.[prop], data);
1327
- return value !== undefined ? String(value) : match;
1328
- });
1329
- // Handle loops: {{#each items}}{{name}}{{/each}}
1330
- rendered = rendered.replace(/\{\{#each (\w+)\}\}(.*?)\{\{\/each\}\}/gs, (match, arrayKey, template) => {
1331
- const array = data[arrayKey];
1332
- if (!Array.isArray(array))
1333
- return '';
1334
- return array
1335
- .map(item => {
1336
- let itemTemplate = template;
1337
- // Replace variables in the loop template
1338
- itemTemplate = itemTemplate.replace(/\{\{(\w+)\}\}/g, (match, key) => {
1339
- return item[key] !== undefined ? String(item[key]) : match;
1340
- });
1341
- return itemTemplate;
1342
- })
1343
- .join('');
1344
- });
1345
- // Handle conditionals: {{#if condition}}content{{/if}}
1346
- rendered = rendered.replace(/\{\{#if (\w+)\}\}(.*?)\{\{\/if\}\}/gs, (match, conditionKey, content) => {
1347
- const condition = data[conditionKey];
1348
- return condition ? content : '';
1349
- });
1350
- // Handle layout
1351
- if (options.defaultLayout) {
1352
- const layoutPath = path.join(options.views, 'layouts', `${options.defaultLayout}.html`);
1353
- try {
1354
- let layoutContent;
1355
- if (options.cache && templateCache.has(layoutPath)) {
1356
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1357
- layoutContent = templateCache.get(layoutPath);
1358
- }
1359
- else {
1360
- layoutContent = await fs.readFile(layoutPath, 'utf-8');
1361
- if (options.cache) {
1362
- templateCache.set(layoutPath, layoutContent);
1363
- }
1364
- }
1365
- rendered = layoutContent.replace(/\{\{body\}\}/, rendered);
1366
- }
1367
- catch {
1368
- // Layout not found, use template as-is
1369
- }
1370
- }
1371
- res.setHeader('Content-Type', 'text/html');
1372
- res.end(rendered);
1373
- }
1374
- catch {
1375
- res.status(500).json({ success: false, error: 'Template rendering failed' });
1376
- }
1377
- };
1378
- next();
1379
- };
1380
- },
1381
- // HTTP/2 Server Push middleware
1382
- http2Push: (options = {}) => {
1383
- return (req, res, next) => {
1384
- // Add HTTP/2 push capability to response
1385
- res.push = (path, options = {}) => {
1386
- // Check if HTTP/2 is supported
1387
- if (req.httpVersion === '2.0' && res.stream && res.stream.pushAllowed) {
1388
- try {
1389
- const pushStream = res.stream.pushStream({
1390
- ':method': 'GET',
1391
- ':path': path,
1392
- ...options.headers,
1393
- });
1394
- if (pushStream) {
1395
- // Handle push stream
1396
- return pushStream;
1397
- }
1398
- }
1399
- catch {
1400
- // Push failed, continue normally
1401
- }
1402
- }
1403
- return null;
1404
- };
1405
- // Auto-push configured resources
1406
- if (options.resources && (!options.condition || options.condition(req))) {
1407
- for (const resource of options.resources) {
1408
- res.push?.(resource.path, {
1409
- headers: {
1410
- 'content-type': resource.type || 'text/plain',
1411
- },
1412
- });
1413
- }
1414
- }
1415
- next();
1416
- };
1417
- },
1418
- // Server-Sent Events middleware
1419
- sse: (options = {}) => {
1420
- return (req, res, next) => {
1421
- // Only handle SSE requests
1422
- if (req.headers.accept?.includes('text/event-stream')) {
1423
- // Set SSE headers
1424
- if (!res.headersSent) {
1425
- res.writeHead(200, {
1426
- 'Content-Type': 'text/event-stream',
1427
- 'Cache-Control': 'no-cache',
1428
- Connection: 'keep-alive',
1429
- 'Access-Control-Allow-Origin': options.cors ? '*' : undefined,
1430
- 'Access-Control-Allow-Headers': options.cors ? 'Cache-Control' : undefined,
1431
- });
1432
- }
1433
- // Add SSE methods to response
1434
- res.sendEvent = (data, event, id) => {
1435
- if (id)
1436
- res.write(`id: ${id}\n`);
1437
- if (event)
1438
- res.write(`event: ${event}\n`);
1439
- res.write(`data: ${typeof data === 'string' ? data : JSON.stringify(data)}\n\n`);
1440
- };
1441
- res.sendComment = (comment) => {
1442
- res.write(`: ${comment}\n\n`);
1443
- };
1444
- res.sendRetry = (ms) => {
1445
- res.write(`retry: ${ms}\n\n`);
1446
- };
1447
- // Set up heartbeat if configured
1448
- let heartbeatInterval = null;
1449
- if (options.heartbeat) {
1450
- heartbeatInterval = setInterval(() => {
1451
- res.sendComment('heartbeat');
1452
- }, options.heartbeat);
1453
- }
1454
- // Set retry if configured
1455
- if (options.retry) {
1456
- res.sendRetry(options.retry);
1457
- }
1458
- // Clean up on close
1459
- req.on('close', () => {
1460
- if (heartbeatInterval) {
1461
- clearInterval(heartbeatInterval);
1462
- }
1463
- });
1464
- // Don't call next() - this middleware handles the response
1465
- return;
1466
- }
1467
- next();
1468
- };
1469
- },
1470
- // Range request middleware for streaming
1471
- range: (options = {}) => {
1472
- return async (req, res, next) => {
1473
- // Add range support to response
1474
- res.sendRange = async (filePath, stats) => {
1475
- try {
1476
- const fs = await import('fs/promises');
1477
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1478
- const _path = await import('path');
1479
- if (!stats) {
1480
- stats = await fs.stat(filePath);
1481
- }
1482
- const fileSize = stats.size;
1483
- const range = req.headers.range;
1484
- // Set Accept-Ranges header
1485
- res.setHeader('Accept-Ranges', options.acceptRanges || 'bytes');
1486
- if (!range) {
1487
- // No range requested, send entire file
1488
- res.setHeader('Content-Length', fileSize);
1489
- const data = await fs.readFile(filePath);
1490
- res.end(data);
1491
- return;
1492
- }
1493
- // Parse range header
1494
- const ranges = range
1495
- .replace(/bytes=/, '')
1496
- .split(',')
1497
- .map(r => {
1498
- const [start, end] = r.split('-');
1499
- return {
1500
- start: start ? parseInt(start) : 0,
1501
- end: end ? parseInt(end) : fileSize - 1,
1502
- };
1503
- });
1504
- // Validate ranges
1505
- if (options.maxRanges && ranges.length > options.maxRanges) {
1506
- res.status(416).json({ success: false, error: 'Too many ranges' });
1507
- return;
1508
- }
1509
- if (ranges.length === 1) {
1510
- // Single range
1511
- const { start, end } = ranges[0];
1512
- const chunkSize = end - start + 1;
1513
- if (start >= fileSize || end >= fileSize) {
1514
- res.status(416);
1515
- res.setHeader('Content-Range', `bytes */${fileSize}`);
1516
- res.json({ success: false, error: 'Range not satisfiable' });
1517
- return;
1518
- }
1519
- res.status(206);
1520
- res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
1521
- res.setHeader('Content-Length', chunkSize);
1522
- // Stream the range
1523
- const stream = createReadStream(filePath, {
1524
- start,
1525
- end,
1526
- });
1527
- stream.pipe(res);
1528
- }
1529
- else {
1530
- // Multiple ranges - multipart response
1531
- const boundary = 'MULTIPART_BYTERANGES';
1532
- res.status(206);
1533
- res.setHeader('Content-Type', `multipart/byteranges; boundary=${boundary}`);
1534
- for (const { start, end } of ranges) {
1535
- if (start >= fileSize || end >= fileSize)
1536
- continue;
1537
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1538
- const chunkSize = end - start + 1;
1539
- res.write(`\r\n--${boundary}\r\n`);
1540
- res.write(`Content-Range: bytes ${start}-${end}/${fileSize}\r\n\r\n`);
1541
- const stream = createReadStream(filePath, {
1542
- start,
1543
- end,
1544
- });
1545
- await new Promise(resolve => {
1546
- stream.on('end', () => resolve());
1547
- stream.pipe(res, { end: false });
1548
- });
1549
- }
1550
- res.write(`\r\n--${boundary}--\r\n`);
1551
- res.end();
1552
- }
1553
- }
1554
- catch {
1555
- res.status(500).json({ success: false, error: 'Range request failed' });
1556
- }
1557
- };
1558
- next();
1559
- };
1560
- },
1561
- // CSRF Protection middleware
1562
- csrf: (options = {}) => {
1563
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1564
- const secret = options.secret || 'moro-csrf-secret';
1565
- const tokenLength = options.tokenLength || 32;
1566
- const cookieName = options.cookieName || '_csrf';
1567
- const headerName = options.headerName || 'x-csrf-token';
1568
- const ignoreMethods = options.ignoreMethods || ['GET', 'HEAD', 'OPTIONS'];
1569
- const generateToken = () => {
1570
- return crypto.randomBytes(tokenLength).toString('hex');
1571
- };
1572
- const verifyToken = (token, sessionToken) => {
1573
- return token && sessionToken && token === sessionToken;
1574
- };
1575
- return (req, res, next) => {
1576
- // Add CSRF token generation method
1577
- req.csrfToken = () => {
1578
- if (!req._csrfToken) {
1579
- req._csrfToken = generateToken();
1580
- // Set token in cookie
1581
- res.cookie(cookieName, req._csrfToken, {
1582
- httpOnly: true,
1583
- sameSite: options.sameSite !== false ? 'strict' : undefined,
1584
- secure: req.headers['x-forwarded-proto'] === 'https' || req.socket.encrypted,
1585
- });
1586
- }
1587
- return req._csrfToken;
1588
- };
1589
- // Skip verification for safe methods
1590
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1591
- if (ignoreMethods.includes(req.method)) {
1592
- next();
1593
- return;
1594
- }
1595
- // Get token from header or body
1596
- const token = req.headers[headerName] || (req.body && req.body._csrf) || (req.query && req.query._csrf);
1597
- // Get session token from cookie
1598
- const sessionToken = req.cookies?.[cookieName];
1599
- if (!verifyToken(token, sessionToken || '')) {
1600
- res.status(403).json({
1601
- success: false,
1602
- error: 'Invalid CSRF token',
1603
- code: 'CSRF_TOKEN_MISMATCH',
1604
- });
1605
- return;
1606
- }
1607
- next();
1608
- };
1609
- },
1610
- // Content Security Policy middleware
1611
- csp: (options = {}) => {
1612
- return (req, res, next) => {
1613
- const directives = options.directives || {
1614
- defaultSrc: ["'self'"],
1615
- scriptSrc: ["'self'"],
1616
- styleSrc: ["'self'", "'unsafe-inline'"],
1617
- imgSrc: ["'self'", 'data:', 'https:'],
1618
- connectSrc: ["'self'"],
1619
- fontSrc: ["'self'"],
1620
- objectSrc: ["'none'"],
1621
- mediaSrc: ["'self'"],
1622
- frameSrc: ["'none'"],
1623
- };
1624
- // Generate nonce if requested
1625
- let nonce;
1626
- if (options.nonce) {
1627
- nonce = crypto.randomBytes(16).toString('base64');
1628
- req.cspNonce = nonce;
1629
- }
1630
- // Build CSP header value
1631
- const cspParts = [];
1632
- for (const [directive, sources] of Object.entries(directives)) {
1633
- if (directive === 'upgradeInsecureRequests' && sources === true) {
1634
- cspParts.push('upgrade-insecure-requests');
1635
- }
1636
- else if (directive === 'blockAllMixedContent' && sources === true) {
1637
- cspParts.push('block-all-mixed-content');
1638
- }
1639
- else if (Array.isArray(sources)) {
1640
- let sourceList = sources.join(' ');
1641
- // Add nonce to script-src and style-src if enabled
1642
- if (nonce && (directive === 'scriptSrc' || directive === 'styleSrc')) {
1643
- sourceList += ` 'nonce-${nonce}'`;
1644
- }
1645
- // Convert camelCase to kebab-case
1646
- const kebabDirective = directive.replace(/([A-Z])/g, '-$1').toLowerCase();
1647
- cspParts.push(`${kebabDirective} ${sourceList}`);
1648
- }
1649
- }
1650
- // Add report-uri if specified
1651
- if (options.reportUri) {
1652
- cspParts.push(`report-uri ${options.reportUri}`);
1653
- }
1654
- const cspValue = cspParts.join('; ');
1655
- const headerName = options.reportOnly
1656
- ? 'Content-Security-Policy-Report-Only'
1657
- : 'Content-Security-Policy';
1658
- res.setHeader(headerName, cspValue);
1659
- next();
1660
- };
1661
- },
1662
- };
1663
- function parseSize(size) {
1664
- const units = {
1665
- b: 1,
1666
- kb: 1024,
1667
- mb: 1024 * 1024,
1668
- gb: 1024 * 1024 * 1024,
1669
- };
1670
- const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
1671
- if (!match)
1672
- return 1024 * 1024; // Default 1MB
1673
- const value = parseFloat(match[1]);
1674
- const unit = match[2] || 'b';
1675
- return Math.round(value * units[unit]);
1676
- }
1677
1090
  //# sourceMappingURL=http-server.js.map