imposters 0.1.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 (426) hide show
  1. package/LICENSE +21 -0
  2. package/Program/package.json +6 -0
  3. package/README.md +365 -0
  4. package/api/AdminApi/package.json +6 -0
  5. package/api/ApiErrors/package.json +6 -0
  6. package/api/ApiSchemas/package.json +6 -0
  7. package/api/Conversions/package.json +6 -0
  8. package/api/ImpostersGroup/package.json +6 -0
  9. package/api/ImpostersHandlers/package.json +6 -0
  10. package/api/SystemGroup/package.json +6 -0
  11. package/api/SystemHandlers/package.json +6 -0
  12. package/bin/imposters +47 -0
  13. package/cli/Commands/package.json +6 -0
  14. package/cli/ConfigLoader/package.json +6 -0
  15. package/client/HandlerHttpClient/package.json +6 -0
  16. package/client/ImpostersClient/package.json +6 -0
  17. package/client/testing/package.json +6 -0
  18. package/dist/cjs/Program.js +4 -0
  19. package/dist/cjs/Program.js.map +1 -0
  20. package/dist/cjs/api/AdminApi.js +11 -0
  21. package/dist/cjs/api/AdminApi.js.map +1 -0
  22. package/dist/cjs/api/ApiErrors.js +30 -0
  23. package/dist/cjs/api/ApiErrors.js.map +1 -0
  24. package/dist/cjs/api/ApiSchemas.js +36 -0
  25. package/dist/cjs/api/ApiSchemas.js.map +1 -0
  26. package/dist/cjs/api/Conversions.js +41 -0
  27. package/dist/cjs/api/Conversions.js.map +1 -0
  28. package/dist/cjs/api/ImpostersGroup.js +37 -0
  29. package/dist/cjs/api/ImpostersGroup.js.map +1 -0
  30. package/dist/cjs/api/ImpostersHandlers.js +361 -0
  31. package/dist/cjs/api/ImpostersHandlers.js.map +1 -0
  32. package/dist/cjs/api/SystemGroup.js +12 -0
  33. package/dist/cjs/api/SystemGroup.js.map +1 -0
  34. package/dist/cjs/api/SystemHandlers.js +74 -0
  35. package/dist/cjs/api/SystemHandlers.js.map +1 -0
  36. package/dist/cjs/cli/Commands.js +104 -0
  37. package/dist/cjs/cli/Commands.js.map +1 -0
  38. package/dist/cjs/cli/ConfigLoader.js +34 -0
  39. package/dist/cjs/cli/ConfigLoader.js.map +1 -0
  40. package/dist/cjs/client/HandlerHttpClient.js +50 -0
  41. package/dist/cjs/client/HandlerHttpClient.js.map +1 -0
  42. package/dist/cjs/client/ImpostersClient.js +20 -0
  43. package/dist/cjs/client/ImpostersClient.js.map +1 -0
  44. package/dist/cjs/client/index.js +57 -0
  45. package/dist/cjs/client/index.js.map +1 -0
  46. package/dist/cjs/client/testing.js +94 -0
  47. package/dist/cjs/client/testing.js.map +1 -0
  48. package/dist/cjs/domain/imposter.js +125 -0
  49. package/dist/cjs/domain/imposter.js.map +1 -0
  50. package/dist/cjs/domain/route.js +185 -0
  51. package/dist/cjs/domain/route.js.map +1 -0
  52. package/dist/cjs/index.js +106 -0
  53. package/dist/cjs/index.js.map +1 -0
  54. package/dist/cjs/layers/ApiLayer.js +18 -0
  55. package/dist/cjs/layers/ApiLayer.js.map +1 -0
  56. package/dist/cjs/layers/MainLayer.js +27 -0
  57. package/dist/cjs/layers/MainLayer.js.map +1 -0
  58. package/dist/cjs/matching/ExpressionEvaluator.js +103 -0
  59. package/dist/cjs/matching/ExpressionEvaluator.js.map +1 -0
  60. package/dist/cjs/matching/RequestMatcher.js +145 -0
  61. package/dist/cjs/matching/RequestMatcher.js.map +1 -0
  62. package/dist/cjs/matching/ResponseGenerator.js +80 -0
  63. package/dist/cjs/matching/ResponseGenerator.js.map +1 -0
  64. package/dist/cjs/matching/TemplateEngine.js +55 -0
  65. package/dist/cjs/matching/TemplateEngine.js.map +1 -0
  66. package/dist/cjs/repositories/ImposterRepository.js +118 -0
  67. package/dist/cjs/repositories/ImposterRepository.js.map +1 -0
  68. package/dist/cjs/schemas/ConfigFileSchema.js +44 -0
  69. package/dist/cjs/schemas/ConfigFileSchema.js.map +1 -0
  70. package/dist/cjs/schemas/ImposterSchema.js +202 -0
  71. package/dist/cjs/schemas/ImposterSchema.js.map +1 -0
  72. package/dist/cjs/schemas/RequestLogSchema.js +51 -0
  73. package/dist/cjs/schemas/RequestLogSchema.js.map +1 -0
  74. package/dist/cjs/schemas/StubSchema.js +84 -0
  75. package/dist/cjs/schemas/StubSchema.js.map +1 -0
  76. package/dist/cjs/schemas/common.js +67 -0
  77. package/dist/cjs/schemas/common.js.map +1 -0
  78. package/dist/cjs/server/AdminServer.js +36 -0
  79. package/dist/cjs/server/AdminServer.js.map +1 -0
  80. package/dist/cjs/server/BunServer.js +13 -0
  81. package/dist/cjs/server/BunServer.js.map +1 -0
  82. package/dist/cjs/server/FiberManager.js +21 -0
  83. package/dist/cjs/server/FiberManager.js.map +1 -0
  84. package/dist/cjs/server/ImposterServer.js +234 -0
  85. package/dist/cjs/server/ImposterServer.js.map +1 -0
  86. package/dist/cjs/services/AppConfig.js +18 -0
  87. package/dist/cjs/services/AppConfig.js.map +1 -0
  88. package/dist/cjs/services/MetricsService.js +113 -0
  89. package/dist/cjs/services/MetricsService.js.map +1 -0
  90. package/dist/cjs/services/PortAllocator.js +50 -0
  91. package/dist/cjs/services/PortAllocator.js.map +1 -0
  92. package/dist/cjs/services/ProxyService.js +109 -0
  93. package/dist/cjs/services/ProxyService.js.map +1 -0
  94. package/dist/cjs/services/RequestLogger.js +60 -0
  95. package/dist/cjs/services/RequestLogger.js.map +1 -0
  96. package/dist/cjs/services/Uuid.js +10 -0
  97. package/dist/cjs/services/Uuid.js.map +1 -0
  98. package/dist/cjs/services/UuidLive.js +16 -0
  99. package/dist/cjs/services/UuidLive.js.map +1 -0
  100. package/dist/cjs/ui/UiRouter.js +242 -0
  101. package/dist/cjs/ui/UiRouter.js.map +1 -0
  102. package/dist/cjs/ui/admin/AdminLayout.js +36 -0
  103. package/dist/cjs/ui/admin/AdminLayout.js.map +1 -0
  104. package/dist/cjs/ui/admin/AdminUiRouter.js +155 -0
  105. package/dist/cjs/ui/admin/AdminUiRouter.js.map +1 -0
  106. package/dist/cjs/ui/admin/pages/AdminDashboard.js +55 -0
  107. package/dist/cjs/ui/admin/pages/AdminDashboard.js.map +1 -0
  108. package/dist/cjs/ui/admin/partials.js +64 -0
  109. package/dist/cjs/ui/admin/partials.js.map +1 -0
  110. package/dist/cjs/ui/html.js +42 -0
  111. package/dist/cjs/ui/html.js.map +1 -0
  112. package/dist/cjs/ui/layout.js +39 -0
  113. package/dist/cjs/ui/layout.js.map +1 -0
  114. package/dist/cjs/ui/pages/dashboard.js +51 -0
  115. package/dist/cjs/ui/pages/dashboard.js.map +1 -0
  116. package/dist/cjs/ui/pages/request-detail.js +119 -0
  117. package/dist/cjs/ui/pages/request-detail.js.map +1 -0
  118. package/dist/cjs/ui/pages/requests.js +120 -0
  119. package/dist/cjs/ui/pages/requests.js.map +1 -0
  120. package/dist/cjs/ui/pages/stubs.js +46 -0
  121. package/dist/cjs/ui/pages/stubs.js.map +1 -0
  122. package/dist/cjs/ui/partials.js +104 -0
  123. package/dist/cjs/ui/partials.js.map +1 -0
  124. package/dist/dts/Program.d.ts +2 -0
  125. package/dist/dts/Program.d.ts.map +1 -0
  126. package/dist/dts/api/AdminApi.d.ts +490 -0
  127. package/dist/dts/api/AdminApi.d.ts.map +1 -0
  128. package/dist/dts/api/ApiErrors.d.ts +26 -0
  129. package/dist/dts/api/ApiErrors.d.ts.map +1 -0
  130. package/dist/dts/api/ApiSchemas.d.ts +36 -0
  131. package/dist/dts/api/ApiSchemas.d.ts.map +1 -0
  132. package/dist/dts/api/Conversions.d.ts +7 -0
  133. package/dist/dts/api/Conversions.d.ts.map +1 -0
  134. package/dist/dts/api/ImpostersGroup.d.ts +448 -0
  135. package/dist/dts/api/ImpostersGroup.d.ts.map +1 -0
  136. package/dist/dts/api/ImpostersHandlers.d.ts +9 -0
  137. package/dist/dts/api/ImpostersHandlers.d.ts.map +1 -0
  138. package/dist/dts/api/SystemGroup.d.ts +46 -0
  139. package/dist/dts/api/SystemGroup.d.ts.map +1 -0
  140. package/dist/dts/api/SystemHandlers.d.ts +4 -0
  141. package/dist/dts/api/SystemHandlers.d.ts.map +1 -0
  142. package/dist/dts/cli/Commands.d.ts +4 -0
  143. package/dist/dts/cli/Commands.d.ts.map +1 -0
  144. package/dist/dts/cli/ConfigLoader.d.ts +13 -0
  145. package/dist/dts/cli/ConfigLoader.d.ts.map +1 -0
  146. package/dist/dts/client/HandlerHttpClient.d.ts +5 -0
  147. package/dist/dts/client/HandlerHttpClient.d.ts.map +1 -0
  148. package/dist/dts/client/ImpostersClient.d.ts +1868 -0
  149. package/dist/dts/client/ImpostersClient.d.ts.map +1 -0
  150. package/dist/dts/client/index.d.ts +6 -0
  151. package/dist/dts/client/index.d.ts.map +1 -0
  152. package/dist/dts/client/testing.d.ts +35 -0
  153. package/dist/dts/client/testing.d.ts.map +1 -0
  154. package/dist/dts/domain/imposter.d.ts +123 -0
  155. package/dist/dts/domain/imposter.d.ts.map +1 -0
  156. package/dist/dts/domain/route.d.ts +128 -0
  157. package/dist/dts/domain/route.d.ts.map +1 -0
  158. package/dist/dts/index.d.ts +60 -0
  159. package/dist/dts/index.d.ts.map +1 -0
  160. package/dist/dts/layers/ApiLayer.d.ts +3 -0
  161. package/dist/dts/layers/ApiLayer.d.ts.map +1 -0
  162. package/dist/dts/layers/MainLayer.d.ts +3 -0
  163. package/dist/dts/layers/MainLayer.d.ts.map +1 -0
  164. package/dist/dts/matching/ExpressionEvaluator.d.ts +11 -0
  165. package/dist/dts/matching/ExpressionEvaluator.d.ts.map +1 -0
  166. package/dist/dts/matching/RequestMatcher.d.ts +13 -0
  167. package/dist/dts/matching/RequestMatcher.d.ts.map +1 -0
  168. package/dist/dts/matching/ResponseGenerator.d.ts +9 -0
  169. package/dist/dts/matching/ResponseGenerator.d.ts.map +1 -0
  170. package/dist/dts/matching/TemplateEngine.d.ts +4 -0
  171. package/dist/dts/matching/TemplateEngine.d.ts.map +1 -0
  172. package/dist/dts/repositories/ImposterRepository.d.ts +33 -0
  173. package/dist/dts/repositories/ImposterRepository.d.ts.map +1 -0
  174. package/dist/dts/schemas/ConfigFileSchema.d.ts +142 -0
  175. package/dist/dts/schemas/ConfigFileSchema.d.ts.map +1 -0
  176. package/dist/dts/schemas/ImposterSchema.d.ts +368 -0
  177. package/dist/dts/schemas/ImposterSchema.d.ts.map +1 -0
  178. package/dist/dts/schemas/RequestLogSchema.d.ts +36 -0
  179. package/dist/dts/schemas/RequestLogSchema.d.ts.map +1 -0
  180. package/dist/dts/schemas/StubSchema.d.ts +112 -0
  181. package/dist/dts/schemas/StubSchema.d.ts.map +1 -0
  182. package/dist/dts/schemas/common.d.ts +56 -0
  183. package/dist/dts/schemas/common.d.ts.map +1 -0
  184. package/dist/dts/server/AdminServer.d.ts +11 -0
  185. package/dist/dts/server/AdminServer.d.ts.map +1 -0
  186. package/dist/dts/server/BunServer.d.ts +17 -0
  187. package/dist/dts/server/BunServer.d.ts.map +1 -0
  188. package/dist/dts/server/FiberManager.d.ts +12 -0
  189. package/dist/dts/server/FiberManager.d.ts.map +1 -0
  190. package/dist/dts/server/ImposterServer.d.ts +29 -0
  191. package/dist/dts/server/ImposterServer.d.ts.map +1 -0
  192. package/dist/dts/services/AppConfig.d.ts +14 -0
  193. package/dist/dts/services/AppConfig.d.ts.map +1 -0
  194. package/dist/dts/services/MetricsService.d.ts +26 -0
  195. package/dist/dts/services/MetricsService.d.ts.map +1 -0
  196. package/dist/dts/services/PortAllocator.d.ts +29 -0
  197. package/dist/dts/services/PortAllocator.d.ts.map +1 -0
  198. package/dist/dts/services/ProxyService.d.ts +24 -0
  199. package/dist/dts/services/ProxyService.d.ts.map +1 -0
  200. package/dist/dts/services/RequestLogger.d.ts +23 -0
  201. package/dist/dts/services/RequestLogger.d.ts.map +1 -0
  202. package/dist/dts/services/Uuid.d.ts +9 -0
  203. package/dist/dts/services/Uuid.d.ts.map +1 -0
  204. package/dist/dts/services/UuidLive.d.ts +4 -0
  205. package/dist/dts/services/UuidLive.d.ts.map +1 -0
  206. package/dist/dts/ui/UiRouter.d.ts +15 -0
  207. package/dist/dts/ui/UiRouter.d.ts.map +1 -0
  208. package/dist/dts/ui/admin/AdminLayout.d.ts +7 -0
  209. package/dist/dts/ui/admin/AdminLayout.d.ts.map +1 -0
  210. package/dist/dts/ui/admin/AdminUiRouter.d.ts +6 -0
  211. package/dist/dts/ui/admin/AdminUiRouter.d.ts.map +1 -0
  212. package/dist/dts/ui/admin/pages/AdminDashboard.d.ts +7 -0
  213. package/dist/dts/ui/admin/pages/AdminDashboard.d.ts.map +1 -0
  214. package/dist/dts/ui/admin/partials.d.ts +14 -0
  215. package/dist/dts/ui/admin/partials.d.ts.map +1 -0
  216. package/dist/dts/ui/html.d.ts +12 -0
  217. package/dist/dts/ui/html.d.ts.map +1 -0
  218. package/dist/dts/ui/layout.d.ts +9 -0
  219. package/dist/dts/ui/layout.d.ts.map +1 -0
  220. package/dist/dts/ui/pages/dashboard.d.ts +10 -0
  221. package/dist/dts/ui/pages/dashboard.d.ts.map +1 -0
  222. package/dist/dts/ui/pages/request-detail.d.ts +11 -0
  223. package/dist/dts/ui/pages/request-detail.d.ts.map +1 -0
  224. package/dist/dts/ui/pages/requests.d.ts +15 -0
  225. package/dist/dts/ui/pages/requests.d.ts.map +1 -0
  226. package/dist/dts/ui/pages/stubs.d.ts +8 -0
  227. package/dist/dts/ui/pages/stubs.d.ts.map +1 -0
  228. package/dist/dts/ui/partials.d.ts +13 -0
  229. package/dist/dts/ui/partials.d.ts.map +1 -0
  230. package/dist/esm/Program.js +2 -0
  231. package/dist/esm/Program.js.map +1 -0
  232. package/dist/esm/api/AdminApi.js +5 -0
  233. package/dist/esm/api/AdminApi.js.map +1 -0
  234. package/dist/esm/api/ApiErrors.js +20 -0
  235. package/dist/esm/api/ApiErrors.js.map +1 -0
  236. package/dist/esm/api/ApiSchemas.js +29 -0
  237. package/dist/esm/api/ApiSchemas.js.map +1 -0
  238. package/dist/esm/api/Conversions.js +32 -0
  239. package/dist/esm/api/Conversions.js.map +1 -0
  240. package/dist/esm/api/ImpostersGroup.js +30 -0
  241. package/dist/esm/api/ImpostersGroup.js.map +1 -0
  242. package/dist/esm/api/ImpostersHandlers.js +354 -0
  243. package/dist/esm/api/ImpostersHandlers.js.map +1 -0
  244. package/dist/esm/api/SystemGroup.js +6 -0
  245. package/dist/esm/api/SystemGroup.js.map +1 -0
  246. package/dist/esm/api/SystemHandlers.js +67 -0
  247. package/dist/esm/api/SystemHandlers.js.map +1 -0
  248. package/dist/esm/cli/Commands.js +98 -0
  249. package/dist/esm/cli/Commands.js.map +1 -0
  250. package/dist/esm/cli/ConfigLoader.js +25 -0
  251. package/dist/esm/cli/ConfigLoader.js.map +1 -0
  252. package/dist/esm/client/HandlerHttpClient.js +42 -0
  253. package/dist/esm/client/HandlerHttpClient.js.map +1 -0
  254. package/dist/esm/client/ImpostersClient.js +10 -0
  255. package/dist/esm/client/ImpostersClient.js.map +1 -0
  256. package/dist/esm/client/index.js +4 -0
  257. package/dist/esm/client/index.js.map +1 -0
  258. package/dist/esm/client/testing.js +86 -0
  259. package/dist/esm/client/testing.js.map +1 -0
  260. package/dist/esm/domain/imposter.js +103 -0
  261. package/dist/esm/domain/imposter.js.map +1 -0
  262. package/dist/esm/domain/route.js +164 -0
  263. package/dist/esm/domain/route.js.map +1 -0
  264. package/dist/esm/index.js +60 -0
  265. package/dist/esm/index.js.map +1 -0
  266. package/dist/esm/layers/ApiLayer.js +11 -0
  267. package/dist/esm/layers/ApiLayer.js.map +1 -0
  268. package/dist/esm/layers/MainLayer.js +20 -0
  269. package/dist/esm/layers/MainLayer.js.map +1 -0
  270. package/dist/esm/matching/ExpressionEvaluator.js +94 -0
  271. package/dist/esm/matching/ExpressionEvaluator.js.map +1 -0
  272. package/dist/esm/matching/RequestMatcher.js +135 -0
  273. package/dist/esm/matching/RequestMatcher.js.map +1 -0
  274. package/dist/esm/matching/ResponseGenerator.js +71 -0
  275. package/dist/esm/matching/ResponseGenerator.js.map +1 -0
  276. package/dist/esm/matching/TemplateEngine.js +47 -0
  277. package/dist/esm/matching/TemplateEngine.js.map +1 -0
  278. package/dist/esm/package.json +4 -0
  279. package/dist/esm/repositories/ImposterRepository.js +110 -0
  280. package/dist/esm/repositories/ImposterRepository.js.map +1 -0
  281. package/dist/esm/schemas/ConfigFileSchema.js +37 -0
  282. package/dist/esm/schemas/ConfigFileSchema.js.map +1 -0
  283. package/dist/esm/schemas/ImposterSchema.js +195 -0
  284. package/dist/esm/schemas/ImposterSchema.js.map +1 -0
  285. package/dist/esm/schemas/RequestLogSchema.js +44 -0
  286. package/dist/esm/schemas/RequestLogSchema.js.map +1 -0
  287. package/dist/esm/schemas/StubSchema.js +77 -0
  288. package/dist/esm/schemas/StubSchema.js.map +1 -0
  289. package/dist/esm/schemas/common.js +59 -0
  290. package/dist/esm/schemas/common.js.map +1 -0
  291. package/dist/esm/server/AdminServer.js +27 -0
  292. package/dist/esm/server/AdminServer.js.map +1 -0
  293. package/dist/esm/server/BunServer.js +6 -0
  294. package/dist/esm/server/BunServer.js.map +1 -0
  295. package/dist/esm/server/FiberManager.js +14 -0
  296. package/dist/esm/server/FiberManager.js.map +1 -0
  297. package/dist/esm/server/ImposterServer.js +225 -0
  298. package/dist/esm/server/ImposterServer.js.map +1 -0
  299. package/dist/esm/services/AppConfig.js +11 -0
  300. package/dist/esm/services/AppConfig.js.map +1 -0
  301. package/dist/esm/services/MetricsService.js +105 -0
  302. package/dist/esm/services/MetricsService.js.map +1 -0
  303. package/dist/esm/services/PortAllocator.js +41 -0
  304. package/dist/esm/services/PortAllocator.js.map +1 -0
  305. package/dist/esm/services/ProxyService.js +101 -0
  306. package/dist/esm/services/ProxyService.js.map +1 -0
  307. package/dist/esm/services/RequestLogger.js +53 -0
  308. package/dist/esm/services/RequestLogger.js.map +1 -0
  309. package/dist/esm/services/Uuid.js +3 -0
  310. package/dist/esm/services/Uuid.js.map +1 -0
  311. package/dist/esm/services/UuidLive.js +9 -0
  312. package/dist/esm/services/UuidLive.js.map +1 -0
  313. package/dist/esm/ui/UiRouter.js +235 -0
  314. package/dist/esm/ui/UiRouter.js.map +1 -0
  315. package/dist/esm/ui/admin/AdminLayout.js +29 -0
  316. package/dist/esm/ui/admin/AdminLayout.js.map +1 -0
  317. package/dist/esm/ui/admin/AdminUiRouter.js +148 -0
  318. package/dist/esm/ui/admin/AdminUiRouter.js.map +1 -0
  319. package/dist/esm/ui/admin/pages/AdminDashboard.js +48 -0
  320. package/dist/esm/ui/admin/pages/AdminDashboard.js.map +1 -0
  321. package/dist/esm/ui/admin/partials.js +54 -0
  322. package/dist/esm/ui/admin/partials.js.map +1 -0
  323. package/dist/esm/ui/html.js +32 -0
  324. package/dist/esm/ui/html.js.map +1 -0
  325. package/dist/esm/ui/layout.js +32 -0
  326. package/dist/esm/ui/layout.js.map +1 -0
  327. package/dist/esm/ui/pages/dashboard.js +44 -0
  328. package/dist/esm/ui/pages/dashboard.js.map +1 -0
  329. package/dist/esm/ui/pages/request-detail.js +112 -0
  330. package/dist/esm/ui/pages/request-detail.js.map +1 -0
  331. package/dist/esm/ui/pages/requests.js +112 -0
  332. package/dist/esm/ui/pages/requests.js.map +1 -0
  333. package/dist/esm/ui/pages/stubs.js +39 -0
  334. package/dist/esm/ui/pages/stubs.js.map +1 -0
  335. package/dist/esm/ui/partials.js +91 -0
  336. package/dist/esm/ui/partials.js.map +1 -0
  337. package/domain/imposter/package.json +6 -0
  338. package/domain/route/package.json +6 -0
  339. package/layers/ApiLayer/package.json +6 -0
  340. package/layers/MainLayer/package.json +6 -0
  341. package/matching/ExpressionEvaluator/package.json +6 -0
  342. package/matching/RequestMatcher/package.json +6 -0
  343. package/matching/ResponseGenerator/package.json +6 -0
  344. package/matching/TemplateEngine/package.json +6 -0
  345. package/package.json +435 -0
  346. package/repositories/ImposterRepository/package.json +6 -0
  347. package/schemas/ConfigFileSchema/package.json +6 -0
  348. package/schemas/ImposterSchema/package.json +6 -0
  349. package/schemas/RequestLogSchema/package.json +6 -0
  350. package/schemas/StubSchema/package.json +6 -0
  351. package/schemas/common/package.json +6 -0
  352. package/server/AdminServer/package.json +6 -0
  353. package/server/BunServer/package.json +6 -0
  354. package/server/FiberManager/package.json +6 -0
  355. package/server/ImposterServer/package.json +6 -0
  356. package/services/AppConfig/package.json +6 -0
  357. package/services/MetricsService/package.json +6 -0
  358. package/services/PortAllocator/package.json +6 -0
  359. package/services/ProxyService/package.json +6 -0
  360. package/services/RequestLogger/package.json +6 -0
  361. package/services/Uuid/package.json +6 -0
  362. package/services/UuidLive/package.json +6 -0
  363. package/src/Program.ts +1 -0
  364. package/src/api/AdminApi.ts +7 -0
  365. package/src/api/ApiErrors.ts +20 -0
  366. package/src/api/ApiSchemas.ts +36 -0
  367. package/src/api/Conversions.ts +34 -0
  368. package/src/api/ImpostersGroup.ts +103 -0
  369. package/src/api/ImpostersHandlers.ts +387 -0
  370. package/src/api/SystemGroup.ts +12 -0
  371. package/src/api/SystemHandlers.ts +76 -0
  372. package/src/cli/Commands.ts +119 -0
  373. package/src/cli/ConfigLoader.ts +41 -0
  374. package/src/client/HandlerHttpClient.ts +50 -0
  375. package/src/client/ImpostersClient.ts +21 -0
  376. package/src/client/index.ts +9 -0
  377. package/src/client/testing.ts +105 -0
  378. package/src/domain/imposter.ts +186 -0
  379. package/src/domain/route.ts +255 -0
  380. package/src/index.ts +153 -0
  381. package/src/layers/ApiLayer.ts +21 -0
  382. package/src/layers/MainLayer.ts +43 -0
  383. package/src/matching/ExpressionEvaluator.ts +102 -0
  384. package/src/matching/RequestMatcher.ts +162 -0
  385. package/src/matching/ResponseGenerator.ts +86 -0
  386. package/src/matching/TemplateEngine.ts +54 -0
  387. package/src/repositories/ImposterRepository.ts +145 -0
  388. package/src/schemas/ConfigFileSchema.ts +32 -0
  389. package/src/schemas/ImposterSchema.ts +232 -0
  390. package/src/schemas/RequestLogSchema.ts +38 -0
  391. package/src/schemas/StubSchema.ts +90 -0
  392. package/src/schemas/common.ts +95 -0
  393. package/src/server/AdminServer.ts +22 -0
  394. package/src/server/BunServer.ts +19 -0
  395. package/src/server/FiberManager.ts +25 -0
  396. package/src/server/ImposterServer.ts +244 -0
  397. package/src/services/AppConfig.ts +22 -0
  398. package/src/services/MetricsService.ts +157 -0
  399. package/src/services/PortAllocator.ts +68 -0
  400. package/src/services/ProxyService.ts +139 -0
  401. package/src/services/RequestLogger.ts +87 -0
  402. package/src/services/Uuid.ts +9 -0
  403. package/src/services/UuidLive.ts +9 -0
  404. package/src/types/bun.d.ts +6 -0
  405. package/src/ui/UiRouter.ts +278 -0
  406. package/src/ui/admin/AdminLayout.ts +36 -0
  407. package/src/ui/admin/AdminUiRouter.ts +170 -0
  408. package/src/ui/admin/pages/AdminDashboard.ts +54 -0
  409. package/src/ui/admin/partials.ts +83 -0
  410. package/src/ui/html.ts +37 -0
  411. package/src/ui/layout.ts +44 -0
  412. package/src/ui/pages/dashboard.ts +64 -0
  413. package/src/ui/pages/request-detail.ts +142 -0
  414. package/src/ui/pages/requests.ts +141 -0
  415. package/src/ui/pages/stubs.ts +52 -0
  416. package/src/ui/partials.ts +133 -0
  417. package/ui/UiRouter/package.json +6 -0
  418. package/ui/admin/AdminLayout/package.json +6 -0
  419. package/ui/admin/AdminUiRouter/package.json +6 -0
  420. package/ui/admin/pages/AdminDashboard/package.json +6 -0
  421. package/ui/admin/partials/package.json +6 -0
  422. package/ui/html/package.json +6 -0
  423. package/ui/layout/package.json +6 -0
  424. package/ui/pages/dashboard/package.json +6 -0
  425. package/ui/pages/requests/package.json +6 -0
  426. package/ui/pages/stubs/package.json +6 -0
