@furystack/rest-service 13.0.0 → 14.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.
- package/CHANGELOG.md +124 -0
- package/README.md +47 -28
- package/esm/actions/error-action.d.ts +7 -2
- package/esm/actions/error-action.d.ts.map +1 -1
- package/esm/actions/error-action.js +7 -2
- package/esm/actions/error-action.js.map +1 -1
- package/esm/actions/get-current-user.d.ts +1 -4
- package/esm/actions/get-current-user.d.ts.map +1 -1
- package/esm/actions/get-current-user.js +1 -4
- package/esm/actions/get-current-user.js.map +1 -1
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/is-authenticated.d.ts +3 -4
- package/esm/actions/is-authenticated.d.ts.map +1 -1
- package/esm/actions/is-authenticated.js +3 -4
- package/esm/actions/is-authenticated.js.map +1 -1
- package/esm/actions/is-authenticated.spec.js +10 -6
- package/esm/actions/is-authenticated.spec.js.map +1 -1
- package/esm/actions/logout-action.spec.js +11 -9
- package/esm/actions/logout-action.spec.js.map +1 -1
- package/esm/actions/logout.d.ts +2 -6
- package/esm/actions/logout.d.ts.map +1 -1
- package/esm/actions/logout.js +3 -7
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/not-found-action.d.ts +1 -3
- package/esm/actions/not-found-action.d.ts.map +1 -1
- package/esm/actions/not-found-action.js +1 -3
- package/esm/actions/not-found-action.js.map +1 -1
- package/esm/actions/password-login-action.d.ts +5 -12
- package/esm/actions/password-login-action.d.ts.map +1 -1
- package/esm/actions/password-login-action.js +6 -13
- package/esm/actions/password-login-action.js.map +1 -1
- package/esm/actions/password-login-action.spec.js +23 -28
- package/esm/actions/password-login-action.spec.js.map +1 -1
- package/esm/authenticate.d.ts +5 -11
- package/esm/authenticate.d.ts.map +1 -1
- package/esm/authenticate.js +6 -12
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authentication-providers/authentication-provider.d.ts +17 -0
- package/esm/authentication-providers/authentication-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.d.ts +7 -0
- package/esm/authentication-providers/basic-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.js +7 -0
- package/esm/authentication-providers/basic-auth-provider.js.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.js +8 -0
- package/esm/authentication-providers/cookie-auth-provider.js.map +1 -1
- package/esm/authorize.d.ts +5 -17
- package/esm/authorize.d.ts.map +1 -1
- package/esm/authorize.js +5 -17
- package/esm/authorize.js.map +1 -1
- package/esm/authorize.spec.js +34 -29
- package/esm/authorize.spec.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
- package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
- package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
- package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
- package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
- package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.js +8 -13
- package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
- package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/utils.d.ts +17 -2
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +33 -20
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/get-schema-from-api.d.ts +1 -1
- package/esm/get-schema-from-api.d.ts.map +1 -1
- package/esm/get-schema-from-api.js +1 -0
- package/esm/get-schema-from-api.js.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +54 -88
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +90 -92
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.d.ts +2 -1
- package/esm/helpers.spec.d.ts.map +1 -1
- package/esm/helpers.spec.js +41 -38
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +56 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +27 -29
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-server-pool.d.ts +64 -0
- package/esm/http-server-pool.d.ts.map +1 -0
- package/esm/http-server-pool.js +95 -0
- package/esm/http-server-pool.js.map +1 -0
- package/esm/http-server-pool.spec.d.ts +2 -0
- package/esm/http-server-pool.spec.d.ts.map +1 -0
- package/esm/http-server-pool.spec.js +193 -0
- package/esm/http-server-pool.spec.js.map +1 -0
- package/esm/http-user-context.d.ts +34 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +75 -95
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.d.ts +8 -2
- package/esm/http-user-context.spec.d.ts.map +1 -1
- package/esm/http-user-context.spec.js +226 -332
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/identity-cache-keys.d.ts +28 -0
- package/esm/identity-cache-keys.d.ts.map +1 -0
- package/esm/identity-cache-keys.js +13 -0
- package/esm/identity-cache-keys.js.map +1 -0
- package/esm/identity-cache-keys.spec.d.ts +2 -0
- package/esm/identity-cache-keys.spec.d.ts.map +1 -0
- package/esm/identity-cache-keys.spec.js +21 -0
- package/esm/identity-cache-keys.spec.js.map +1 -0
- package/esm/identity-event-bus.d.ts +102 -0
- package/esm/identity-event-bus.d.ts.map +1 -0
- package/esm/identity-event-bus.js +151 -0
- package/esm/identity-event-bus.js.map +1 -0
- package/esm/identity-event-bus.spec.d.ts +2 -0
- package/esm/identity-event-bus.spec.d.ts.map +1 -0
- package/esm/identity-event-bus.spec.js +392 -0
- package/esm/identity-event-bus.spec.js.map +1 -0
- package/esm/index.d.ts +9 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +9 -4
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +11 -10
- package/esm/login-response-strategy.d.ts.map +1 -1
- package/esm/login-response-strategy.js +7 -6
- package/esm/login-response-strategy.js.map +1 -1
- package/esm/login-response-strategy.spec.js +25 -29
- package/esm/login-response-strategy.spec.js.map +1 -1
- package/esm/openapi/generate-openapi-document.spec.js.map +1 -1
- package/esm/proxy-manager.spec.js +152 -176
- package/esm/proxy-manager.spec.js.map +1 -1
- package/esm/proxy-runtime.d.ts +27 -0
- package/esm/proxy-runtime.d.ts.map +1 -0
- package/esm/proxy-runtime.js +45 -0
- package/esm/proxy-runtime.js.map +1 -0
- package/esm/proxy-runtime.spec.d.ts +2 -0
- package/esm/proxy-runtime.spec.d.ts.map +1 -0
- package/esm/proxy-runtime.spec.js +39 -0
- package/esm/proxy-runtime.spec.js.map +1 -0
- package/esm/request-action-implementation.d.ts +25 -0
- package/esm/request-action-implementation.d.ts.map +1 -1
- package/esm/request-action-implementation.js +9 -0
- package/esm/request-action-implementation.js.map +1 -1
- package/esm/rest-api-runtime.d.ts +76 -0
- package/esm/rest-api-runtime.d.ts.map +1 -0
- package/esm/rest-api-runtime.js +131 -0
- package/esm/rest-api-runtime.js.map +1 -0
- package/esm/rest-api-runtime.spec.d.ts +2 -0
- package/esm/rest-api-runtime.spec.d.ts.map +1 -0
- package/esm/rest-api-runtime.spec.js +58 -0
- package/esm/rest-api-runtime.spec.js.map +1 -0
- package/esm/rest-service.integration.spec.d.ts +1 -1
- package/esm/rest-service.integration.spec.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +57 -80
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/rest.integration.test.js +26 -84
- package/esm/rest.integration.test.js.map +1 -1
- package/esm/server-telemetry.d.ts +49 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +25 -0
- package/esm/server-telemetry.js.map +1 -0
- package/esm/server-telemetry.spec.d.ts +2 -0
- package/esm/server-telemetry.spec.d.ts.map +1 -0
- package/esm/server-telemetry.spec.js +52 -0
- package/esm/server-telemetry.spec.js.map +1 -0
- package/esm/static-server-manager.spec.js +60 -125
- package/esm/static-server-manager.spec.js.map +1 -1
- package/esm/static-site-runtime.d.ts +21 -0
- package/esm/static-site-runtime.d.ts.map +1 -0
- package/esm/static-site-runtime.js +48 -0
- package/esm/static-site-runtime.js.map +1 -0
- package/esm/static-site-runtime.spec.d.ts +2 -0
- package/esm/static-site-runtime.spec.d.ts.map +1 -0
- package/esm/static-site-runtime.spec.js +30 -0
- package/esm/static-site-runtime.spec.js.map +1 -0
- package/esm/user-resolution-cache.d.ts +65 -0
- package/esm/user-resolution-cache.d.ts.map +1 -0
- package/esm/user-resolution-cache.js +101 -0
- package/esm/user-resolution-cache.js.map +1 -0
- package/esm/user-resolution-cache.spec.d.ts +2 -0
- package/esm/user-resolution-cache.spec.d.ts.map +1 -0
- package/esm/user-resolution-cache.spec.js +220 -0
- package/esm/user-resolution-cache.spec.js.map +1 -0
- package/esm/user-store.d.ts +39 -0
- package/esm/user-store.d.ts.map +1 -0
- package/esm/user-store.js +62 -0
- package/esm/user-store.js.map +1 -0
- package/esm/user-store.spec.d.ts +2 -0
- package/esm/user-store.spec.d.ts.map +1 -0
- package/esm/user-store.spec.js +72 -0
- package/esm/user-store.spec.js.map +1 -0
- package/esm/validate.d.ts +13 -12
- package/esm/validate.d.ts.map +1 -1
- package/esm/validate.integration.spec.js +27 -28
- package/esm/validate.integration.spec.js.map +1 -1
- package/esm/validate.js +12 -0
- package/esm/validate.js.map +1 -1
- package/package.json +14 -12
- package/src/actions/error-action.ts +7 -3
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/get-current-user.ts +1 -4
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/is-authenticated.ts +3 -4
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +3 -7
- package/src/actions/not-found-action.ts +1 -3
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +6 -13
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +6 -12
- package/src/authentication-providers/authentication-provider.ts +17 -0
- package/src/authentication-providers/basic-auth-provider.ts +7 -0
- package/src/authentication-providers/cookie-auth-provider.ts +8 -0
- package/src/authorize.spec.ts +37 -39
- package/src/authorize.ts +6 -17
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
- package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
- package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
- package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -2
- package/src/endpoint-generators/create-get-schema-action.ts +1 -1
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
- package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
- package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
- package/src/endpoint-generators/create-post-endpoint.ts +13 -23
- package/src/endpoint-generators/utils.ts +36 -28
- package/src/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +2 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +78 -24
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +246 -378
- package/src/http-user-context.ts +122 -92
- package/src/identity-cache-keys.spec.ts +24 -0
- package/src/identity-cache-keys.ts +31 -0
- package/src/identity-event-bus.spec.ts +285 -0
- package/src/identity-event-bus.ts +230 -0
- package/src/index.ts +9 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +14 -12
- package/src/openapi/generate-openapi-document.spec.ts +2 -2
- package/src/proxy-manager.spec.ts +158 -188
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/request-action-implementation.ts +25 -0
- package/src/rest-api-runtime.spec.ts +77 -0
- package/src/rest-api-runtime.ts +204 -0
- package/src/rest-service.integration.spec.ts +65 -85
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +43 -0
- package/src/static-server-manager.spec.ts +63 -151
- package/src/static-site-runtime.spec.ts +39 -0
- package/src/static-site-runtime.ts +75 -0
- package/src/user-resolution-cache.spec.ts +241 -0
- package/src/user-resolution-cache.ts +164 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +69 -0
- package/src/validate.integration.spec.ts +40 -35
- package/src/validate.ts +14 -12
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -179
- package/esm/api-manager.js.map +0 -1
- package/esm/proxy-manager.d.ts +0 -53
- package/esm/proxy-manager.d.ts.map +0 -1
- package/esm/proxy-manager.js +0 -84
- package/esm/proxy-manager.js.map +0 -1
- package/esm/server-manager.d.ts +0 -48
- package/esm/server-manager.d.ts.map +0 -1
- package/esm/server-manager.js +0 -100
- package/esm/server-manager.js.map +0 -1
- package/esm/static-server-manager.d.ts +0 -19
- package/esm/static-server-manager.d.ts.map +0 -1
- package/esm/static-server-manager.js +0 -65
- package/esm/static-server-manager.js.map +0 -1
- package/src/api-manager.ts +0 -304
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- package/src/static-server-manager.ts +0 -90
package/src/helpers.ts
CHANGED
|
@@ -1,55 +1,138 @@
|
|
|
1
|
-
import type { User } from '@furystack/core'
|
|
2
1
|
import { useSystemIdentityContext } from '@furystack/core'
|
|
3
2
|
import type { Injector } from '@furystack/inject'
|
|
4
3
|
import type { RestApi } from '@furystack/rest'
|
|
5
|
-
import type { DataSet } from '@furystack/repository'
|
|
6
4
|
import { PasswordAuthenticator } from '@furystack/security'
|
|
7
|
-
|
|
8
|
-
import type { ImplementApiOptions } from './api-manager.js'
|
|
9
|
-
import { ApiManager } from './api-manager.js'
|
|
5
|
+
import { PathHelper } from '@furystack/utils'
|
|
10
6
|
import type { AuthenticationProvider } from './authentication-providers/authentication-provider.js'
|
|
11
7
|
import { createBasicAuthProvider } from './authentication-providers/basic-auth-provider.js'
|
|
12
8
|
import { createCookieAuthProvider } from './authentication-providers/cookie-auth-provider.js'
|
|
13
9
|
import { authenticateUserWithDataSet, findSessionById, findUserByName } from './authentication-providers/helpers.js'
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
10
|
+
import { CreateGetOpenApiDocumentAction } from './endpoint-generators/create-get-openapi-document-action.js'
|
|
11
|
+
import { CreateGetSchemaAction } from './endpoint-generators/create-get-schema-action.js'
|
|
12
|
+
import { defaultHttpAuthenticationSettings, HttpAuthenticationSettings } from './http-authentication-settings.js'
|
|
13
|
+
import { HttpServerPoolToken, type ServerApi } from './http-server-pool.js'
|
|
14
|
+
import { HttpUserContext } from './http-user-context.js'
|
|
15
|
+
import type { CorsOptions } from './models/cors-options.js'
|
|
16
|
+
import { buildProxyServerApi, type ProxyOptions } from './proxy-runtime.js'
|
|
17
|
+
import { compileApi, onRestApiMessage, type RestApiImplementation, shouldExecRequest } from './rest-api-runtime.js'
|
|
18
|
+
import { ServerTelemetryToken } from './server-telemetry.js'
|
|
19
|
+
import { buildStaticSiteServerApi, type StaticServerOptions } from './static-site-runtime.js'
|
|
20
|
+
import type { SessionStore, UserStore } from './user-store.js'
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Options accepted by {@link useRestService}. Cross-cutting concerns
|
|
24
|
+
* (authentication, telemetry) have moved to DI tokens — they are not
|
|
25
|
+
* configured here.
|
|
25
26
|
*/
|
|
26
|
-
export
|
|
27
|
-
|
|
27
|
+
export interface ImplementApiOptions<T extends RestApi> {
|
|
28
|
+
/** The structure of the implemented API. */
|
|
29
|
+
api: RestApiImplementation<T>
|
|
30
|
+
/** Injector used to resolve request-scoped services and to acquire the HTTP server. */
|
|
31
|
+
injector: Injector
|
|
32
|
+
/** Optional host name; defaults to `localhost`. */
|
|
33
|
+
hostName?: string
|
|
34
|
+
/** Root path prepended to every API route. */
|
|
35
|
+
root: string
|
|
36
|
+
/** Port the API should listen on. */
|
|
37
|
+
port: number
|
|
38
|
+
/** Optional CORS configuration applied to every matched request. */
|
|
39
|
+
cors?: CorsOptions
|
|
40
|
+
/** Optional query-string deserialiser override. */
|
|
41
|
+
deserializeQueryParams?: (param: string) => Record<string, unknown>
|
|
42
|
+
/** When `true`, adds `GET /schema` and `GET /openapi.json` endpoints. */
|
|
43
|
+
enableGetSchema?: boolean
|
|
44
|
+
/** OpenAPI display name. */
|
|
45
|
+
name?: string
|
|
46
|
+
/** OpenAPI description. */
|
|
47
|
+
description?: string
|
|
48
|
+
/** OpenAPI version. */
|
|
49
|
+
version?: string
|
|
50
|
+
}
|
|
28
51
|
|
|
29
52
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
53
|
+
* Acquires a pooled HTTP server (creating one if needed), compiles `api`
|
|
54
|
+
* and attaches a {@link ServerApi} that dispatches matching requests.
|
|
55
|
+
* Multiple `useRestService` calls on the same `port` + `hostName` share a
|
|
56
|
+
* single underlying HTTP server.
|
|
57
|
+
*/
|
|
58
|
+
export const useRestService = async <T extends RestApi>(options: ImplementApiOptions<T>): Promise<ServerApi> => {
|
|
59
|
+
const {
|
|
60
|
+
injector,
|
|
61
|
+
api,
|
|
62
|
+
hostName,
|
|
63
|
+
port,
|
|
64
|
+
root,
|
|
65
|
+
cors,
|
|
66
|
+
deserializeQueryParams,
|
|
67
|
+
enableGetSchema,
|
|
68
|
+
name,
|
|
69
|
+
description,
|
|
70
|
+
version,
|
|
71
|
+
} = options
|
|
72
|
+
|
|
73
|
+
const extendedApi: typeof api = enableGetSchema
|
|
74
|
+
? {
|
|
75
|
+
...api,
|
|
76
|
+
GET: {
|
|
77
|
+
...api.GET,
|
|
78
|
+
'/schema': CreateGetSchemaAction(api, name, description, version),
|
|
79
|
+
'/openapi.json': CreateGetOpenApiDocumentAction(api, name, description, version),
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
: api
|
|
83
|
+
|
|
84
|
+
const supportedMethods = Object.keys(extendedApi)
|
|
85
|
+
const rootApiPath = PathHelper.normalize(root)
|
|
86
|
+
const compiledApi = compileApi(extendedApi, root)
|
|
87
|
+
|
|
88
|
+
const serverApi: ServerApi = {
|
|
89
|
+
shouldExec: ({ req }) =>
|
|
90
|
+
shouldExecRequest({
|
|
91
|
+
method: req.method?.toUpperCase() as never,
|
|
92
|
+
url: PathHelper.normalize(req.url || ''),
|
|
93
|
+
rootApiPath,
|
|
94
|
+
supportedMethods,
|
|
95
|
+
}),
|
|
96
|
+
onRequest: (msg) =>
|
|
97
|
+
onRestApiMessage({
|
|
98
|
+
...msg,
|
|
99
|
+
compiledApi,
|
|
100
|
+
rootApiPath,
|
|
101
|
+
port,
|
|
102
|
+
supportedMethods,
|
|
103
|
+
cors,
|
|
104
|
+
injector,
|
|
105
|
+
hostName,
|
|
106
|
+
deserializeQueryParams,
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
111
|
+
const record = await pool.acquire({ port, hostName })
|
|
112
|
+
record.apis.push(serverApi)
|
|
113
|
+
return serverApi
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Installs HTTP authentication on the given injector: builds the provider
|
|
118
|
+
* chain (Basic + Cookie by default), appends any caller-provided providers
|
|
119
|
+
* and rebinds {@link HttpAuthenticationSettings}.
|
|
34
120
|
*
|
|
35
|
-
* @
|
|
36
|
-
*
|
|
121
|
+
* Prerequisite: {@link UserStore} and {@link SessionStore} must be bound to
|
|
122
|
+
* persistent implementations before calling this helper, since the Basic
|
|
123
|
+
* and Cookie providers need to read users and sessions immediately.
|
|
37
124
|
*/
|
|
38
|
-
export const useHttpAuthentication =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
125
|
+
export const useHttpAuthentication = (injector: Injector, overrides?: Partial<HttpAuthenticationSettings>): void => {
|
|
126
|
+
const mergedSettings: HttpAuthenticationSettings = {
|
|
127
|
+
...defaultHttpAuthenticationSettings(),
|
|
128
|
+
...overrides,
|
|
129
|
+
}
|
|
43
130
|
const systemInjector = useSystemIdentityContext({ injector, username: 'useHttpAuthentication' })
|
|
44
|
-
const passwordAuthenticator = injector.
|
|
45
|
-
const userDataSet = mergedSettings.
|
|
46
|
-
const sessionDataSet = mergedSettings.
|
|
47
|
-
DefaultSession,
|
|
48
|
-
'sessionId'
|
|
49
|
-
>
|
|
131
|
+
const passwordAuthenticator = injector.get(PasswordAuthenticator)
|
|
132
|
+
const userDataSet = systemInjector.get(mergedSettings.userDataSet)
|
|
133
|
+
const sessionDataSet = systemInjector.get(mergedSettings.sessionDataSet)
|
|
50
134
|
|
|
51
135
|
const providers: AuthenticationProvider[] = []
|
|
52
|
-
|
|
53
136
|
if (mergedSettings.enableBasicAuth) {
|
|
54
137
|
providers.push(
|
|
55
138
|
createBasicAuthProvider((username, password) =>
|
|
@@ -57,7 +140,6 @@ export const useHttpAuthentication = <TUser extends User, TSession extends Defau
|
|
|
57
140
|
),
|
|
58
141
|
)
|
|
59
142
|
}
|
|
60
|
-
|
|
61
143
|
providers.push(
|
|
62
144
|
createCookieAuthProvider(
|
|
63
145
|
mergedSettings.cookieName,
|
|
@@ -65,90 +147,39 @@ export const useHttpAuthentication = <TUser extends User, TSession extends Defau
|
|
|
65
147
|
(username) => findUserByName(userDataSet, systemInjector, username),
|
|
66
148
|
),
|
|
67
149
|
)
|
|
68
|
-
|
|
69
|
-
providers.push(...(settings?.authenticationProviders ?? []))
|
|
150
|
+
providers.push(...(overrides?.authenticationProviders ?? []))
|
|
70
151
|
mergedSettings.authenticationProviders = providers
|
|
71
152
|
|
|
72
|
-
injector.
|
|
153
|
+
injector.bind(HttpAuthenticationSettings, () => mergedSettings)
|
|
154
|
+
injector.invalidate(HttpAuthenticationSettings)
|
|
155
|
+
injector.invalidate(HttpUserContext)
|
|
73
156
|
}
|
|
74
157
|
|
|
75
158
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* @param options.injector The Injector instance
|
|
79
|
-
* @param options.settings Settings for the static file server
|
|
80
|
-
* @returns a promise that resolves when the server is ready
|
|
159
|
+
* Attaches a static-file {@link ServerApi} to the pooled HTTP server
|
|
160
|
+
* identified by `options.port`/`options.hostName`.
|
|
81
161
|
*/
|
|
82
|
-
export const useStaticFiles = (options: { injector: Injector } & StaticServerOptions) => {
|
|
162
|
+
export const useStaticFiles = async (options: { injector: Injector } & StaticServerOptions): Promise<ServerApi> => {
|
|
83
163
|
const { injector, ...settings } = options
|
|
84
|
-
|
|
164
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
165
|
+
const record = await pool.acquire({ port: settings.port, hostName: settings.hostName })
|
|
166
|
+
const serverApi = buildStaticSiteServerApi(settings)
|
|
167
|
+
record.apis.push(serverApi)
|
|
168
|
+
return serverApi
|
|
85
169
|
}
|
|
86
170
|
|
|
87
171
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* and emits 'onProxyFailed' events for monitoring.
|
|
93
|
-
*
|
|
94
|
-
* WebSocket connections can also be proxied by setting `enableWebsockets: true`, allowing
|
|
95
|
-
* bidirectional real-time communication through the proxy.
|
|
96
|
-
*
|
|
97
|
-
* @param options The settings for the proxy server
|
|
98
|
-
* @param options.injector The Injector instance
|
|
99
|
-
* @param options.sourceBaseUrl The base URL path to match for proxying (e.g., '/api', '/old').
|
|
100
|
-
* Can be specified with or without a trailing slash.
|
|
101
|
-
* @param options.targetBaseUrl The target server URL (must be a valid HTTP/HTTPS URL)
|
|
102
|
-
* @param options.pathRewrite Optional function to rewrite the path before forwarding.
|
|
103
|
-
* Receives the path after sourceBaseUrl, including leading slash and query string.
|
|
104
|
-
* Example: for 'GET /api/users?active=true' with sourceBaseUrl='/api',
|
|
105
|
-
* pathRewrite receives '/users?active=true'
|
|
106
|
-
* @param options.sourceHostName The hostname for the source server (optional, defaults to all interfaces)
|
|
107
|
-
* @param options.sourcePort The port for the source server
|
|
108
|
-
* @param options.headers Optional function to transform request headers.
|
|
109
|
-
* **Note**: Receives headers AFTER filtering hop-by-hop headers
|
|
110
|
-
* (Connection, Keep-Alive, Transfer-Encoding, Upgrade, etc.) for security
|
|
111
|
-
* and protocol compliance. The proxy automatically adds X-Forwarded-* headers.
|
|
112
|
-
* This transformation applies to both HTTP and WebSocket requests.
|
|
113
|
-
* @param options.cookies Optional function to transform request cookies (array of cookie strings)
|
|
114
|
-
* @param options.responseCookies Optional function to transform response Set-Cookie headers
|
|
115
|
-
* @param options.timeout Optional timeout in milliseconds for proxy requests (default: 30000).
|
|
116
|
-
* If exceeded, the request is aborted and 502 is returned.
|
|
117
|
-
* Applies to both HTTP and WebSocket upgrade requests.
|
|
118
|
-
* @param options.enableWebsockets Optional flag to enable WebSocket proxying (default: false).
|
|
119
|
-
* When enabled, WebSocket upgrade requests will be forwarded to the target.
|
|
120
|
-
* @returns a promise that resolves when the proxy is set up
|
|
121
|
-
* @example
|
|
122
|
-
* ```ts
|
|
123
|
-
* // Basic HTTP proxy with timeout
|
|
124
|
-
* await useProxy({
|
|
125
|
-
* injector,
|
|
126
|
-
* sourceBaseUrl: '/api',
|
|
127
|
-
* targetBaseUrl: 'https://api.example.com',
|
|
128
|
-
* sourcePort: 3000,
|
|
129
|
-
* timeout: 5000,
|
|
130
|
-
* })
|
|
131
|
-
*
|
|
132
|
-
* // Proxy with WebSocket support
|
|
133
|
-
* await useProxy({
|
|
134
|
-
* injector,
|
|
135
|
-
* sourceBaseUrl: '/ws',
|
|
136
|
-
* targetBaseUrl: 'https://ws.example.com',
|
|
137
|
-
* sourcePort: 3000,
|
|
138
|
-
* enableWebsockets: true,
|
|
139
|
-
* })
|
|
140
|
-
*
|
|
141
|
-
* // Proxy with error monitoring (HTTP and WebSocket)
|
|
142
|
-
* const proxyManager = injector.getInstance(ProxyManager)
|
|
143
|
-
* proxyManager.subscribe('onProxyFailed', ({ from, to, error }) => {
|
|
144
|
-
* console.error(`HTTP Proxy failed: ${from} -> ${to}`, error)
|
|
145
|
-
* })
|
|
146
|
-
* proxyManager.subscribe('onWebSocketProxyFailed', ({ from, to, error }) => {
|
|
147
|
-
* console.error(`WebSocket Proxy failed: ${from} -> ${to}`, error)
|
|
148
|
-
* })
|
|
149
|
-
* ```
|
|
172
|
+
* Attaches a proxy {@link ServerApi} that forwards HTTP (and optionally
|
|
173
|
+
* WebSocket) traffic from `options.sourceBaseUrl` to `options.targetBaseUrl`.
|
|
174
|
+
* Emits `onProxyFailed` and `onWebSocketProxyFailed` events into the shared
|
|
175
|
+
* {@link ServerTelemetryToken} — subscribe there for observability.
|
|
150
176
|
*/
|
|
151
|
-
export const useProxy = (options: { injector: Injector } & ProxyOptions) => {
|
|
177
|
+
export const useProxy = async (options: { injector: Injector } & ProxyOptions): Promise<ServerApi> => {
|
|
152
178
|
const { injector, ...settings } = options
|
|
153
|
-
|
|
179
|
+
const telemetry = injector.get(ServerTelemetryToken)
|
|
180
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
181
|
+
const record = await pool.acquire({ port: settings.sourcePort, hostName: settings.sourceHostName })
|
|
182
|
+
const serverApi = buildProxyServerApi(settings, telemetry)
|
|
183
|
+
record.apis.push(serverApi)
|
|
184
|
+
return serverApi
|
|
154
185
|
}
|
|
@@ -1,32 +1,86 @@
|
|
|
1
|
-
import { User } from '@furystack/core'
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import type { User } from '@furystack/core'
|
|
2
|
+
import type { Injector } from '@furystack/inject'
|
|
3
|
+
import { defineService, type Token } from '@furystack/inject'
|
|
4
|
+
import type { DataSetToken } from '@furystack/repository'
|
|
5
5
|
import type { AuthenticationProvider } from './authentication-providers/authentication-provider.js'
|
|
6
|
-
import {
|
|
6
|
+
import type { useHttpAuthentication } from './helpers.js'
|
|
7
|
+
import type { HttpUserContext } from './http-user-context.js'
|
|
8
|
+
import type { DefaultSession } from './models/default-session.js'
|
|
9
|
+
import { SessionDataSet, UserDataSet } from './user-store.js'
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
|
-
*
|
|
12
|
+
* HTTP-authentication settings — rebound by {@link useHttpAuthentication}
|
|
13
|
+
* during application setup and consumed by {@link HttpUserContext},
|
|
14
|
+
* cookie-login strategies and OpenAPI generators.
|
|
15
|
+
*
|
|
16
|
+
* `userDataSet` / `sessionDataSet` are token references — consumers resolve
|
|
17
|
+
* them through the injector at the appropriate scope. `authenticationProviders`
|
|
18
|
+
* is an ordered list walked by {@link HttpUserContext} on every
|
|
19
|
+
* unauthenticated request.
|
|
10
20
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
export interface HttpAuthenticationSettings {
|
|
22
|
+
/** Data set token for user lookups. */
|
|
23
|
+
userDataSet: DataSetToken<User, 'username'>
|
|
24
|
+
/** Data set token for session lookups. */
|
|
25
|
+
sessionDataSet: DataSetToken<DefaultSession, 'sessionId'>
|
|
26
|
+
/** Cookie name used by the built-in cookie-auth provider. */
|
|
27
|
+
cookieName: string
|
|
28
|
+
/** Whether the built-in Basic Auth provider is installed by {@link useHttpAuthentication}. */
|
|
29
|
+
enableBasicAuth: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Ordered list of authentication providers. Populated during
|
|
32
|
+
* {@link useHttpAuthentication} and extended by plugins such as
|
|
33
|
+
* `useJwtAuthentication`.
|
|
34
|
+
*/
|
|
35
|
+
authenticationProviders: AuthenticationProvider[]
|
|
36
|
+
/**
|
|
37
|
+
* TTL applied to the user-resolution cache. Every authenticated request
|
|
38
|
+
* runs at most one provider chain walk per cache key per TTL window;
|
|
39
|
+
* subsequent calls within the window return the cached `User`.
|
|
40
|
+
*
|
|
41
|
+
* Bounds the staleness window for out-of-band identity changes
|
|
42
|
+
* (session invalidation on a sibling instance, role mutation in storage,
|
|
43
|
+
* etc.) when the application is deployed across multiple nodes.
|
|
44
|
+
*
|
|
45
|
+
* Set to `0` to disable caching entirely. When omitted, the default
|
|
46
|
+
* {@link DEFAULT_USER_CACHE_TTL_MS} is used by `UserResolutionCache`.
|
|
47
|
+
*/
|
|
48
|
+
userCacheTtlMs?: number
|
|
49
|
+
/**
|
|
50
|
+
* Maximum number of entries in the user-resolution cache before LRU
|
|
51
|
+
* eviction kicks in. When omitted, the default
|
|
52
|
+
* {@link DEFAULT_USER_CACHE_CAPACITY} is used by `UserResolutionCache`.
|
|
53
|
+
*/
|
|
54
|
+
userCacheCapacity?: number
|
|
55
|
+
}
|
|
17
56
|
|
|
18
|
-
|
|
57
|
+
/** Default TTL applied to the user-resolution cache (30 seconds). */
|
|
58
|
+
export const DEFAULT_USER_CACHE_TTL_MS = 30_000
|
|
19
59
|
|
|
20
|
-
|
|
21
|
-
|
|
60
|
+
/** Default LRU capacity ceiling for the user-resolution cache. */
|
|
61
|
+
export const DEFAULT_USER_CACHE_CAPACITY = 10_000
|
|
22
62
|
|
|
23
|
-
|
|
24
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Returns a fresh copy of the default {@link HttpAuthenticationSettings}.
|
|
65
|
+
* Used as the starting point inside {@link useHttpAuthentication}.
|
|
66
|
+
*/
|
|
67
|
+
export const defaultHttpAuthenticationSettings = (): HttpAuthenticationSettings => ({
|
|
68
|
+
userDataSet: UserDataSet,
|
|
69
|
+
sessionDataSet: SessionDataSet,
|
|
70
|
+
cookieName: 'fss',
|
|
71
|
+
enableBasicAuth: true,
|
|
72
|
+
authenticationProviders: [],
|
|
73
|
+
userCacheTtlMs: DEFAULT_USER_CACHE_TTL_MS,
|
|
74
|
+
userCacheCapacity: DEFAULT_USER_CACHE_CAPACITY,
|
|
75
|
+
})
|
|
25
76
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
77
|
+
/**
|
|
78
|
+
* DI token carrying the current {@link HttpAuthenticationSettings}. Rebind
|
|
79
|
+
* via {@link useHttpAuthentication} (preferred) or directly through
|
|
80
|
+
* {@link Injector.bind} for advanced scenarios.
|
|
81
|
+
*/
|
|
82
|
+
export const HttpAuthenticationSettings: Token<HttpAuthenticationSettings, 'singleton'> = defineService({
|
|
83
|
+
name: 'furystack/rest-service/HttpAuthenticationSettings',
|
|
84
|
+
lifetime: 'singleton',
|
|
85
|
+
factory: () => defaultHttpAuthenticationSettings(),
|
|
86
|
+
})
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { getPort } from '@furystack/core/port-generator'
|
|
2
|
+
import { createInjector } from '@furystack/inject'
|
|
3
|
+
import { usingAsync } from '@furystack/utils'
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'http'
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { WebSocket } from 'ws'
|
|
7
|
+
import { HttpServerPoolToken, type OnRequest, type OnUpgrade, type ServerApi } from './http-server-pool.js'
|
|
8
|
+
import { ServerTelemetryToken } from './server-telemetry.js'
|
|
9
|
+
|
|
10
|
+
/** Returns a `ServerApi` that executes `handler` on every matching request. */
|
|
11
|
+
const makeApi = (handler: (options: OnRequest) => Promise<void>): ServerApi => ({
|
|
12
|
+
shouldExec: () => true,
|
|
13
|
+
onRequest: handler,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('HttpServerPool', () => {
|
|
17
|
+
it('Should return the cached record on subsequent acquire calls for the same url', async () => {
|
|
18
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
19
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
20
|
+
const port = getPort()
|
|
21
|
+
const first = await pool.acquire({ port })
|
|
22
|
+
const second = await pool.acquire({ port })
|
|
23
|
+
expect(second).toBe(first)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('Should de-duplicate concurrent acquire calls into a single server creation', async () => {
|
|
28
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
29
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
30
|
+
const port = getPort()
|
|
31
|
+
const [a, b, c] = await Promise.all([pool.acquire({ port }), pool.acquire({ port }), pool.acquire({ port })])
|
|
32
|
+
expect(b).toBe(a)
|
|
33
|
+
expect(c).toBe(a)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('Should emit onServerListening when a new record is opened', async () => {
|
|
38
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
39
|
+
const telemetry = injector.get(ServerTelemetryToken)
|
|
40
|
+
const listening = vi.fn()
|
|
41
|
+
telemetry.subscribe('onServerListening', listening)
|
|
42
|
+
|
|
43
|
+
const port = getPort()
|
|
44
|
+
await injector.get(HttpServerPoolToken).acquire({ port })
|
|
45
|
+
expect(listening).toHaveBeenCalledWith({ url: `http://localhost:${port}`, port, hostName: undefined })
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('Should destroy the socket and route the rejection through telemetry when onUpgrade rejects', async () => {
|
|
50
|
+
const injector = createInjector()
|
|
51
|
+
try {
|
|
52
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
53
|
+
const telemetry = injector.get(ServerTelemetryToken)
|
|
54
|
+
const onRequestFailed = vi.fn()
|
|
55
|
+
telemetry.subscribe('onRequestFailed', onRequestFailed)
|
|
56
|
+
|
|
57
|
+
const port = getPort()
|
|
58
|
+
const record = await pool.acquire({ port })
|
|
59
|
+
|
|
60
|
+
const rejection = new Error('upgrade boom')
|
|
61
|
+
const onUpgrade = vi.fn<(options: OnUpgrade) => Promise<void>>().mockRejectedValue(rejection)
|
|
62
|
+
record.apis.push({
|
|
63
|
+
shouldExec: () => true,
|
|
64
|
+
onRequest: () => Promise.resolve(),
|
|
65
|
+
onUpgrade,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const socketClosed = new Promise<void>((resolve) => {
|
|
69
|
+
const ws = new WebSocket(`ws://127.0.0.1:${port}`)
|
|
70
|
+
ws.on('close', () => resolve())
|
|
71
|
+
ws.on('error', () => resolve())
|
|
72
|
+
})
|
|
73
|
+
await socketClosed
|
|
74
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
75
|
+
|
|
76
|
+
expect(onUpgrade).toHaveBeenCalledTimes(1)
|
|
77
|
+
expect(onRequestFailed).toHaveBeenCalledTimes(1)
|
|
78
|
+
const [error] = onRequestFailed.mock.calls[0][0] as [unknown, IncomingMessage, ServerResponse]
|
|
79
|
+
expect(error).toBe(rejection)
|
|
80
|
+
} finally {
|
|
81
|
+
await injector[Symbol.asyncDispose]()
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('Should destroy the socket when no API claims the upgrade', async () => {
|
|
86
|
+
const injector = createInjector()
|
|
87
|
+
try {
|
|
88
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
89
|
+
const port = getPort()
|
|
90
|
+
await pool.acquire({ port })
|
|
91
|
+
|
|
92
|
+
const ws = new WebSocket(`ws://127.0.0.1:${port}`)
|
|
93
|
+
await new Promise<void>((resolve) => {
|
|
94
|
+
ws.on('close', () => resolve())
|
|
95
|
+
ws.on('error', () => resolve())
|
|
96
|
+
})
|
|
97
|
+
expect(ws.readyState).toBe(WebSocket.CLOSED)
|
|
98
|
+
} finally {
|
|
99
|
+
await injector[Symbol.asyncDispose]()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('Should destroy the raw socket when no API matches a normal request', async () => {
|
|
104
|
+
const injector = createInjector()
|
|
105
|
+
try {
|
|
106
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
107
|
+
const port = getPort()
|
|
108
|
+
await pool.acquire({ port })
|
|
109
|
+
|
|
110
|
+
await expect(fetch(`http://127.0.0.1:${port}/nothing`)).rejects.toBeDefined()
|
|
111
|
+
} finally {
|
|
112
|
+
await injector[Symbol.asyncDispose]()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('Should surface onRequest rejections through telemetry without crashing the server', async () => {
|
|
117
|
+
const injector = createInjector()
|
|
118
|
+
try {
|
|
119
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
120
|
+
const telemetry = injector.get(ServerTelemetryToken)
|
|
121
|
+
const onRequestFailed = vi.fn()
|
|
122
|
+
telemetry.subscribe('onRequestFailed', onRequestFailed)
|
|
123
|
+
|
|
124
|
+
const port = getPort()
|
|
125
|
+
const record = await pool.acquire({ port })
|
|
126
|
+
const rejection = new Error('request boom')
|
|
127
|
+
record.apis.push(
|
|
128
|
+
makeApi(async ({ res }) => {
|
|
129
|
+
res.end()
|
|
130
|
+
throw rejection
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
await fetch(`http://127.0.0.1:${port}/test`)
|
|
135
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
136
|
+
|
|
137
|
+
expect(onRequestFailed).toHaveBeenCalledTimes(1)
|
|
138
|
+
const [error] = onRequestFailed.mock.calls[0][0] as [unknown, IncomingMessage, ServerResponse]
|
|
139
|
+
expect(error).toBe(rejection)
|
|
140
|
+
} finally {
|
|
141
|
+
await injector[Symbol.asyncDispose]()
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('Should destroy open client sockets during disposal', async () => {
|
|
146
|
+
const injector = createInjector()
|
|
147
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
148
|
+
const port = getPort()
|
|
149
|
+
const record = await pool.acquire({ port })
|
|
150
|
+
|
|
151
|
+
record.apis.push({
|
|
152
|
+
shouldExec: () => true,
|
|
153
|
+
onRequest: async ({ res }) => {
|
|
154
|
+
res.writeHead(200)
|
|
155
|
+
res.write('streaming')
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// Keep a client socket open so the dispose path has something to destroy.
|
|
160
|
+
const inflight = fetch(`http://127.0.0.1:${port}/slow`).catch(() => undefined)
|
|
161
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
162
|
+
|
|
163
|
+
await injector[Symbol.asyncDispose]()
|
|
164
|
+
await inflight
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('Should absorb server.close errors during dispose without crashing', async () => {
|
|
168
|
+
const injector = createInjector()
|
|
169
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
170
|
+
const port = getPort()
|
|
171
|
+
const record = await pool.acquire({ port })
|
|
172
|
+
|
|
173
|
+
// Force `server.close` to invoke its callback with an error -- the pool
|
|
174
|
+
// wraps every close in `Promise.allSettled`, so the rejected branch
|
|
175
|
+
// (`if (err) { reject(err); return }`) is exercised but the injector's
|
|
176
|
+
// dispose still resolves cleanly.
|
|
177
|
+
const closeError = new Error('close boom')
|
|
178
|
+
const closeSpy = vi.fn((cb?: (err?: Error) => void) => {
|
|
179
|
+
cb?.(closeError)
|
|
180
|
+
return record.server
|
|
181
|
+
})
|
|
182
|
+
record.server.close = closeSpy
|
|
183
|
+
;(record.server as unknown as { off: (...args: unknown[]) => void }).off = () => undefined
|
|
184
|
+
|
|
185
|
+
await expect(injector[Symbol.asyncDispose]()).resolves.toBeUndefined()
|
|
186
|
+
expect(closeSpy).toHaveBeenCalled()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('Should drain in-flight acquire promises during dispose', async () => {
|
|
190
|
+
const injector = createInjector()
|
|
191
|
+
const port = getPort()
|
|
192
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
193
|
+
const inflight = pool.acquire({ port })
|
|
194
|
+
await injector[Symbol.asyncDispose]()
|
|
195
|
+
// The in-flight promise either resolved (server opened before dispose)
|
|
196
|
+
// or rejected cleanly — both are acceptable; we just assert no process
|
|
197
|
+
// crash / unhandled rejection escaped the pool's `allSettled` guard.
|
|
198
|
+
await inflight.catch(() => undefined)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('Should stop tracking sockets that close before dispose', async () => {
|
|
202
|
+
const injector = createInjector()
|
|
203
|
+
try {
|
|
204
|
+
const pool = injector.get(HttpServerPoolToken)
|
|
205
|
+
const port = getPort()
|
|
206
|
+
const record = await pool.acquire({ port })
|
|
207
|
+
record.apis.push(
|
|
208
|
+
makeApi(async ({ res }) => {
|
|
209
|
+
res.end('ok')
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
await fetch(`http://127.0.0.1:${port}/ok`)
|
|
214
|
+
// Let the TCP socket actually flip to 'close' before we tear down.
|
|
215
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
216
|
+
} finally {
|
|
217
|
+
await injector[Symbol.asyncDispose]()
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
})
|