@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,17 +1,17 @@
1
1
  import type { User } from '@furystack/core'
2
2
  import { useSystemIdentityContext } from '@furystack/core'
3
3
  import type { Injector } from '@furystack/inject'
4
- import { Injectable, Injected } from '@furystack/inject'
4
+ import { defineService, type Token } from '@furystack/inject'
5
5
  import { PasswordAuthenticator, UnauthenticatedError } from '@furystack/security'
6
6
  import { EventHub, type ListenerErrorPayload } from '@furystack/utils'
7
7
  import { randomBytes } from 'crypto'
8
8
  import type { IncomingMessage } from 'http'
9
9
  import { extractSessionIdFromCookies } from './authentication-providers/helpers.js'
10
10
  import { HttpAuthenticationSettings } from './http-authentication-settings.js'
11
- import type { DefaultSession } from './models/default-session.js'
12
11
 
13
12
  /**
14
- * Events emitted by the {@link HttpUserContext}
13
+ * Events emitted by a per-request {@link HttpUserContext} instance. Since
14
+ * the context is scoped, each request sees its own event stream.
15
15
  */
16
16
  export type HttpUserContextEvents = {
17
17
  onLogin: { user: User }
@@ -21,45 +21,77 @@ export type HttpUserContextEvents = {
21
21
  }
22
22
 
23
23
  /**
24
- * Injectable UserContext for FuryStack HTTP Api
24
+ * Request-scoped authentication/authorization facade.
25
+ *
26
+ * `HttpUserContext` consolidates identity lookup, authentication-provider
27
+ * walk-through and cookie session helpers behind a single object. Resolved
28
+ * users are memoized per `request.headers` via a {@link WeakMap} so repeated
29
+ * calls within the same request skip re-authentication — and so ancestor
30
+ * scopes caching the same `HttpUserContext` instance cannot leak an
31
+ * authenticated user into sibling requests.
25
32
  */
26
- @Injectable({ lifetime: 'scoped' })
27
- export class HttpUserContext extends EventHub<HttpUserContextEvents> {
28
- public getUserDataSet = () => this.authentication.getUserDataSet(this.systemInjector)
29
-
30
- public getSessionDataSet = () => this.authentication.getSessionDataSet(this.systemInjector)
31
-
32
- private getUserByName = async (userName: string) => {
33
- const userDataSet = this.getUserDataSet()
34
- const users = await userDataSet.find(this.systemInjector, { filter: { username: { $eq: userName } }, top: 2 })
35
- if (users.length !== 1) {
36
- throw new UnauthenticatedError()
37
- }
38
- return users[0]
39
- }
33
+ export interface HttpUserContext extends EventHub<HttpUserContextEvents> {
34
+ /** The active {@link HttpAuthenticationSettings} captured at resolve time. */
35
+ readonly authentication: HttpAuthenticationSettings
36
+ /** Whether the current user is authenticated according to the bound providers. */
37
+ isAuthenticated(request: Pick<IncomingMessage, 'headers'>): Promise<boolean>
38
+ /** Returns `true` when the current user has **all** of the supplied roles. */
39
+ isAuthorized(request: Pick<IncomingMessage, 'headers'>, ...roles: string[]): Promise<boolean>
40
+ /** Verifies a username/password pair against the system `PasswordAuthenticator`. */
41
+ authenticateUser(userName: string, password: string): Promise<User>
42
+ /** Returns the cached user for this request or resolves it via the provider chain. */
43
+ getCurrentUser(request: Pick<IncomingMessage, 'headers'>): Promise<User>
44
+ /** Extracts the session id for `request` using the configured cookie name. */
45
+ getSessionIdFromRequest(request: Pick<IncomingMessage, 'headers'>): string | null
46
+ /** Walks the configured authentication providers and returns the first match. */
47
+ authenticateRequest(request: Pick<IncomingMessage, 'headers'>): Promise<User>
48
+ /** Creates and persists a session for `user`, setting the cookie on `response`. */
49
+ cookieLogin(user: User, response: { setHeader: (header: string, value: string) => void }): Promise<User>
50
+ /** Clears the session identified by the request's cookie (if any). */
51
+ cookieLogout(
52
+ request: Pick<IncomingMessage, 'headers'>,
53
+ response: { setHeader: (header: string, value: string) => void },
54
+ ): Promise<void>
55
+ }
40
56
 
41
- private user?: User
57
+ /**
58
+ * Internal concrete EventHub-backed {@link HttpUserContext}. Exposed via
59
+ * the {@link HttpUserContextToken} token; constructed by its factory.
60
+ */
61
+ class HttpUserContextImpl extends EventHub<HttpUserContextEvents> implements HttpUserContext {
62
+ /**
63
+ * Per-request user cache keyed by the request's `headers` object identity.
64
+ * Each incoming HTTP/WS message allocates its own `headers` object, so
65
+ * entries here never leak between requests — even when this
66
+ * `HttpUserContext` instance happens to be cached by an ancestor scope
67
+ * (e.g. because setup code resolved it on the root injector before any
68
+ * per-request child scope existed).
69
+ */
70
+ private readonly userCache = new WeakMap<object, Promise<User>>()
42
71
 
43
72
  /**
44
- * @param request The request to be authenticated
45
- * @returns whether the current user is authenticated
73
+ * `authenticator` is resolved lazily so that tests (and applications) that
74
+ * never invoke `authenticateUser` don't need to bind password-credential
75
+ * stores.
46
76
  */
47
- public async isAuthenticated(request: IncomingMessage) {
77
+ constructor(
78
+ public readonly authentication: HttpAuthenticationSettings,
79
+ private readonly resolveAuthenticator: () => PasswordAuthenticator,
80
+ private readonly systemInjector: Injector,
81
+ ) {
82
+ super()
83
+ }
84
+
85
+ public async isAuthenticated(request: Pick<IncomingMessage, 'headers'>): Promise<boolean> {
48
86
  try {
49
87
  const currentUser = await this.getCurrentUser(request)
50
88
  return currentUser !== null
51
- } catch (error) {
89
+ } catch {
52
90
  return false
53
91
  }
54
92
  }
55
93
 
56
- /**
57
- * Returns whether the current user can be authorized with ALL of the specified roles
58
- * @param request The request to be authenticated
59
- * @param roles The list of roles to authorize
60
- * @returns a boolean value that indicates whether the user is authorized
61
- */
62
- public async isAuthorized(request: IncomingMessage, ...roles: string[]): Promise<boolean> {
94
+ public async isAuthorized(request: Pick<IncomingMessage, 'headers'>, ...roles: string[]): Promise<boolean> {
63
95
  const currentUser = await this.getCurrentUser(request)
64
96
  for (const role of roles) {
65
97
  if (!currentUser || !currentUser.roles.some((c) => c === role)) {
@@ -69,15 +101,8 @@ export class HttpUserContext extends EventHub<HttpUserContextEvents> {
69
101
  return true
70
102
  }
71
103
 
72
- /**
73
- * Checks if the system contains a user with the provided name and password, throws an error otherwise
74
- * @param userName The username
75
- * @param password The password
76
- * @returns the authenticated User
77
- */
78
- public async authenticateUser(userName: string, password: string) {
79
- const result = await this.authenticator.checkPasswordForUser(userName, password)
80
-
104
+ public async authenticateUser(userName: string, password: string): Promise<User> {
105
+ const result = await this.resolveAuthenticator().checkPasswordForUser(userName, password)
81
106
  if (!result.isValid) {
82
107
  throw new UnauthenticatedError()
83
108
  }
@@ -88,24 +113,26 @@ export class HttpUserContext extends EventHub<HttpUserContextEvents> {
88
113
  return user
89
114
  }
90
115
 
91
- public async getCurrentUser(request: Pick<IncomingMessage, 'headers'>) {
92
- if (!this.user) {
93
- this.user = await this.authenticateRequest(request)
94
- return this.user
116
+ public async getCurrentUser(request: Pick<IncomingMessage, 'headers'>): Promise<User> {
117
+ const key = request.headers
118
+ const cached = this.userCache.get(key)
119
+ if (cached) {
120
+ return cached
121
+ }
122
+ const pending = this.authenticateRequest(request)
123
+ this.userCache.set(key, pending)
124
+ try {
125
+ return await pending
126
+ } catch (error) {
127
+ this.userCache.delete(key)
128
+ throw error
95
129
  }
96
- return this.user
97
130
  }
98
131
 
99
132
  public getSessionIdFromRequest(request: Pick<IncomingMessage, 'headers'>): string | null {
100
133
  return extractSessionIdFromCookies(request, this.authentication.cookieName)
101
134
  }
102
135
 
103
- /**
104
- * Iterates registered authentication providers in order.
105
- * - A provider returning `User` means authentication succeeded.
106
- * - A provider returning `null` means it does not apply; try the next one.
107
- * - A provider throwing means it owns the request but auth failed; propagate the error.
108
- */
109
136
  public async authenticateRequest(request: Pick<IncomingMessage, 'headers'>): Promise<User> {
110
137
  for (const provider of this.authentication.authenticationProviders) {
111
138
  const user = await provider.authenticate(request)
@@ -114,20 +141,14 @@ export class HttpUserContext extends EventHub<HttpUserContextEvents> {
114
141
  throw new UnauthenticatedError()
115
142
  }
116
143
 
117
- /**
118
- * Creates and sets up a cookie-based session for the provided user
119
- * @param user The user to create a session for
120
- * @param serverResponse A serverResponse to set the cookie
121
- * @returns the current User
122
- */
123
144
  public async cookieLogin(
124
145
  user: User,
125
- serverResponse: { setHeader: (header: string, value: string) => void },
146
+ response: { setHeader: (header: string, value: string) => void },
126
147
  ): Promise<User> {
127
148
  const sessionId = randomBytes(32).toString('hex')
128
- await this.getSessionDataSet().add(this.systemInjector, { sessionId, username: user.username })
129
- serverResponse.setHeader('Set-Cookie', `${this.authentication.cookieName}=${sessionId}; Path=/; HttpOnly`)
130
- this.user = user
149
+ const sessionDataSet = this.systemInjector.get(this.authentication.sessionDataSet)
150
+ await sessionDataSet.add(this.systemInjector, { sessionId, username: user.username })
151
+ response.setHeader('Set-Cookie', `${this.authentication.cookieName}=${sessionId}; Path=/; HttpOnly`)
131
152
  this.emit('onLogin', { user })
132
153
  return user
133
154
  }
@@ -135,45 +156,47 @@ export class HttpUserContext extends EventHub<HttpUserContextEvents> {
135
156
  public async cookieLogout(
136
157
  request: Pick<IncomingMessage, 'headers'>,
137
158
  response: { setHeader: (header: string, value: string) => void },
138
- ) {
139
- this.user = undefined
159
+ ): Promise<void> {
160
+ this.userCache.delete(request.headers)
140
161
  const sessionId = this.getSessionIdFromRequest(request)
141
162
  response.setHeader('Set-Cookie', `${this.authentication.cookieName}=; Path=/; HttpOnly`)
142
163
 
143
164
  if (sessionId) {
144
- const sessionDataSet = this.getSessionDataSet()
145
- const sessions = await sessionDataSet.find(this.systemInjector, { filter: { sessionId: { $eq: sessionId } } })
146
- await sessionDataSet.remove(this.systemInjector, ...sessions.map((s) => s[sessionDataSet.primaryKey]))
165
+ const sessionDataSet = this.systemInjector.get(this.authentication.sessionDataSet)
166
+ const sessions = await sessionDataSet.find(this.systemInjector, {
167
+ filter: { sessionId: { $eq: sessionId } },
168
+ })
169
+ await sessionDataSet.remove(this.systemInjector, ...sessions.map((s) => s.sessionId))
147
170
  }
148
171
  this.emit('onLogout', undefined)
149
172
  }
150
173
 
151
- @Injected(HttpAuthenticationSettings)
152
- declare public readonly authentication: HttpAuthenticationSettings<User, DefaultSession>
153
-
154
- @Injected((injector: Injector) => useSystemIdentityContext({ injector, username: 'HttpUserContext' }))
155
- declare private readonly systemInjector: Injector
156
-
157
- @Injected(PasswordAuthenticator)
158
- declare private readonly authenticator: PasswordAuthenticator
159
-
160
- public init() {
161
- this.getUserDataSet().addListener('onEntityUpdated', ({ id, change }) => {
162
- if (this.user?.username === id) {
163
- this.user = { ...this.user, ...change }
164
- }
165
- })
166
-
167
- this.getUserDataSet().addListener('onEntityRemoved', ({ key }) => {
168
- if (this.user?.username === key) {
169
- this.user = undefined
170
- this.emit('onSessionInvalidated', undefined)
171
- }
172
- })
173
-
174
- this.getSessionDataSet().addListener('onEntityRemoved', () => {
175
- this.user = undefined
176
- this.emit('onSessionInvalidated', undefined)
174
+ private async getUserByName(userName: string): Promise<User> {
175
+ const userDataSet = this.systemInjector.get(this.authentication.userDataSet)
176
+ const users = await userDataSet.find(this.systemInjector, {
177
+ filter: { username: { $eq: userName } },
178
+ top: 2,
177
179
  })
180
+ if (users.length !== 1) {
181
+ throw new UnauthenticatedError()
182
+ }
183
+ return users[0]
178
184
  }
179
185
  }
186
+
187
+ /**
188
+ * DI token for the per-request {@link HttpUserContext}. Scoped — each HTTP
189
+ * request resolves its own instance with a fresh `user` cache.
190
+ */
191
+ export const HttpUserContext: Token<HttpUserContext, 'scoped'> = defineService({
192
+ name: 'furystack/rest-service/HttpUserContext',
193
+ lifetime: 'scoped',
194
+ factory: ({ inject, injector, onDispose }): HttpUserContext => {
195
+ const authentication = inject(HttpAuthenticationSettings)
196
+ const systemInjector = useSystemIdentityContext({ injector, username: 'HttpUserContext' })
197
+ onDispose(() => systemInjector[Symbol.asyncDispose]())
198
+ // Password authenticator resolution is deferred: tests that exercise only
199
+ // unauthenticated endpoints never have to bind credential stores.
200
+ return new HttpUserContextImpl(authentication, () => injector.get(PasswordAuthenticator), systemInjector)
201
+ },
202
+ })
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from './actions/index.js'
2
2
  export * from './add-cors-header.js'
3
- export * from './api-manager.js'
4
3
  export * from './authenticate.js'
5
4
  export * from './authentication-providers/index.js'
6
5
  export * from './authorize.js'
@@ -8,15 +7,18 @@ export * from './endpoint-generators/index.js'
8
7
  export * from './get-schema-from-api.js'
9
8
  export * from './helpers.js'
10
9
  export * from './http-authentication-settings.js'
10
+ export * from './http-server-pool.js'
11
11
  export * from './http-user-context.js'
12
12
  export * from './login-response-strategy.js'
13
13
  export * from './mime-types.js'
14
14
  export * from './models/index.js'
15
- export * from './proxy-manager.js'
15
+ export * from './proxy-runtime.js'
16
16
  export * from './read-post-body.js'
17
17
  export * from './request-action-implementation.js'
18
+ export * from './rest-api-runtime.js'
18
19
  export * from './schema-validator/index.js'
19
- export * from './server-manager.js'
20
20
  export * from './server-response-extensions.js'
21
- export * from './static-server-manager.js'
21
+ export * from './server-telemetry.js'
22
+ export * from './static-site-runtime.js'
23
+ export * from './user-store.js'
22
24
  export * from './validate.js'
@@ -1,25 +1,25 @@
1
- import { InMemoryStore, User, addStore } from '@furystack/core'
2
- import { Injector } from '@furystack/inject'
3
- import { getRepository } from '@furystack/repository'
4
- import { PasswordCredential, PasswordResetToken, usePasswordPolicy } from '@furystack/security'
1
+ import type { User } from '@furystack/core'
2
+ import { InMemoryStore, User as UserModel, useSystemIdentityContext } from '@furystack/core'
3
+ import { createInjector, type Injector } from '@furystack/inject'
4
+ import {
5
+ PasswordCredential,
6
+ PasswordCredentialStore,
7
+ PasswordResetToken,
8
+ PasswordResetTokenStore,
9
+ usePasswordPolicy,
10
+ } from '@furystack/security'
5
11
  import { usingAsync } from '@furystack/utils'
6
12
  import { describe, expect, it } from 'vitest'
7
-
8
13
  import { useHttpAuthentication } from './helpers.js'
9
14
  import { createCookieLoginStrategy } from './login-response-strategy.js'
10
15
  import { DefaultSession } from './models/default-session.js'
16
+ import { SessionDataSet, SessionStore, UserStore } from './user-store.js'
11
17
 
12
18
  const setupInjector = (i: Injector) => {
13
- addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' }))
14
- .addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
15
- .addStore(new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
16
- .addStore(new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
17
-
18
- const repo = getRepository(i)
19
- repo.createDataSet(User, 'username')
20
- repo.createDataSet(DefaultSession, 'sessionId')
21
- repo.createDataSet(PasswordCredential, 'userName')
22
- repo.createDataSet(PasswordResetToken, 'token')
19
+ i.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }))
20
+ i.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
21
+ i.bind(PasswordCredentialStore, () => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
22
+ i.bind(PasswordResetTokenStore, () => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
23
23
 
24
24
  usePasswordPolicy(i)
25
25
  useHttpAuthentication(i)
@@ -28,8 +28,8 @@ const setupInjector = (i: Injector) => {
28
28
  describe('createCookieLoginStrategy', () => {
29
29
  const testUser: User = { username: 'testuser', roles: ['admin'] }
30
30
 
31
- it('Should return the user as the response body', async () => {
32
- await usingAsync(new Injector(), async (i) => {
31
+ it('returns the user as the response body', async () => {
32
+ await usingAsync(createInjector(), async (i) => {
33
33
  setupInjector(i)
34
34
  const strategy = createCookieLoginStrategy(i)
35
35
  const result = await strategy.createLoginResponse(testUser, i)
@@ -37,8 +37,8 @@ describe('createCookieLoginStrategy', () => {
37
37
  })
38
38
  })
39
39
 
40
- it('Should return status code 200', async () => {
41
- await usingAsync(new Injector(), async (i) => {
40
+ it('returns status code 200', async () => {
41
+ await usingAsync(createInjector(), async (i) => {
42
42
  setupInjector(i)
43
43
  const strategy = createCookieLoginStrategy(i)
44
44
  const result = await strategy.createLoginResponse(testUser, i)
@@ -46,8 +46,8 @@ describe('createCookieLoginStrategy', () => {
46
46
  })
47
47
  })
48
48
 
49
- it('Should include a Set-Cookie header with the session ID', async () => {
50
- await usingAsync(new Injector(), async (i) => {
49
+ it('emits a Set-Cookie header with the generated session id', async () => {
50
+ await usingAsync(createInjector(), async (i) => {
51
51
  setupInjector(i)
52
52
  const strategy = createCookieLoginStrategy(i)
53
53
  const result = await strategy.createLoginResponse(testUser, i)
@@ -59,23 +59,24 @@ describe('createCookieLoginStrategy', () => {
59
59
  })
60
60
  })
61
61
 
62
- it('Should persist the session in the DataSet', async () => {
63
- await usingAsync(new Injector(), async (i) => {
62
+ it('persists the session via SessionDataSet', async () => {
63
+ await usingAsync(createInjector(), async (i) => {
64
64
  setupInjector(i)
65
65
  const strategy = createCookieLoginStrategy(i)
66
66
  await strategy.createLoginResponse(testUser, i)
67
67
 
68
- const repo = getRepository(i)
69
- const sessionDataSet = repo.getDataSetFor(DefaultSession, 'sessionId')
70
- const sessions = await sessionDataSet.find(i, { filter: { username: { $eq: 'testuser' } } })
71
- expect(sessions).toHaveLength(1)
72
- expect(sessions[0].username).toBe('testuser')
73
- expect(sessions[0].sessionId).toBeTruthy()
68
+ await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
69
+ const sessionDataSet = systemScope.get(SessionDataSet)
70
+ const sessions = await sessionDataSet.find(systemScope, { filter: { username: { $eq: 'testuser' } } })
71
+ expect(sessions).toHaveLength(1)
72
+ expect(sessions[0].username).toBe('testuser')
73
+ expect(sessions[0].sessionId).toBeTruthy()
74
+ })
74
75
  })
75
76
  })
76
77
 
77
- it('Should create unique session IDs for each call', async () => {
78
- await usingAsync(new Injector(), async (i) => {
78
+ it('creates unique session ids for each call', async () => {
79
+ await usingAsync(createInjector(), async (i) => {
79
80
  setupInjector(i)
80
81
  const strategy = createCookieLoginStrategy(i)
81
82
 
@@ -9,14 +9,14 @@ import { JsonResult } from './request-action-implementation.js'
9
9
 
10
10
  /**
11
11
  * A pluggable strategy that turns an authenticated {@link User} into an
12
- * {@link ActionResult} containing the session/token data for the client.
12
+ * {@link ActionResult} carrying the session/token payload for the client.
13
13
  *
14
- * Pass a concrete strategy to action factories like
15
- * {@link createPasswordLoginAction} or `createGoogleLoginAction` to
16
- * decouple the authentication mechanism from the session/token mechanism.
14
+ * Pass a concrete strategy to action factories such as
15
+ * {@link createPasswordLoginAction} or `createGoogleLoginAction` to decouple
16
+ * the authentication mechanism from the session/token mechanism.
17
17
  *
18
- * @typeParam TResult The shape of the response body (e.g. `User` for cookies,
19
- * `{ accessToken: string; refreshToken: string }` for JWT)
18
+ * @typeParam TResult The shape of the response body — e.g. `User` for
19
+ * cookies, `{ accessToken, refreshToken }` for JWT.
20
20
  */
21
21
  export type LoginResponseStrategy<TResult> = {
22
22
  createLoginResponse: (user: User, injector: Injector) => Promise<ActionResult<TResult>>
@@ -25,19 +25,20 @@ export type LoginResponseStrategy<TResult> = {
25
25
  /**
26
26
  * Creates a cookie-based {@link LoginResponseStrategy}.
27
27
  *
28
- * On each login it generates a random session ID, persists it in the session
29
- * DataSet, and returns the user with a `Set-Cookie` header.
28
+ * On each login it generates a random session id, persists it via the
29
+ * configured session DataSet and returns the user with a `Set-Cookie`
30
+ * response header.
30
31
  *
31
- * @param injector The root injector (must have {@link HttpAuthenticationSettings} configured)
32
- * @returns A strategy that returns `ActionResult<User>`
32
+ * @param injector - The injector scope (typically the root) that carries
33
+ * `HttpAuthenticationSettings` and a persistent session store.
33
34
  */
34
35
  export const createCookieLoginStrategy = (injector: Injector): LoginResponseStrategy<User> => {
35
- const settings = injector.getInstance(HttpAuthenticationSettings)
36
+ const settings = injector.get(HttpAuthenticationSettings)
36
37
  const systemInjector = useSystemIdentityContext({ injector, username: 'CookieLoginStrategy' })
38
+ const sessionDataSet = systemInjector.get(settings.sessionDataSet)
37
39
 
38
40
  return {
39
41
  createLoginResponse: async (user) => {
40
- const sessionDataSet = settings.getSessionDataSet(systemInjector)
41
42
  const sessionId = randomBytes(32).toString('hex')
42
43
  await sessionDataSet.add(systemInjector, { sessionId, username: user.username })
43
44
  return JsonResult(user, 200, {
@@ -193,6 +193,3 @@ export const generateOpenApiDocument = ({
193
193
 
194
194
  return doc
195
195
  }
196
-
197
- /** @deprecated Use generateOpenApiDocument instead */
198
- export const generateSwaggerJsonFromApiSchema = generateOpenApiDocument