@@ -0,0 +1,25 @@
1
+ import { Context, Effect, FiberMap, Layer } from "effect"
2
+
3
+ export interface FiberManagerShape {
4
+ readonly start: (id: string, effect: Effect.Effect<never, unknown>) => Effect.Effect<void>
5
+ readonly stop: (id: string) => Effect.Effect<void>
6
+ readonly isRunning: (id: string) => Effect.Effect<boolean>
7
+ }
8
+
9
+ export class FiberManager extends Context.Tag("FiberManager")<FiberManager, FiberManagerShape>() {}
10
+
11
+ export const FiberManagerLive = Layer.scoped(
12
+ FiberManager,
13
+ Effect.gen(function*() {
14
+ const fiberMap = yield* FiberMap.make<string>()
15
+
16
+ const start = (id: string, effect: Effect.Effect<never, unknown>): Effect.Effect<void> =>
17
+ FiberMap.run(fiberMap, id, effect)
18
+
19
+ const stop = (id: string): Effect.Effect<void> => FiberMap.remove(fiberMap, id)
20
+
21
+ const isRunning = (id: string): Effect.Effect<boolean> => FiberMap.has(fiberMap, id)
22
+
23
+ return { start, stop, isRunning }
24
+ })
25
+ )
@@ -0,0 +1,244 @@
1
+ import { Context, Data, Effect, HashMap, Layer, Ref, Runtime } from "effect"
2
+ import * as DateTime from "effect/DateTime"
3
+ import { ImposterConfig, type ImposterNotFoundError, type ProxyConfigDomain } from "../domain/imposter"
4
+ import { extractRequestContext, findMatchingStub } from "../matching/RequestMatcher"
5
+ import { buildResponse, makeResponseState } from "../matching/ResponseGenerator"
6
+ import { ImposterRepository } from "../repositories/ImposterRepository"
7
+ import { NonEmptyString } from "../schemas/common"
8
+ import type { RequestLogEntry } from "../schemas/RequestLogSchema"
9
+ import type { Stub } from "../schemas/StubSchema"
10
+ import { MetricsService } from "../services/MetricsService"
11
+ import { ProxyService } from "../services/ProxyService"
12
+ import { RequestLogger } from "../services/RequestLogger"
13
+ import { makeUiRouter } from "../ui/UiRouter"
14
+ import { ServerFactory } from "./BunServer"
15
+ import { FiberManager } from "./FiberManager"
16
+
17
+ export class ImposterServerError extends Data.TaggedError("ImposterServerError")<{
18
+ readonly imposterId: string
19
+ readonly reason: string
20
+ }> {}
21
+
22
+ export interface ImposterServerShape {
23
+ readonly start: (id: string) => Effect.Effect<void, ImposterServerError | ImposterNotFoundError>
24
+ readonly stop: (id: string) => Effect.Effect<void>
25
+ readonly updateStubs: (id: string) => Effect.Effect<void>
26
+ readonly updateProxyConfig: (id: string) => Effect.Effect<void>
27
+ readonly isRunning: (id: string) => Effect.Effect<boolean>
28
+ }
29
+
30
+ export class ImposterServer extends Context.Tag("ImposterServer")<ImposterServer, ImposterServerShape>() {}
31
+
32
+ interface ImposterState {
33
+ readonly stubsRef: Ref.Ref<ReadonlyArray<Stub>>
34
+ readonly proxyConfigRef: Ref.Ref<ProxyConfigDomain | undefined>
35
+ }
36
+
37
+ export const ImposterServerLive = Layer.effect(
38
+ ImposterServer,
39
+ Effect.gen(function*() {
40
+ const repo = yield* ImposterRepository
41
+ const fiberManager = yield* FiberManager
42
+ const serverFactory = yield* ServerFactory
43
+ const requestLogger = yield* RequestLogger
44
+ const metricsService = yield* MetricsService
45
+ const proxyService = yield* ProxyService
46
+ const stateMapRef = yield* Ref.make<HashMap.HashMap<string, ImposterState>>(HashMap.empty())
47
+
48
+ const start = (id: string): Effect.Effect<void, ImposterServerError | ImposterNotFoundError> =>
49
+ Effect.gen(function*() {
50
+ const record = yield* repo.get(id)
51
+ const config = record.config
52
+
53
+ // Create per-imposter state
54
+ const stubsRef = yield* Ref.make<ReadonlyArray<Stub>>(record.stubs)
55
+ const proxyConfigRef = yield* Ref.make<ProxyConfigDomain | undefined>(config.proxy)
56
+ const responseState = yield* makeResponseState()
57
+
58
+ // Store state for hot-reload
59
+ yield* Ref.update(stateMapRef, HashMap.set(id, { stubsRef, proxyConfigRef } as ImposterState))
60
+
61
+ // Capture runtime for running effects inside fetch handler
62
+ const rt = yield* Effect.runtime<never>()
63
+ const runPromise = Runtime.runPromise(rt)
64
+
65
+ // UI router for /_admin pages
66
+ const uiRouter = makeUiRouter({ id, config, stubsRef, repo, requestLogger, runPromise })
67
+
68
+ const handler = async (request: Request): Promise<Response> => {
69
+ // Try UI router first (returns null if not a /_admin path)
70
+ const uiResponse = await uiRouter(request)
71
+ if (uiResponse !== null) return uiResponse
72
+
73
+ return runPromise(
74
+ Effect.gen(function*() {
75
+ const startTime = Date.now()
76
+ const stubs = yield* Ref.get(stubsRef)
77
+ const ctx = yield* Effect.promise(() => extractRequestContext(request))
78
+ const stub = findMatchingStub(ctx, stubs)
79
+
80
+ let response: Response
81
+ let proxied = false
82
+ if (!stub) {
83
+ const proxyConfig = yield* Ref.get(proxyConfigRef)
84
+ if (proxyConfig) {
85
+ const url = new URL(request.url)
86
+ response = yield* proxyService.forward(ctx, proxyConfig, url).pipe(
87
+ Effect.catchTag("ProxyError", (err) =>
88
+ Effect.succeed(
89
+ new Response(
90
+ JSON.stringify({ error: "Proxy failed", target: err.targetUrl, reason: err.reason }),
91
+ { status: 502, headers: { "content-type": "application/json" } }
92
+ )
93
+ ))
94
+ )
95
+ proxied = true
96
+ // Record mode: save as stub + update stubsRef
97
+ if (proxyConfig.mode === "record" && response.status < 500) {
98
+ const responseClone = response.clone()
99
+ const newStub = yield* proxyService.recordAsStub(ctx, responseClone)
100
+ yield* repo.addStub(id, newStub).pipe(Effect.catchAll(() => Effect.void))
101
+ const freshStubs = yield* repo.getStubs(id).pipe(
102
+ Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
103
+ )
104
+ yield* Ref.set(stubsRef, freshStubs)
105
+ }
106
+ } else {
107
+ response = new Response(
108
+ JSON.stringify({ error: "No matching stub found", method: ctx.method, path: ctx.path }),
109
+ { status: 404, headers: { "content-type": "application/json" } }
110
+ )
111
+ }
112
+ } else {
113
+ const responses = stub.responses
114
+ const index = yield* responseState.getNextIndex(id, stub.id, responses.length, stub.responseMode)
115
+ const responseConfig = responses[index]!
116
+ const delay = responseConfig.delay
117
+ if (delay !== undefined && delay > 0) {
118
+ yield* Effect.sleep(`${delay} millis`)
119
+ }
120
+ response = yield* Effect.promise(() => buildResponse(responseConfig, ctx))
121
+ }
122
+
123
+ // Capture response for logging
124
+ const respText = yield* Effect.promise(() => response.text())
125
+ const respHeaders: Record<string, string> = {}
126
+ response.headers.forEach((val, key) => {
127
+ respHeaders[key] = val
128
+ })
129
+ // Reconstruct since .text() consumed body
130
+ response = new Response(respText, { status: response.status, headers: response.headers })
131
+
132
+ const logBody = respText.length > 10240 ? respText.slice(0, 10240) : (respText || undefined)
133
+
134
+ const duration = Date.now() - startTime
135
+ const logEntry: RequestLogEntry = {
136
+ id: NonEmptyString.make(crypto.randomUUID()),
137
+ imposterId: NonEmptyString.make(id),
138
+ timestamp: DateTime.unsafeMake(startTime),
139
+ request: {
140
+ method: ctx.method,
141
+ path: ctx.path,
142
+ headers: ctx.headers,
143
+ query: ctx.query,
144
+ body: ctx.body
145
+ },
146
+ response: {
147
+ status: response.status,
148
+ headers: respHeaders,
149
+ ...(logBody !== undefined ? { body: logBody } : {}),
150
+ ...(stub ? { matchedStubId: NonEmptyString.make(stub.id) } : {}),
151
+ proxied
152
+ },
153
+ duration
154
+ }
155
+ yield* requestLogger.log(logEntry).pipe(Effect.catchAll(() => Effect.void))
156
+ yield* metricsService.recordRequest(logEntry).pipe(Effect.catchAll(() => Effect.void))
157
+
158
+ return response
159
+ }).pipe(
160
+ Effect.catchAllCause((cause) =>
161
+ Effect.succeed(
162
+ new Response(
163
+ JSON.stringify({ error: "Internal server error", details: String(cause) }),
164
+ { status: 500, headers: { "content-type": "application/json" } }
165
+ )
166
+ )
167
+ )
168
+ )
169
+ )
170
+ }
171
+
172
+ // Build the long-running fiber effect with acquireRelease
173
+ const fiberEffect = Effect.acquireRelease(
174
+ Effect.try({
175
+ try: () => serverFactory.create({ port: config.port, fetch: handler }),
176
+ catch: (err) =>
177
+ new ImposterServerError({ imposterId: id, reason: `Failed to bind port ${config.port}: ${err}` })
178
+ }),
179
+ (server) => Effect.sync(() => server.stop(true))
180
+ ).pipe(
181
+ Effect.andThen(Effect.never),
182
+ Effect.scoped
183
+ )
184
+
185
+ // Wrap fiber in onError for crash supervision
186
+ const supervisedEffect = fiberEffect.pipe(
187
+ Effect.onError(() =>
188
+ Effect.gen(function*() {
189
+ yield* Ref.update(stateMapRef, HashMap.remove(id))
190
+ yield* repo.update(id, (r) => ({
191
+ ...r,
192
+ config: ImposterConfig({ ...r.config, status: "stopped" })
193
+ })).pipe(Effect.catchAll(() => Effect.void))
194
+ yield* responseState.reset(id)
195
+ })
196
+ )
197
+ ) as Effect.Effect<never, unknown>
198
+
199
+ yield* fiberManager.start(id, supervisedEffect)
200
+
201
+ // Update status to running
202
+ yield* repo.update(id, (r) => ({
203
+ ...r,
204
+ config: ImposterConfig({ ...r.config, status: "running" })
205
+ })).pipe(Effect.catchTag("ImposterNotFoundError", () => Effect.void))
206
+ })
207
+
208
+ const stop = (id: string): Effect.Effect<void> =>
209
+ Effect.gen(function*() {
210
+ yield* fiberManager.stop(id)
211
+ yield* Ref.update(stateMapRef, HashMap.remove(id))
212
+ yield* repo.update(id, (r) => ({
213
+ ...r,
214
+ config: ImposterConfig({ ...r.config, status: "stopped" })
215
+ })).pipe(Effect.catchAll(() => Effect.void))
216
+ yield* requestLogger.removeImposter(id)
217
+ })
218
+
219
+ const updateStubs = (id: string): Effect.Effect<void> =>
220
+ Effect.gen(function*() {
221
+ const stubs = yield* repo.getStubs(id).pipe(Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>)))
222
+ const stateMap = yield* Ref.get(stateMapRef)
223
+ const state = HashMap.get(stateMap, id)
224
+ if (state._tag === "Some") {
225
+ yield* Ref.set(state.value.stubsRef, stubs)
226
+ }
227
+ })
228
+
229
+ const updateProxyConfig = (id: string): Effect.Effect<void> =>
230
+ Effect.gen(function*() {
231
+ const record = yield* repo.get(id).pipe(Effect.catchAll(() => Effect.succeed(null)))
232
+ if (record === null) return
233
+ const stateMap = yield* Ref.get(stateMapRef)
234
+ const state = HashMap.get(stateMap, id)
235
+ if (state._tag === "Some") {
236
+ yield* Ref.set(state.value.proxyConfigRef, record.config.proxy)
237
+ }
238
+ })
239
+
240
+ const isRunning = (id: string): Effect.Effect<boolean> => fiberManager.isRunning(id)
241
+
242
+ return { start, stop, updateStubs, updateProxyConfig, isRunning } satisfies ImposterServerShape
243
+ })
244
+ )
@@ -0,0 +1,22 @@
1
+ import { Config, Context, Layer } from "effect"
2
+
3
+ export interface AppConfigShape {
4
+ readonly adminPort: number
5
+ readonly portRangeMin: number
6
+ readonly portRangeMax: number
7
+ readonly maxImposters: number
8
+ readonly logLevel: "debug" | "info" | "warn" | "error"
9
+ }
10
+
11
+ export class AppConfig extends Context.Tag("AppConfig")<AppConfig, AppConfigShape>() {}
12
+
13
+ const config = Config.all({
14
+ adminPort: Config.number("ADMIN_PORT").pipe(Config.withDefault(2525)),
15
+ portRangeMin: Config.number("PORT_RANGE_MIN").pipe(Config.withDefault(3000)),
16
+ portRangeMax: Config.number("PORT_RANGE_MAX").pipe(Config.withDefault(4000)),
17
+ maxImposters: Config.number("MAX_IMPOSTERS").pipe(Config.withDefault(100)),
18
+ logLevel: Config.literal("debug", "info", "warn", "error")("LOG_LEVEL")
19
+ .pipe(Config.withDefault("info" as const))
20
+ })
21
+
22
+ export const AppConfigLive = Layer.effect(AppConfig, config)
@@ -0,0 +1,157 @@
1
+ import { Context, Effect, HashMap, Layer, Ref } from "effect"
2
+ import * as DateTime from "effect/DateTime"
3
+ import type { RequestLogEntry } from "../schemas/RequestLogSchema"
4
+
5
+ const BUFFER_SIZE = 1000
6
+
7
+ interface ImposterMetrics {
8
+ totalRequests: number
9
+ requestsByMethod: Record<string, number>
10
+ requestsByStatusCode: Record<string, number>
11
+ responseTimes: Float64Array
12
+ responseTimeIndex: number
13
+ responseTimeCount: number
14
+ firstRequestAt: DateTime.Utc
15
+ lastRequestAt: DateTime.Utc
16
+ errorCount: number
17
+ }
18
+
19
+ export interface Statistics {
20
+ readonly totalRequests: number
21
+ readonly requestsPerMinute: number
22
+ readonly averageResponseTime: number
23
+ readonly errorRate: number
24
+ readonly requestsByMethod: Record<string, number>
25
+ readonly requestsByStatusCode: Record<string, number>
26
+ readonly lastRequestAt?: DateTime.Utc
27
+ readonly p50ResponseTime?: number
28
+ readonly p95ResponseTime?: number
29
+ readonly p99ResponseTime?: number
30
+ }
31
+
32
+ const makeEmptyMetrics = (now: DateTime.Utc): ImposterMetrics => ({
33
+ totalRequests: 0,
34
+ requestsByMethod: {},
35
+ requestsByStatusCode: {},
36
+ responseTimes: new Float64Array(BUFFER_SIZE),
37
+ responseTimeIndex: 0,
38
+ responseTimeCount: 0,
39
+ firstRequestAt: now,
40
+ lastRequestAt: now,
41
+ errorCount: 0
42
+ })
43
+
44
+ const computePercentile = (sorted: Array<number>, p: number): number => {
45
+ if (sorted.length === 0) return 0
46
+ const index = Math.ceil((p / 100) * sorted.length) - 1
47
+ return sorted[Math.max(0, index)]!
48
+ }
49
+
50
+ const computeStats = (metrics: ImposterMetrics): Statistics => {
51
+ const count = metrics.responseTimeCount
52
+ const total = metrics.totalRequests
53
+
54
+ // Compute average response time
55
+ let sumRT = 0
56
+ for (let i = 0; i < Math.min(count, BUFFER_SIZE); i++) {
57
+ sumRT += metrics.responseTimes[i]!
58
+ }
59
+ const avgRT = count > 0 ? sumRT / Math.min(count, BUFFER_SIZE) : 0
60
+
61
+ // Compute requests per minute
62
+ const elapsedMs = DateTime.toEpochMillis(metrics.lastRequestAt) - DateTime.toEpochMillis(metrics.firstRequestAt)
63
+ const elapsedMinutes = elapsedMs / 60000
64
+ const rpm = elapsedMinutes > 0 ? total / elapsedMinutes : total
65
+
66
+ // Compute error rate
67
+ const errorRate = total > 0 ? metrics.errorCount / total : 0
68
+
69
+ // Compute percentiles
70
+ const bufferLen = Math.min(count, BUFFER_SIZE)
71
+ const sorted = Array.from(metrics.responseTimes.subarray(0, bufferLen)).sort((a, b) => a - b)
72
+
73
+ return {
74
+ totalRequests: total,
75
+ requestsPerMinute: Math.round(rpm * 100) / 100,
76
+ averageResponseTime: Math.round(avgRT * 100) / 100,
77
+ errorRate: Math.round(errorRate * 10000) / 10000,
78
+ requestsByMethod: { ...metrics.requestsByMethod },
79
+ requestsByStatusCode: { ...metrics.requestsByStatusCode },
80
+ ...(total > 0 ? { lastRequestAt: metrics.lastRequestAt } : {}),
81
+ ...(bufferLen > 0
82
+ ? {
83
+ p50ResponseTime: computePercentile(sorted, 50),
84
+ p95ResponseTime: computePercentile(sorted, 95),
85
+ p99ResponseTime: computePercentile(sorted, 99)
86
+ }
87
+ : {})
88
+ }
89
+ }
90
+
91
+ export interface MetricsServiceShape {
92
+ readonly recordRequest: (entry: RequestLogEntry) => Effect.Effect<void>
93
+ readonly getStats: (imposterId: string) => Effect.Effect<Statistics>
94
+ readonly resetStats: (imposterId: string) => Effect.Effect<void>
95
+ }
96
+
97
+ export class MetricsService extends Context.Tag("MetricsService")<MetricsService, MetricsServiceShape>() {}
98
+
99
+ export const MetricsServiceLive = Layer.effect(
100
+ MetricsService,
101
+ Effect.gen(function*() {
102
+ const storeRef = yield* Ref.make(HashMap.empty<string, ImposterMetrics>())
103
+
104
+ const recordRequest = (entry: RequestLogEntry): Effect.Effect<void> =>
105
+ Ref.update(storeRef, (store) => {
106
+ const existing = HashMap.get(store, entry.imposterId)
107
+ const now = entry.timestamp
108
+ const metrics = existing._tag === "Some" ? existing.value : makeEmptyMetrics(now)
109
+
110
+ metrics.totalRequests += 1
111
+
112
+ // Method counts
113
+ const method = entry.request.method.toUpperCase()
114
+ metrics.requestsByMethod[method] = (metrics.requestsByMethod[method] ?? 0) + 1
115
+
116
+ // Status code counts
117
+ const statusKey = String(entry.response.status)
118
+ metrics.requestsByStatusCode[statusKey] = (metrics.requestsByStatusCode[statusKey] ?? 0) + 1
119
+
120
+ // Response time circular buffer
121
+ metrics.responseTimes[metrics.responseTimeIndex % BUFFER_SIZE] = entry.duration
122
+ metrics.responseTimeIndex = (metrics.responseTimeIndex + 1) % BUFFER_SIZE
123
+ metrics.responseTimeCount += 1
124
+
125
+ // Error tracking (4xx + 5xx)
126
+ if (entry.response.status >= 400) {
127
+ metrics.errorCount += 1
128
+ }
129
+
130
+ metrics.lastRequestAt = now
131
+
132
+ return HashMap.set(store, entry.imposterId, metrics)
133
+ })
134
+
135
+ const getStats = (imposterId: string): Effect.Effect<Statistics> =>
136
+ Ref.get(storeRef).pipe(
137
+ Effect.map((store) => {
138
+ const existing = HashMap.get(store, imposterId)
139
+ if (existing._tag === "None") {
140
+ return {
141
+ totalRequests: 0,
142
+ requestsPerMinute: 0,
143
+ averageResponseTime: 0,
144
+ errorRate: 0,
145
+ requestsByMethod: {},
146
+ requestsByStatusCode: {}
147
+ }
148
+ }
149
+ return computeStats(existing.value)
150
+ })
151
+ )
152
+
153
+ const resetStats = (imposterId: string): Effect.Effect<void> => Ref.update(storeRef, HashMap.remove(imposterId))
154
+
155
+ return { recordRequest, getStats, resetStats } satisfies MetricsServiceShape
156
+ })
157
+ )
@@ -0,0 +1,68 @@
1
+ import { Context, Data, Effect, HashSet, Layer, Ref } from "effect"
2
+ import { AppConfig } from "./AppConfig"
3
+
4
+ export class PortAllocatorError extends Data.TaggedError("PortAllocatorError")<{
5
+ readonly reason: string
6
+ readonly port?: number
7
+ }> {}
8
+
9
+ export class PortExhaustedError extends Data.TaggedError("PortExhaustedError")<{
10
+ readonly rangeMin: number
11
+ readonly rangeMax: number
12
+ }> {}
13
+
14
+ export interface PortAllocatorShape {
15
+ readonly allocate: (preferred?: number) => Effect.Effect<number, PortAllocatorError | PortExhaustedError>
16
+ readonly release: (port: number) => Effect.Effect<void>
17
+ readonly isAvailable: (port: number) => Effect.Effect<boolean>
18
+ }
19
+
20
+ export class PortAllocator extends Context.Tag("PortAllocator")<PortAllocator, PortAllocatorShape>() {}
21
+
22
+ export const PortAllocatorLive = Layer.effect(
23
+ PortAllocator,
24
+ Effect.gen(function*() {
25
+ const config = yield* AppConfig
26
+ const portsRef = yield* Ref.make(HashSet.empty<number>())
27
+
28
+ type AllocateResult = readonly [
29
+ Effect.Effect<number, PortAllocatorError | PortExhaustedError>,
30
+ HashSet.HashSet<number>
31
+ ]
32
+
33
+ const allocate = (preferred?: number): Effect.Effect<number, PortAllocatorError | PortExhaustedError> => {
34
+ if (preferred !== undefined) {
35
+ return Ref.modify(portsRef, (ports): AllocateResult => {
36
+ if (HashSet.has(ports, preferred)) {
37
+ return [
38
+ Effect.fail(
39
+ new PortAllocatorError({ reason: `Port ${preferred} is already allocated`, port: preferred })
40
+ ),
41
+ ports
42
+ ]
43
+ }
44
+ return [Effect.succeed(preferred), HashSet.add(ports, preferred)]
45
+ }).pipe(Effect.flatten)
46
+ }
47
+
48
+ return Ref.modify(portsRef, (ports): AllocateResult => {
49
+ for (let port = config.portRangeMin; port <= config.portRangeMax; port++) {
50
+ if (!HashSet.has(ports, port)) {
51
+ return [Effect.succeed(port), HashSet.add(ports, port)]
52
+ }
53
+ }
54
+ return [
55
+ Effect.fail(new PortExhaustedError({ rangeMin: config.portRangeMin, rangeMax: config.portRangeMax })),
56
+ ports
57
+ ]
58
+ }).pipe(Effect.flatten)
59
+ }
60
+
61
+ const release = (port: number): Effect.Effect<void> => Ref.update(portsRef, HashSet.remove(port))
62
+
63
+ const isAvailable = (port: number): Effect.Effect<boolean> =>
64
+ Ref.get(portsRef).pipe(Effect.map((ports) => !HashSet.has(ports, port)))
65
+
66
+ return { allocate, release, isAvailable }
67
+ })
68
+ )
@@ -0,0 +1,139 @@
1
+ import { Context, Data, Effect, Layer } from "effect"
2
+ import type { ProxyConfigDomain } from "../domain/imposter"
3
+ import type { RequestContext } from "../matching/RequestMatcher"
4
+ import { NonEmptyString } from "../schemas/common"
5
+ import type { Stub } from "../schemas/StubSchema"
6
+ import { Uuid } from "./Uuid"
7
+
8
+ export class ProxyError extends Data.TaggedError("ProxyError")<{
9
+ readonly targetUrl: string
10
+ readonly reason: string
11
+ readonly cause?: unknown
12
+ }> {}
13
+
14
+ const HOP_BY_HOP_HEADERS = new Set([
15
+ "host",
16
+ "connection",
17
+ "keep-alive",
18
+ "transfer-encoding",
19
+ "upgrade",
20
+ "proxy-authenticate",
21
+ "proxy-authorization",
22
+ "te",
23
+ "trailers"
24
+ ])
25
+
26
+ export interface ProxyServiceShape {
27
+ readonly forward: (
28
+ ctx: RequestContext,
29
+ config: ProxyConfigDomain,
30
+ originalUrl: URL
31
+ ) => Effect.Effect<Response, ProxyError>
32
+ readonly recordAsStub: (
33
+ request: RequestContext,
34
+ response: Response
35
+ ) => Effect.Effect<Stub>
36
+ }
37
+
38
+ export class ProxyService extends Context.Tag("ProxyService")<ProxyService, ProxyServiceShape>() {}
39
+
40
+ export const ProxyServiceLive = Layer.effect(
41
+ ProxyService,
42
+ Effect.gen(function*() {
43
+ const uuid = yield* Uuid
44
+
45
+ const forward = (
46
+ ctx: RequestContext,
47
+ config: ProxyConfigDomain,
48
+ originalUrl: URL
49
+ ): Effect.Effect<Response, ProxyError> =>
50
+ Effect.gen(function*() {
51
+ // Build target URL preserving path and query
52
+ const targetBase = config.targetUrl.replace(/\/$/, "")
53
+ const targetUrl = `${targetBase}${originalUrl.pathname}${originalUrl.search}`
54
+
55
+ // Build headers
56
+ const headers = new Headers()
57
+ for (const [key, val] of Object.entries(ctx.headers)) {
58
+ if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
59
+ headers.set(key, val)
60
+ }
61
+ }
62
+
63
+ // Remove headers specified in config
64
+ for (const h of config.removeHeaders) {
65
+ headers.delete(h)
66
+ }
67
+
68
+ // Add headers specified in config
69
+ if (config.addHeaders) {
70
+ for (const [key, val] of Object.entries(config.addHeaders)) {
71
+ headers.set(key, val)
72
+ }
73
+ }
74
+
75
+ // Build body
76
+ let body: string | undefined
77
+ if (ctx.body !== undefined && ctx.body !== null) {
78
+ body = typeof ctx.body === "string" ? ctx.body : JSON.stringify(ctx.body)
79
+ }
80
+
81
+ const response = yield* Effect.tryPromise({
82
+ try: (signal) =>
83
+ fetch(targetUrl, {
84
+ method: ctx.method,
85
+ headers,
86
+ ...(body !== undefined && ctx.method !== "GET" && ctx.method !== "HEAD" ? { body } : {}),
87
+ redirect: config.followRedirects ? "follow" : "manual",
88
+ signal
89
+ }),
90
+ catch: (err) => new ProxyError({ targetUrl, reason: `Failed to reach target: ${err}`, cause: err })
91
+ }).pipe(Effect.timeoutFail({
92
+ duration: `${config.timeout} millis`,
93
+ onTimeout: () => new ProxyError({ targetUrl, reason: `Request timed out after ${config.timeout}ms` })
94
+ }))
95
+
96
+ return response
97
+ })
98
+
99
+ const recordAsStub = (
100
+ request: RequestContext,
101
+ response: Response
102
+ ): Effect.Effect<Stub> =>
103
+ Effect.gen(function*() {
104
+ const id = yield* uuid.generateShort
105
+
106
+ const respHeaders: Record<string, string> = {}
107
+ response.headers.forEach((val, key) => {
108
+ respHeaders[key] = val
109
+ })
110
+
111
+ const respText = yield* Effect.promise(() => response.text())
112
+ let respBody: unknown = respText
113
+ const contentType = response.headers.get("content-type") ?? ""
114
+ if (contentType.includes("application/json") && respText) {
115
+ try {
116
+ respBody = JSON.parse(respText)
117
+ } catch {
118
+ // keep as string
119
+ }
120
+ }
121
+
122
+ return {
123
+ id: NonEmptyString.make(id),
124
+ predicates: [
125
+ { field: "method" as const, operator: "equals" as const, value: request.method, caseSensitive: true },
126
+ { field: "path" as const, operator: "equals" as const, value: request.path, caseSensitive: true }
127
+ ],
128
+ responses: [{
129
+ status: response.status,
130
+ headers: respHeaders,
131
+ body: respBody
132
+ }],
133
+ responseMode: "sequential" as const
134
+ }
135
+ })
136
+
137
+ return { forward, recordAsStub } satisfies ProxyServiceShape
138
+ })
139
+ )