@furystack/rest-service 12.3.5 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +47 -28
  3. package/esm/actions/get-current-user.spec.js +10 -6
  4. package/esm/actions/get-current-user.spec.js.map +1 -1
  5. package/esm/actions/index.d.ts +0 -1
  6. package/esm/actions/index.d.ts.map +1 -1
  7. package/esm/actions/index.js +0 -1
  8. package/esm/actions/index.js.map +1 -1
  9. package/esm/actions/is-authenticated.spec.js +10 -6
  10. package/esm/actions/is-authenticated.spec.js.map +1 -1
  11. package/esm/actions/logout-action.spec.js +11 -9
  12. package/esm/actions/logout-action.spec.js.map +1 -1
  13. package/esm/actions/logout.js +1 -1
  14. package/esm/actions/logout.js.map +1 -1
  15. package/esm/actions/password-login-action.js +1 -1
  16. package/esm/actions/password-login-action.js.map +1 -1
  17. package/esm/actions/password-login-action.spec.js +23 -28
  18. package/esm/actions/password-login-action.spec.js.map +1 -1
  19. package/esm/authenticate.js +1 -1
  20. package/esm/authenticate.js.map +1 -1
  21. package/esm/authenticate.spec.js +41 -33
  22. package/esm/authenticate.spec.js.map +1 -1
  23. package/esm/authorize.spec.js +34 -29
  24. package/esm/authorize.spec.js.map +1 -1
  25. package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
  26. package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
  27. package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
  28. package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
  29. package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
  30. package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
  31. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
  32. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
  33. package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
  34. package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
  35. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
  36. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
  37. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
  38. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
  39. package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
  40. package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
  41. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
  42. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
  43. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -15
  44. package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
  45. package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -24
  46. package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
  47. package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
  48. package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
  49. package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
  50. package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
  51. package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
  52. package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
  53. package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
  54. package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
  55. package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
  56. package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
  57. package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
  58. package/esm/endpoint-generators/create-post-endpoint.js +8 -13
  59. package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
  60. package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
  61. package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
  62. package/esm/endpoint-generators/utils.d.ts +17 -2
  63. package/esm/endpoint-generators/utils.d.ts.map +1 -1
  64. package/esm/endpoint-generators/utils.js +33 -20
  65. package/esm/endpoint-generators/utils.js.map +1 -1
  66. package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts +0 -2
  67. package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts.map +1 -1
  68. package/esm/get-schema-from-api.d.ts +1 -1
  69. package/esm/get-schema-from-api.d.ts.map +1 -1
  70. package/esm/get-schema-from-api.spec.js.map +1 -1
  71. package/esm/helpers.d.ts +53 -87
  72. package/esm/helpers.d.ts.map +1 -1
  73. package/esm/helpers.js +91 -92
  74. package/esm/helpers.js.map +1 -1
  75. package/esm/helpers.spec.d.ts +2 -1
  76. package/esm/helpers.spec.d.ts.map +1 -1
  77. package/esm/helpers.spec.js +41 -38
  78. package/esm/helpers.spec.js.map +1 -1
  79. package/esm/http-authentication-settings.d.ts +35 -12
  80. package/esm/http-authentication-settings.d.ts.map +1 -1
  81. package/esm/http-authentication-settings.js +21 -29
  82. package/esm/http-authentication-settings.js.map +1 -1
  83. package/esm/http-server-pool.d.ts +64 -0
  84. package/esm/http-server-pool.d.ts.map +1 -0
  85. package/esm/http-server-pool.js +95 -0
  86. package/esm/http-server-pool.js.map +1 -0
  87. package/esm/http-server-pool.spec.d.ts +2 -0
  88. package/esm/http-server-pool.spec.d.ts.map +1 -0
  89. package/esm/http-server-pool.spec.js +193 -0
  90. package/esm/http-server-pool.spec.js.map +1 -0
  91. package/esm/http-user-context.d.ts +31 -44
  92. package/esm/http-user-context.d.ts.map +1 -1
  93. package/esm/http-user-context.js +76 -95
  94. package/esm/http-user-context.js.map +1 -1
  95. package/esm/http-user-context.spec.d.ts +8 -2
  96. package/esm/http-user-context.spec.d.ts.map +1 -1
  97. package/esm/http-user-context.spec.js +189 -337
  98. package/esm/http-user-context.spec.js.map +1 -1
  99. package/esm/index.d.ts +6 -4
  100. package/esm/index.d.ts.map +1 -1
  101. package/esm/index.js +6 -4
  102. package/esm/index.js.map +1 -1
  103. package/esm/login-response-strategy.d.ts +11 -10
  104. package/esm/login-response-strategy.d.ts.map +1 -1
  105. package/esm/login-response-strategy.js +7 -6
  106. package/esm/login-response-strategy.js.map +1 -1
  107. package/esm/login-response-strategy.spec.js +25 -29
  108. package/esm/login-response-strategy.spec.js.map +1 -1
  109. package/esm/openapi/generate-openapi-document.d.ts +0 -8
  110. package/esm/openapi/generate-openapi-document.d.ts.map +1 -1
  111. package/esm/openapi/generate-openapi-document.js +0 -2
  112. package/esm/openapi/generate-openapi-document.js.map +1 -1
  113. package/esm/proxy-manager.spec.js +152 -176
  114. package/esm/proxy-manager.spec.js.map +1 -1
  115. package/esm/proxy-runtime.d.ts +27 -0
  116. package/esm/proxy-runtime.d.ts.map +1 -0
  117. package/esm/proxy-runtime.js +45 -0
  118. package/esm/proxy-runtime.js.map +1 -0
  119. package/esm/proxy-runtime.spec.d.ts +2 -0
  120. package/esm/proxy-runtime.spec.d.ts.map +1 -0
  121. package/esm/proxy-runtime.spec.js +39 -0
  122. package/esm/proxy-runtime.spec.js.map +1 -0
  123. package/esm/rest-api-runtime.d.ts +76 -0
  124. package/esm/rest-api-runtime.d.ts.map +1 -0
  125. package/esm/rest-api-runtime.js +131 -0
  126. package/esm/rest-api-runtime.js.map +1 -0
  127. package/esm/rest-api-runtime.spec.d.ts +2 -0
  128. package/esm/rest-api-runtime.spec.d.ts.map +1 -0
  129. package/esm/rest-api-runtime.spec.js +58 -0
  130. package/esm/rest-api-runtime.spec.js.map +1 -0
  131. package/esm/rest-service.integration.spec.d.ts +1 -1
  132. package/esm/rest-service.integration.spec.d.ts.map +1 -1
  133. package/esm/rest-service.integration.spec.js +60 -82
  134. package/esm/rest-service.integration.spec.js.map +1 -1
  135. package/esm/rest.integration.test.js +26 -84
  136. package/esm/rest.integration.test.js.map +1 -1
  137. package/esm/server-telemetry.d.ts +57 -0
  138. package/esm/server-telemetry.d.ts.map +1 -0
  139. package/esm/server-telemetry.js +26 -0
  140. package/esm/server-telemetry.js.map +1 -0
  141. package/esm/server-telemetry.spec.d.ts +2 -0
  142. package/esm/server-telemetry.spec.d.ts.map +1 -0
  143. package/esm/server-telemetry.spec.js +52 -0
  144. package/esm/server-telemetry.spec.js.map +1 -0
  145. package/esm/static-server-manager.spec.js +60 -125
  146. package/esm/static-server-manager.spec.js.map +1 -1
  147. package/esm/static-site-runtime.d.ts +21 -0
  148. package/esm/static-site-runtime.d.ts.map +1 -0
  149. package/esm/static-site-runtime.js +48 -0
  150. package/esm/static-site-runtime.js.map +1 -0
  151. package/esm/static-site-runtime.spec.d.ts +2 -0
  152. package/esm/static-site-runtime.spec.d.ts.map +1 -0
  153. package/esm/static-site-runtime.spec.js +30 -0
  154. package/esm/static-site-runtime.spec.js.map +1 -0
  155. package/esm/user-store.d.ts +39 -0
  156. package/esm/user-store.d.ts.map +1 -0
  157. package/esm/user-store.js +62 -0
  158. package/esm/user-store.js.map +1 -0
  159. package/esm/user-store.spec.d.ts +2 -0
  160. package/esm/user-store.spec.d.ts.map +1 -0
  161. package/esm/user-store.spec.js +72 -0
  162. package/esm/user-store.spec.js.map +1 -0
  163. package/esm/validate.integration.spec.js +26 -83
  164. package/esm/validate.integration.spec.js.map +1 -1
  165. package/package.json +8 -8
  166. package/src/actions/get-current-user.spec.ts +11 -10
  167. package/src/actions/index.ts +0 -1
  168. package/src/actions/is-authenticated.spec.ts +12 -7
  169. package/src/actions/logout-action.spec.ts +12 -12
  170. package/src/actions/logout.ts +1 -1
  171. package/src/actions/password-login-action.spec.ts +30 -30
  172. package/src/actions/password-login-action.ts +1 -1
  173. package/src/authenticate.spec.ts +45 -51
  174. package/src/authenticate.ts +1 -1
  175. package/src/authorize.spec.ts +37 -39
  176. package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
  177. package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
  178. package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
  179. package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
  180. package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
  181. package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
  182. package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -38
  183. package/src/endpoint-generators/create-get-schema-action.ts +1 -1
  184. package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
  185. package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
  186. package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
  187. package/src/endpoint-generators/create-post-endpoint.ts +13 -23
  188. package/src/endpoint-generators/utils.ts +36 -28
  189. package/src/endpoint-generators/with-schema-and-openapi-action.ts +0 -3
  190. package/src/get-schema-from-api.spec.ts +1 -1
  191. package/src/get-schema-from-api.ts +1 -1
  192. package/src/helpers.spec.ts +51 -41
  193. package/src/helpers.ts +140 -109
  194. package/src/http-authentication-settings.ts +51 -25
  195. package/src/http-server-pool.spec.ts +220 -0
  196. package/src/http-server-pool.ts +169 -0
  197. package/src/http-user-context.spec.ts +203 -383
  198. package/src/http-user-context.ts +114 -91
  199. package/src/index.ts +6 -4
  200. package/src/login-response-strategy.spec.ts +32 -31
  201. package/src/login-response-strategy.ts +13 -12
  202. package/src/openapi/generate-openapi-document.ts +0 -3
  203. package/src/proxy-manager.spec.ts +152 -182
  204. package/src/proxy-runtime.spec.ts +50 -0
  205. package/src/proxy-runtime.ts +75 -0
  206. package/src/rest-api-runtime.spec.ts +77 -0
  207. package/src/rest-api-runtime.ts +204 -0
  208. package/src/rest-service.integration.spec.ts +68 -87
  209. package/src/rest.integration.test.ts +27 -87
  210. package/src/server-telemetry.spec.ts +56 -0
  211. package/src/server-telemetry.ts +51 -0
  212. package/src/static-server-manager.spec.ts +63 -151
  213. package/src/static-site-runtime.spec.ts +39 -0
  214. package/src/static-site-runtime.ts +75 -0
  215. package/src/user-store.spec.ts +86 -0
  216. package/src/user-store.ts +68 -0
  217. package/src/validate.integration.spec.ts +39 -97
  218. package/esm/actions/login-action.spec.d.ts +0 -2
  219. package/esm/actions/login-action.spec.d.ts.map +0 -1
  220. package/esm/actions/login-action.spec.js +0 -34
  221. package/esm/actions/login-action.spec.js.map +0 -1
  222. package/esm/actions/login.d.ts +0 -19
  223. package/esm/actions/login.d.ts.map +0 -1
  224. package/esm/actions/login.js +0 -27
  225. package/esm/actions/login.js.map +0 -1
  226. package/esm/api-manager.d.ts +0 -104
  227. package/esm/api-manager.d.ts.map +0 -1
  228. package/esm/api-manager.js +0 -180
  229. package/esm/api-manager.js.map +0 -1
  230. package/esm/proxy-manager.d.ts +0 -53
  231. package/esm/proxy-manager.d.ts.map +0 -1
  232. package/esm/proxy-manager.js +0 -84
  233. package/esm/proxy-manager.js.map +0 -1
  234. package/esm/server-manager.d.ts +0 -48
  235. package/esm/server-manager.d.ts.map +0 -1
  236. package/esm/server-manager.js +0 -100
  237. package/esm/server-manager.js.map +0 -1
  238. package/esm/static-server-manager.d.ts +0 -19
  239. package/esm/static-server-manager.d.ts.map +0 -1
  240. package/esm/static-server-manager.js +0 -65
  241. package/esm/static-server-manager.js.map +0 -1
  242. package/esm/swagger/generate-swagger-json.spec.d.ts +0 -2
  243. package/esm/swagger/generate-swagger-json.spec.d.ts.map +0 -1
  244. package/esm/swagger/generate-swagger-json.spec.js +0 -215
  245. package/esm/swagger/generate-swagger-json.spec.js.map +0 -1
  246. package/src/actions/login-action.spec.ts +0 -42
  247. package/src/actions/login.ts +0 -32
  248. package/src/api-manager.ts +0 -308
  249. package/src/proxy-manager.ts +0 -102
  250. package/src/server-manager.ts +0 -135
  251. package/src/static-server-manager.ts +0 -90
  252. package/src/swagger/generate-swagger-json.spec.ts +0 -250
@@ -1,42 +0,0 @@
1
- import { Injector } from '@furystack/inject'
2
- import { usingAsync } from '@furystack/utils'
3
- import type { IncomingMessage, ServerResponse } from 'http'
4
- import { describe, expect, it, vi } from 'vitest'
5
- import { HttpUserContext } from '../http-user-context.js'
6
- import { LoginAction } from './login.js'
7
-
8
- describe('LoginAction', () => {
9
- const request = { url: 'https://google.com' } as IncomingMessage
10
- const response = {} as ServerResponse
11
-
12
- it('Returns the provided user with 200 on success', async () => {
13
- const testUser = { Name: 'Userke' }
14
- await usingAsync(new Injector(), async (i) => {
15
- i.setExplicitInstance(
16
- {
17
- authenticateUser: vi.fn(async () => testUser),
18
- cookieLogin: vi.fn(async () => testUser),
19
- authentication: {},
20
- },
21
- HttpUserContext,
22
- )
23
- const result = await LoginAction({
24
- request,
25
- response,
26
- injector: i,
27
- getBody: async () => ({ username: 'testuser', password: 'alma' }),
28
- })
29
- expect(result.chunk).toEqual(testUser)
30
- expect(result.statusCode).toBe(200)
31
- })
32
- })
33
-
34
- it('Returns throw error with 400 on fail', async () => {
35
- await usingAsync(new Injector(), async (i) => {
36
- i.setExplicitInstance({ cookieLogin: async () => Promise.reject(new Error(':(')) }, HttpUserContext)
37
- await expect(
38
- LoginAction({ request, response, injector: i, getBody: async () => ({ username: '', password: '' }) }),
39
- ).rejects.toThrowError('Login Failed')
40
- })
41
- })
42
- })
@@ -1,32 +0,0 @@
1
- import type { User } from '@furystack/core'
2
- import { RequestError } from '@furystack/rest'
3
- import { sleepAsync } from '@furystack/utils'
4
-
5
- import { HttpUserContext } from '../http-user-context.js'
6
- import type { RequestAction } from '../request-action-implementation.js'
7
- import { JsonResult } from '../request-action-implementation.js'
8
-
9
- /**
10
- * Action that logs in the current user.
11
- * Should be called with a JSON POST body with `username` and `password` fields.
12
- * Returns the current user instance.
13
- *
14
- * @deprecated Use `createPasswordLoginAction(createCookieLoginStrategy(injector))` instead.
15
- * This static action resolves services from the request-scoped injector on
16
- * every call; the factory approach captures them once at setup time.
17
- */
18
- export const LoginAction: RequestAction<{
19
- result: User
20
- body: { username: string; password: string }
21
- }> = async ({ injector, getBody, response }) => {
22
- const userContext = injector.getInstance(HttpUserContext)
23
- const body = await getBody()
24
- try {
25
- const user = await userContext.authenticateUser(body.username, body.password)
26
- await userContext.cookieLogin(user, response)
27
- return JsonResult(user, 200)
28
- } catch {
29
- await sleepAsync(Math.random() * 1000)
30
- throw new RequestError('Login Failed', 400)
31
- }
32
- }
@@ -1,308 +0,0 @@
1
- import type { User } from '@furystack/core'
2
- import { IdentityContext } from '@furystack/core'
3
- import type { Injector } from '@furystack/inject'
4
- import { Injectable, Injected } from '@furystack/inject'
5
- import type { Method, RestApi } from '@furystack/rest'
6
- import { RequestError, deserializeQueryString } from '@furystack/rest'
7
- import { PathHelper, usingAsync } from '@furystack/utils'
8
- import type { MatchFunction } from 'path-to-regexp'
9
- import { match } from 'path-to-regexp'
10
- import { ErrorAction } from './actions/error-action.js'
11
- import { NotFoundAction } from './actions/not-found-action.js'
12
- import { addCorsHeaders } from './add-cors-header.js'
13
- import { CreateGetSchemaAction } from './endpoint-generators/create-get-schema-action.js'
14
- import {
15
- CreateGetOpenApiDocumentAction,
16
- CreateDeprecatedSwaggerRedirect,
17
- } from './endpoint-generators/create-get-openapi-document-action.js'
18
- import { HttpUserContext } from './http-user-context.js'
19
- import type { CorsOptions } from './models/cors-options.js'
20
- import { readPostBody } from './read-post-body.js'
21
- import type { RequestAction } from './request-action-implementation.js'
22
- import type { OnRequest, ServerApi } from './server-manager.js'
23
- import { ServerManager } from './server-manager.js'
24
- import './server-response-extensions.js'
25
-
26
- export type RestApiImplementation<T extends RestApi> = {
27
- [TMethod in keyof T]: {
28
- [TUrl in keyof T[TMethod]]: T[TMethod][TUrl] extends { result: unknown } ? RequestAction<T[TMethod][TUrl]> : never
29
- }
30
- }
31
-
32
- export interface ImplementApiOptions<T extends RestApi> {
33
- /**
34
- * The structure of the implemented API.
35
- */
36
- api: RestApiImplementation<T>
37
- /**
38
- * The Injector instance to use for dependency injection in the API actions.
39
- */
40
- injector: Injector
41
- /**
42
- * The host name for the API Server. If not provided, the default host (ServerManager.DEFAULT_HOST) will be used.
43
- */
44
- hostName?: string
45
- /**
46
- * The root path for the API. This will be prepended to all API paths.
47
- */
48
- root: string
49
- /**
50
- * The port on which the API server will listen.
51
- */
52
- port: number
53
- /**
54
- * CORS options to configure Cross-Origin Resource Sharing for the API.
55
- */
56
- cors?: CorsOptions
57
- /**
58
- * An optional function to deserialize query parameters from the URL.
59
- * This function should take a query string (e.g., "?key=value") and return an object with the parsed parameters.
60
- * If not provided, the default deserialization will be used.
61
- */
62
- deserializeQueryParams?: (param: string) => any
63
- /**
64
- * Adds an additional 'GET /schema' endpoint that returns the schema definitions of the API.
65
- * Also adds a 'GET /openapi.json' endpoint that returns the API schema in OpenAPI 3.1 format.
66
- */
67
- enableGetSchema?: boolean
68
-
69
- /**
70
- * Optional name for the API, used in the generated schema.
71
- * This can be useful for documentation or identification purposes.
72
- */
73
- name?: string
74
- /**
75
- * Optional description for the API, used in the generated schema.
76
- * This can provide additional context or information about the API's purpose.
77
- */
78
- description?: string
79
- /**
80
- * Optional version for the API, used in the generated schema.
81
- * This can help in versioning the API and tracking changes over time.
82
- */
83
- version?: string
84
- }
85
-
86
- export type NewCompiledApiEntry = {
87
- method: Method
88
- fullPath: string
89
- matcher: MatchFunction<Partial<Record<string, string | string[]>>>
90
- action: RequestAction<any>
91
- }
92
-
93
- export type NewCompiledApi = {
94
- [K in Method]?: NewCompiledApiEntry[]
95
- }
96
-
97
- export type OnRequestOptions = OnRequest & {
98
- compiledApi: NewCompiledApi
99
- hostName?: string
100
- port: number
101
- rootApiPath: string
102
- injector: Injector
103
- cors?: CorsOptions
104
- supportedMethods: string[]
105
- deserializeQueryParams?: (param: string) => Record<string, unknown>
106
- }
107
-
108
- @Injectable({ lifetime: 'singleton' })
109
- export class ApiManager {
110
- private getSuportedMethods(api: RestApiImplementation<RestApi>): Method[] {
111
- return Object.keys(api) as Method[]
112
- }
113
-
114
- private compileApi<T extends RestApi>(api: RestApiImplementation<T>, root: string) {
115
- const supportedMethods = this.getSuportedMethods(api)
116
-
117
- const compiledApi: NewCompiledApi = {}
118
- supportedMethods.forEach((method: Method) => {
119
- compiledApi[method] = [
120
- ...Object.entries(api[method as keyof typeof api]).map(([path, action]) => {
121
- const fullPath = `/${PathHelper.normalize(PathHelper.joinPaths(root, path))}`
122
- const matcher = match(fullPath, { decode: decodeURIComponent })
123
- return { method, fullPath, matcher, action: action as RequestAction<any> }
124
- }),
125
- ]
126
- })
127
- return compiledApi
128
- }
129
-
130
- public async addApi<T extends RestApi>({
131
- api,
132
- hostName,
133
- port,
134
- root,
135
- cors,
136
- injector,
137
- deserializeQueryParams,
138
- enableGetSchema,
139
- name,
140
- description,
141
- version,
142
- }: ImplementApiOptions<T>) {
143
- const extendedApi: typeof api = enableGetSchema
144
- ? {
145
- ...api,
146
- GET: {
147
- ...api.GET,
148
- '/schema': CreateGetSchemaAction(api, name, description, version),
149
- '/openapi.json': CreateGetOpenApiDocumentAction(api, name, description, version),
150
- '/swagger.json': CreateDeprecatedSwaggerRedirect(api, name, description, version),
151
- },
152
- }
153
- : api
154
-
155
- const supportedMethods = this.getSuportedMethods(extendedApi)
156
- const rootApiPath = PathHelper.normalize(root)
157
- const server = await this.serverManager.getOrCreate({ hostName, port })
158
- const compiledApi = this.compileApi(extendedApi, root)
159
- const serverApi = {
160
- shouldExec: (msg) =>
161
- this.shouldExecRequest({
162
- ...msg,
163
- method: msg.req.method?.toUpperCase() as Method,
164
- rootApiPath,
165
- supportedMethods,
166
- url: PathHelper.normalize(msg.req.url || ''),
167
- }),
168
- onRequest: (msg) =>
169
- this.onMessage({
170
- ...msg,
171
- compiledApi,
172
- rootApiPath,
173
- port,
174
- supportedMethods,
175
- cors,
176
- injector,
177
- hostName,
178
- deserializeQueryParams,
179
- }),
180
- } satisfies ServerApi
181
- server.apis.push(serverApi)
182
- return serverApi
183
- }
184
-
185
- public shouldExecRequest(options: {
186
- method?: Method
187
- url?: string
188
- rootApiPath: string
189
- supportedMethods: Method[]
190
- }): boolean {
191
- return options.method &&
192
- options.url &&
193
- (options.supportedMethods.includes(options.method) || options.method === 'OPTIONS') &&
194
- PathHelper.matchesBaseUrl(options.url, options.rootApiPath)
195
- ? true
196
- : false
197
- }
198
-
199
- private getActionFromEndpoint(compiledEndpoint: NewCompiledApi, fullUrl: URL, method: Method) {
200
- let resolvedParams: unknown
201
- const action = compiledEndpoint[method]?.find((route) => {
202
- const result = route.matcher(fullUrl.pathname)
203
- if (result) {
204
- resolvedParams = result.params
205
- }
206
- return result
207
- })
208
- return (
209
- action && {
210
- ...action,
211
- params: resolvedParams,
212
- }
213
- )
214
- }
215
-
216
- private async executeAction({
217
- injector,
218
- req,
219
- res,
220
- fullUrl,
221
- action,
222
- deserializeQueryParams,
223
- params,
224
- }: OnRequestOptions & {
225
- fullUrl: URL
226
- action: RequestAction<{ body: object; result: object; query: object; url: object; headers: object }>
227
- params: unknown
228
- }) {
229
- await usingAsync(injector.createChild({ owner: action }), async (i) => {
230
- const httpUserContext = i.getInstance(HttpUserContext)
231
- i.setExplicitInstance<IdentityContext>(
232
- {
233
- getCurrentUser: <TUser extends User>() => httpUserContext.getCurrentUser(req) as Promise<TUser>,
234
- isAuthorized: (...roles) => httpUserContext.isAuthorized(req, ...roles),
235
- isAuthenticated: () => httpUserContext.isAuthenticated(req),
236
- },
237
- IdentityContext,
238
- )
239
- try {
240
- const actionResult = await action({
241
- request: req,
242
- response: res,
243
- injector: i,
244
- getBody: () => readPostBody<any>(req),
245
- headers: req.headers,
246
- getQuery: () =>
247
- deserializeQueryParams ? deserializeQueryParams(fullUrl.search) : deserializeQueryString(fullUrl.search),
248
- getUrlParams: () => {
249
- return params as object
250
- },
251
- })
252
- res.sendActionResult(actionResult)
253
- } catch (error) {
254
- const errorActionResult = await ErrorAction({
255
- request: req,
256
- response: res,
257
- injector: i,
258
- getBody: async () => error,
259
- })
260
- res.sendActionResult(errorActionResult)
261
- }
262
- return
263
- })
264
- }
265
-
266
- private async onMessage(options: OnRequestOptions) {
267
- const protocol = 'http://'
268
- const host = `${options.hostName || ServerManager.DEFAULT_HOST}:${options.port}`
269
- const fullUrl = new URL(PathHelper.joinUrl(`${protocol}${host}`, options.req.url as string))
270
-
271
- if (options.cors) {
272
- addCorsHeaders(options.cors, options.req, options.res)
273
- }
274
-
275
- if (options.req.method === 'OPTIONS') {
276
- options.res.writeHead(200)
277
- options.res.end()
278
- return
279
- }
280
-
281
- let action: ReturnType<typeof this.getActionFromEndpoint>
282
- try {
283
- action = this.getActionFromEndpoint(options.compiledApi, fullUrl, options.req.method?.toUpperCase() as Method)
284
- } catch (error) {
285
- const responseError =
286
- error instanceof URIError ? new RequestError('Failed to decode URL path parameter', 400) : (error as Error)
287
- const errorActionResult = await ErrorAction({
288
- request: options.req,
289
- response: options.res,
290
- injector: options.injector,
291
- getBody: async () => responseError,
292
- })
293
- options.res.sendActionResult(errorActionResult)
294
- return
295
- }
296
-
297
- if (action) {
298
- await this.executeAction({ ...options, ...action, fullUrl })
299
- } else {
300
- options.res.sendActionResult(
301
- await NotFoundAction({ injector: options.injector, request: options.req, response: options.res }),
302
- )
303
- }
304
- }
305
-
306
- @Injected(ServerManager)
307
- declare private readonly serverManager: ServerManager
308
- }
@@ -1,102 +0,0 @@
1
- import { Injectable, Injected } from '@furystack/inject'
2
- import { EventHub, PathHelper, type ListenerErrorPayload } from '@furystack/utils'
3
- import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'http'
4
- import type { Duplex } from 'stream'
5
- import { HttpProxyHandler } from './http-proxy-handler.js'
6
- import { PathProcessor } from './path-processor.js'
7
- import { ServerManager } from './server-manager.js'
8
- import { WebSocketProxyHandler } from './websocket-proxy-handler.js'
9
-
10
- export interface ProxyOptions {
11
- sourceBaseUrl: string
12
- targetBaseUrl: string
13
- pathRewrite?: (sourcePath: string) => string
14
- sourceHostName?: string
15
- sourcePort: number
16
- headers?: (originalHeaders: OutgoingHttpHeaders) => OutgoingHttpHeaders
17
- cookies?: (originalCookies: string[]) => string[]
18
- responseCookies?: (responseCookies: string[]) => string[]
19
- timeout?: number
20
- enableWebsockets?: boolean
21
- }
22
-
23
- /**
24
- * Manages HTTP and WebSocket proxy configurations and routing
25
- */
26
- @Injectable({ lifetime: 'singleton' })
27
- export class ProxyManager extends EventHub<{
28
- onProxyFailed: { from: string; to: string; error: unknown }
29
- onWebSocketProxyFailed: { from: string; to: string; error: unknown }
30
- onListenerError: ListenerErrorPayload
31
- }> {
32
- @Injected(ServerManager)
33
- declare private readonly serverManager: ServerManager
34
-
35
- private readonly pathProcessor = new PathProcessor()
36
-
37
- /**
38
- * Creates a function that determines if a request should be handled by this proxy
39
- */
40
- public shouldExec =
41
- (sourceBaseUrl: string) =>
42
- ({ req }: { req: Pick<IncomingMessage, 'url' | 'method'> }) =>
43
- req.url ? PathHelper.matchesBaseUrl(req.url, sourceBaseUrl) : false
44
-
45
- /**
46
- * Creates an HTTP request handler for the proxy
47
- */
48
- private createRequestHandler(options: ProxyOptions) {
49
- const handler = new HttpProxyHandler({
50
- sourceBaseUrl: options.sourceBaseUrl,
51
- targetBaseUrl: options.targetBaseUrl,
52
- pathRewrite: options.pathRewrite,
53
- headers: options.headers,
54
- cookies: options.cookies,
55
- responseCookies: options.responseCookies,
56
- timeout: options.timeout,
57
- onError: (error) => this.emit('onProxyFailed', error),
58
- })
59
-
60
- return async ({ req, res }: { req: IncomingMessage; res: ServerResponse }) => {
61
- await handler.handle(req, res)
62
- }
63
- }
64
-
65
- /**
66
- * Creates a WebSocket upgrade handler for the proxy
67
- */
68
- private createUpgradeHandler(options: ProxyOptions) {
69
- const handler = new WebSocketProxyHandler({
70
- sourceBaseUrl: options.sourceBaseUrl,
71
- targetBaseUrl: options.targetBaseUrl,
72
- pathRewrite: options.pathRewrite,
73
- headers: options.headers,
74
- timeout: options.timeout,
75
- onError: (error) => this.emit('onWebSocketProxyFailed', error),
76
- })
77
-
78
- return async ({ req, socket, head }: { req: IncomingMessage; socket: Duplex; head: Buffer }) => {
79
- await handler.handle(req, socket, head)
80
- }
81
- }
82
-
83
- /**
84
- * Adds a new proxy configuration
85
- * @throws Error if targetBaseUrl is invalid or uses non-HTTP/HTTPS protocol
86
- */
87
- public async addProxy(options: ProxyOptions): Promise<void> {
88
- // Validate targetBaseUrl format
89
- const url = this.pathProcessor.validateUrl(options.targetBaseUrl, 'targetBaseUrl')
90
- this.pathProcessor.validateHttpProtocol(url)
91
-
92
- const server = await this.serverManager.getOrCreate({ hostName: options.sourceHostName, port: options.sourcePort })
93
-
94
- const api = {
95
- shouldExec: this.shouldExec(options.sourceBaseUrl),
96
- onRequest: this.createRequestHandler(options),
97
- ...(options.enableWebsockets ? { onUpgrade: this.createUpgradeHandler(options) } : {}),
98
- }
99
-
100
- server.apis.push(api)
101
- }
102
- }
@@ -1,135 +0,0 @@
1
- import { Injectable } from '@furystack/inject'
2
- import { EventHub, type ListenerErrorPayload } from '@furystack/utils'
3
- import type { IncomingMessage, Server, ServerResponse } from 'http'
4
- import { createServer } from 'http'
5
- import type { Socket } from 'net'
6
- import type { Duplex } from 'stream'
7
-
8
- export interface ServerOptions {
9
- hostName?: string
10
- port: number
11
- }
12
-
13
- export interface OnRequest {
14
- req: IncomingMessage
15
- res: ServerResponse
16
- }
17
-
18
- export interface OnUpgrade {
19
- req: IncomingMessage
20
- socket: Duplex
21
- head: Buffer
22
- }
23
-
24
- export interface ServerApi {
25
- shouldExec: (options: OnRequest) => boolean
26
- onRequest: (options: OnRequest) => Promise<void>
27
- onUpgrade?: (options: OnUpgrade) => Promise<void>
28
- }
29
-
30
- export interface ServerRecord {
31
- server: Server
32
- apis: ServerApi[]
33
- }
34
-
35
- @Injectable({ lifetime: 'singleton' })
36
- export class ServerManager
37
- extends EventHub<{
38
- onRequestFailed: [unknown, IncomingMessage, ServerResponse<IncomingMessage>]
39
- onServerListening: { url: string; port: number; hostName?: string }
40
- onServerClosed: { url: string }
41
- onListenerError: ListenerErrorPayload
42
- }>
43
- implements AsyncDisposable
44
- {
45
- public static DEFAULT_HOST = 'localhost'
46
- public servers = new Map<string, ServerRecord>()
47
- private openedSockets = new Set<Socket>()
48
- private readonly pendingCreates = new Map<string, Promise<ServerRecord>>()
49
- private getHostUrl = (options: ServerOptions) =>
50
- `http://${options.hostName || ServerManager.DEFAULT_HOST}:${options.port}`
51
-
52
- private onConnection = (socket: Socket) => {
53
- this.openedSockets.add(socket)
54
- socket.once('close', () => this.openedSockets.delete(socket))
55
- }
56
- public async [Symbol.asyncDispose]() {
57
- await Promise.allSettled([...this.pendingCreates.values()])
58
- this.openedSockets.forEach((s) => s.destroy())
59
- await Promise.allSettled(
60
- [...this.servers.entries()].map(
61
- ([url, s]) =>
62
- new Promise<void>((resolve, reject) => {
63
- s.server.close((err) => {
64
- if (err) {
65
- reject(err)
66
- } else {
67
- this.emit('onServerClosed', { url })
68
- resolve()
69
- }
70
- })
71
- s.server.off('connection', this.onConnection)
72
- }),
73
- ),
74
- )
75
- this.servers.clear()
76
- super[Symbol.dispose]?.()
77
- }
78
-
79
- public async getOrCreate(options: ServerOptions): Promise<ServerRecord> {
80
- const url = this.getHostUrl(options)
81
-
82
- const existing = this.servers.get(url)
83
- if (existing) {
84
- return existing
85
- }
86
-
87
- const pending = this.pendingCreates.get(url)
88
- if (pending) {
89
- return pending
90
- }
91
-
92
- const createPromise = this.createServer(url, options)
93
- this.pendingCreates.set(url, createPromise)
94
- return createPromise
95
- }
96
-
97
- private async createServer(url: string, options: ServerOptions): Promise<ServerRecord> {
98
- const apis: ServerRecord['apis'] = []
99
- const server = createServer((req, res) => {
100
- const apiMatch = apis.find((api) => api.shouldExec({ req, res }))
101
- if (apiMatch) {
102
- apiMatch.onRequest({ req, res }).catch((error) => {
103
- this.emit('onRequestFailed', [error, req, res])
104
- })
105
- } else {
106
- res.destroy()
107
- }
108
- })
109
- server.on('upgrade', (req, socket, head) => {
110
- const apiMatch = apis.find((api) => api.shouldExec({ req, res: {} as ServerResponse }))
111
- if (apiMatch?.onUpgrade) {
112
- apiMatch.onUpgrade({ req, socket, head }).catch((error) => {
113
- this.emit('onRequestFailed', [error, req, {} as ServerResponse])
114
- socket.destroy()
115
- })
116
- } else {
117
- socket.destroy()
118
- }
119
- })
120
- server.on('connection', this.onConnection)
121
- try {
122
- await new Promise<void>((resolve, reject) => {
123
- server.on('listening', () => resolve())
124
- server.on('error', (err) => reject(err))
125
- server.listen(options.port, options.hostName)
126
- })
127
- const record: ServerRecord = { server, apis }
128
- this.servers.set(url, record)
129
- this.emit('onServerListening', { url, port: options.port, hostName: options.hostName })
130
- return record
131
- } finally {
132
- this.pendingCreates.delete(url)
133
- }
134
- }
135
- }