@decohere/http 0.0.1-dev.126 → 0.0.1-dev.5
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/README.md +2 -2
- package/package.json +4 -13
- package/src/app/app.ts +147 -0
- package/src/app/module.ts +19 -0
- package/src/auth/ability.ts +56 -0
- package/src/auth/auth.config.ts +34 -0
- package/src/auth/auth.extensions.ts +115 -0
- package/src/auth/auth.types.ts +124 -0
- package/src/auth/casl-authorization.service.ts +62 -0
- package/{types/src/auth/principal-provider.d.ts → src/auth/principal-provider.ts} +13 -5
- package/src/auth/safe-outbound-url.ts +117 -0
- package/src/auth/token-service.ts +192 -0
- package/src/builder/builder.ts +118 -0
- package/{es6/src/const.js → src/const.ts} +25 -17
- package/src/health/health.controller.ts +34 -0
- package/src/health/health.extensions.ts +53 -0
- package/src/health/readiness.service.ts +33 -0
- package/{types/src/health/readiness.types.d.ts → src/health/readiness.types.ts} +4 -8
- package/src/hosting/hosted-service-host.ts +102 -0
- package/src/hosting/hosted-service.ts +31 -0
- package/src/http/http.compression.ts +38 -0
- package/src/http/http.extensions.ts +244 -0
- package/{types/src/http/http.options.store.d.ts → src/http/http.options.store.ts} +11 -1
- package/src/http/http.options.ts +41 -0
- package/{types/src/http/http.security.d.ts → src/http/http.security.ts} +16 -10
- package/src/http/http.static.ts +359 -0
- package/{es6/src/index.js → src/index.ts} +1 -0
- package/src/openapi/index.ts +22 -0
- package/src/openapi/openapi.generator.ts +78 -0
- package/src/openapi/openapi.types.ts +60 -0
- package/src/openapi/request-schema.ts +155 -0
- package/src/openapi/request-validator.ts +95 -0
- package/src/providers/fastify.provider.ts +454 -0
- package/src/routing/anvil.types.ts +150 -0
- package/src/routing/decorators/argument.decorator.ts +33 -0
- package/src/routing/decorators/auth.decorator.ts +79 -0
- package/src/routing/decorators/controller.decorator.ts +9 -0
- package/src/routing/decorators/field.decorator.ts +15 -0
- package/src/routing/decorators/request.decorator.ts +34 -0
- package/src/routing/decorators/response.decorator.ts +18 -0
- package/src/routing/decorators/route.decorator.ts +52 -0
- package/src/routing/filters/exception-filter-host.ts +90 -0
- package/src/routing/filters/exception-filter.ts +29 -0
- package/src/routing/middleware/http-access-log.middleware.ts +36 -0
- package/src/routing/middleware/middleware.ts +26 -0
- package/src/routing/middleware/request-id.ts +18 -0
- package/src/routing/path.util.ts +38 -0
- package/src/routing/pipes/pipes.ts +132 -0
- package/src/routing/services/route-argument-resolver.ts +92 -0
- package/src/routing/services/route-registry.ts +229 -0
- package/src/routing/services/router.ts +166 -0
- package/src/sse/sse.decorator.ts +41 -0
- package/src/sse/sse.encoder.ts +86 -0
- package/src/sse/sse.types.ts +47 -0
- package/src/sse/sse.wire.ts +34 -0
- package/src/testing/boot-module.ts +63 -0
- package/src/testing/sse-reader.ts +120 -0
- package/tests/app.hosted-services.spec.ts +292 -0
- package/tests/app.shutdown.integration.spec.ts +160 -0
- package/tests/app.spec.ts +132 -0
- package/tests/auth.ability.spec.ts +48 -0
- package/tests/auth.decorators.spec.ts +20 -0
- package/tests/auth.fail-closed.pentest.spec.ts +77 -0
- package/tests/auth.integration.spec.ts +234 -0
- package/tests/auth.jwks-cache.spec.ts +52 -0
- package/tests/auth.missing-auth.pentest.spec.ts +56 -0
- package/tests/auth.oidc-ssrf.pentest.spec.ts +72 -0
- package/tests/auth.oidc.integration.spec.ts +161 -0
- package/tests/auth.token-service.spec.ts +251 -0
- package/tests/builder.spec.ts +12 -0
- package/tests/health.integration.spec.ts +111 -0
- package/tests/helpers/listen-ephemeral.ts +13 -0
- package/tests/helpers/local-oidc-server.ts +100 -0
- package/tests/http.access-log.integration.spec.ts +111 -0
- package/tests/http.boolean-query.integration.spec.ts +59 -0
- package/tests/http.compression.integration.spec.ts +604 -0
- package/tests/http.date-query.integration.spec.ts +71 -0
- package/tests/http.global-prefix.integration.spec.ts +284 -0
- package/tests/http.number-query.integration.spec.ts +65 -0
- package/tests/http.pino-roll-hmr.integration.spec.ts +79 -0
- package/tests/http.pipeline-order.integration.spec.ts +95 -0
- package/tests/http.request-cleanup.integration.spec.ts +57 -0
- package/tests/http.request-id-scope.pentest.spec.ts +50 -0
- package/tests/http.security.integration.spec.ts +106 -0
- package/tests/http.static.integration.spec.ts +847 -0
- package/tests/http.static.spec.ts +111 -0
- package/tests/http.surface.integration.spec.ts +167 -0
- package/tests/openapi.integration.spec.ts +193 -0
- package/tests/openapi.validator-cache.integration.spec.ts +55 -0
- package/tests/routing.decorators.spec.ts +15 -0
- package/tests/routing.registry.spec.ts +44 -0
- package/tests/routing.resolver.spec.ts +92 -0
- package/tests/sse.integration.spec.ts +421 -0
- package/tests/sse.wire.spec.ts +33 -0
- package/LICENSE +0 -15
- package/commonjs/src/app/app.js +0 -166
- package/commonjs/src/app/index.js +0 -19
- package/commonjs/src/app/module.js +0 -17
- package/commonjs/src/auth/ability.js +0 -54
- package/commonjs/src/auth/auth.config.js +0 -23
- package/commonjs/src/auth/auth.extensions.js +0 -106
- package/commonjs/src/auth/auth.types.js +0 -65
- package/commonjs/src/auth/casl-authorization.service.js +0 -36
- package/commonjs/src/auth/index.js +0 -23
- package/commonjs/src/auth/principal-provider.js +0 -19
- package/commonjs/src/auth/safe-outbound-url.js +0 -102
- package/commonjs/src/auth/token-service.js +0 -151
- package/commonjs/src/builder/builder.js +0 -95
- package/commonjs/src/builder/index.js +0 -18
- package/commonjs/src/const.js +0 -87
- package/commonjs/src/health/health.controller.js +0 -75
- package/commonjs/src/health/health.extensions.js +0 -50
- package/commonjs/src/health/index.js +0 -21
- package/commonjs/src/health/readiness.service.js +0 -67
- package/commonjs/src/health/readiness.types.js +0 -7
- package/commonjs/src/hosting/hosted-service-host.js +0 -88
- package/commonjs/src/hosting/hosted-service.js +0 -11
- package/commonjs/src/hosting/index.js +0 -19
- package/commonjs/src/http/http.compression.js +0 -27
- package/commonjs/src/http/http.extensions.js +0 -269
- package/commonjs/src/http/http.options.js +0 -26
- package/commonjs/src/http/http.options.store.js +0 -20
- package/commonjs/src/http/http.security.js +0 -27
- package/commonjs/src/http/http.static.js +0 -248
- package/commonjs/src/http/index.js +0 -22
- package/commonjs/src/index.js +0 -27
- package/commonjs/src/openapi/index.js +0 -45
- package/commonjs/src/openapi/openapi.generator.js +0 -103
- package/commonjs/src/openapi/openapi.types.js +0 -4
- package/commonjs/src/openapi/request-schema.js +0 -187
- package/commonjs/src/openapi/request-validator.js +0 -85
- package/commonjs/src/providers/fastify.provider.js +0 -391
- package/commonjs/src/providers/index.js +0 -18
- package/commonjs/src/routing/anvil.types.js +0 -99
- package/commonjs/src/routing/decorators/argument.decorator.js +0 -49
- package/commonjs/src/routing/decorators/auth.decorator.js +0 -70
- package/commonjs/src/routing/decorators/controller.decorator.js +0 -16
- package/commonjs/src/routing/decorators/field.decorator.js +0 -25
- package/commonjs/src/routing/decorators/index.js +0 -24
- package/commonjs/src/routing/decorators/request.decorator.js +0 -30
- package/commonjs/src/routing/decorators/response.decorator.js +0 -34
- package/commonjs/src/routing/decorators/route.decorator.js +0 -67
- package/commonjs/src/routing/filters/exception-filter-host.js +0 -145
- package/commonjs/src/routing/filters/exception-filter.js +0 -28
- package/commonjs/src/routing/index.js +0 -25
- package/commonjs/src/routing/middleware/http-access-log.middleware.js +0 -47
- package/commonjs/src/routing/middleware/middleware.js +0 -20
- package/commonjs/src/routing/middleware/request-id.js +0 -30
- package/commonjs/src/routing/path.util.js +0 -51
- package/commonjs/src/routing/pipes/index.js +0 -18
- package/commonjs/src/routing/pipes/pipes.js +0 -145
- package/commonjs/src/routing/services/index.js +0 -18
- package/commonjs/src/routing/services/route-argument-resolver.js +0 -77
- package/commonjs/src/routing/services/route-registry.js +0 -194
- package/commonjs/src/routing/services/router.js +0 -158
- package/commonjs/src/sse/index.js +0 -26
- package/commonjs/src/sse/sse.decorator.js +0 -42
- package/commonjs/src/sse/sse.encoder.js +0 -73
- package/commonjs/src/sse/sse.types.js +0 -36
- package/commonjs/src/sse/sse.wire.js +0 -40
- package/commonjs/src/testing/boot-module.js +0 -38
- package/commonjs/src/testing/index.js +0 -20
- package/commonjs/src/testing/sse-reader.js +0 -98
- package/es6/src/app/app.js +0 -148
- package/es6/src/app/index.js +0 -2
- package/es6/src/app/module.js +0 -7
- package/es6/src/auth/ability.js +0 -45
- package/es6/src/auth/auth.config.js +0 -19
- package/es6/src/auth/auth.extensions.js +0 -83
- package/es6/src/auth/auth.types.js +0 -35
- package/es6/src/auth/casl-authorization.service.js +0 -40
- package/es6/src/auth/index.js +0 -6
- package/es6/src/auth/principal-provider.js +0 -14
- package/es6/src/auth/safe-outbound-url.js +0 -99
- package/es6/src/auth/token-service.js +0 -141
- package/es6/src/builder/builder.js +0 -85
- package/es6/src/builder/index.js +0 -1
- package/es6/src/health/health.controller.js +0 -65
- package/es6/src/health/health.extensions.js +0 -40
- package/es6/src/health/index.js +0 -4
- package/es6/src/health/readiness.service.js +0 -57
- package/es6/src/health/readiness.types.js +0 -4
- package/es6/src/hosting/hosted-service-host.js +0 -81
- package/es6/src/hosting/hosted-service.js +0 -8
- package/es6/src/hosting/index.js +0 -2
- package/es6/src/http/http.compression.js +0 -9
- package/es6/src/http/http.extensions.js +0 -259
- package/es6/src/http/http.options.js +0 -16
- package/es6/src/http/http.options.store.js +0 -13
- package/es6/src/http/http.security.js +0 -9
- package/es6/src/http/http.static.js +0 -224
- package/es6/src/http/index.js +0 -5
- package/es6/src/openapi/index.js +0 -3
- package/es6/src/openapi/openapi.generator.js +0 -82
- package/es6/src/openapi/openapi.types.js +0 -1
- package/es6/src/openapi/request-schema.js +0 -160
- package/es6/src/openapi/request-validator.js +0 -71
- package/es6/src/providers/fastify.provider.js +0 -376
- package/es6/src/providers/index.js +0 -1
- package/es6/src/routing/anvil.types.js +0 -57
- package/es6/src/routing/decorators/argument.decorator.js +0 -28
- package/es6/src/routing/decorators/auth.decorator.js +0 -58
- package/es6/src/routing/decorators/controller.decorator.js +0 -6
- package/es6/src/routing/decorators/field.decorator.js +0 -15
- package/es6/src/routing/decorators/index.js +0 -7
- package/es6/src/routing/decorators/request.decorator.js +0 -24
- package/es6/src/routing/decorators/response.decorator.js +0 -16
- package/es6/src/routing/decorators/route.decorator.js +0 -37
- package/es6/src/routing/filters/exception-filter-host.js +0 -127
- package/es6/src/routing/filters/exception-filter.js +0 -10
- package/es6/src/routing/index.js +0 -8
- package/es6/src/routing/middleware/http-access-log.middleware.js +0 -37
- package/es6/src/routing/middleware/middleware.js +0 -10
- package/es6/src/routing/middleware/request-id.js +0 -14
- package/es6/src/routing/path.util.js +0 -33
- package/es6/src/routing/pipes/index.js +0 -1
- package/es6/src/routing/pipes/pipes.js +0 -118
- package/es6/src/routing/services/index.js +0 -1
- package/es6/src/routing/services/route-argument-resolver.js +0 -67
- package/es6/src/routing/services/route-registry.js +0 -184
- package/es6/src/routing/services/router.js +0 -148
- package/es6/src/sse/index.js +0 -2
- package/es6/src/sse/sse.decorator.js +0 -31
- package/es6/src/sse/sse.encoder.js +0 -67
- package/es6/src/sse/sse.types.js +0 -22
- package/es6/src/sse/sse.wire.js +0 -30
- package/es6/src/testing/boot-module.js +0 -35
- package/es6/src/testing/index.js +0 -2
- package/es6/src/testing/sse-reader.js +0 -91
- package/types/src/app/app.d.ts +0 -41
- package/types/src/app/module.d.ts +0 -15
- package/types/src/auth/ability.d.ts +0 -19
- package/types/src/auth/auth.config.d.ts +0 -28
- package/types/src/auth/auth.extensions.d.ts +0 -53
- package/types/src/auth/auth.types.d.ts +0 -73
- package/types/src/auth/casl-authorization.service.d.ts +0 -28
- package/types/src/auth/safe-outbound-url.d.ts +0 -9
- package/types/src/auth/token-service.d.ts +0 -31
- package/types/src/builder/builder.d.ts +0 -36
- package/types/src/const.d.ts +0 -32
- package/types/src/health/health.controller.d.ts +0 -16
- package/types/src/health/health.extensions.d.ts +0 -31
- package/types/src/health/readiness.service.d.ts +0 -9
- package/types/src/hosting/hosted-service-host.d.ts +0 -37
- package/types/src/hosting/hosted-service.d.ts +0 -31
- package/types/src/http/http.compression.d.ts +0 -32
- package/types/src/http/http.extensions.d.ts +0 -79
- package/types/src/http/http.options.d.ts +0 -39
- package/types/src/http/http.static.d.ts +0 -160
- package/types/src/index.d.ts +0 -10
- package/types/src/openapi/index.d.ts +0 -6
- package/types/src/openapi/openapi.generator.d.ts +0 -6
- package/types/src/openapi/openapi.types.d.ts +0 -58
- package/types/src/openapi/request-schema.d.ts +0 -21
- package/types/src/openapi/request-validator.d.ts +0 -9
- package/types/src/providers/fastify.provider.d.ts +0 -56
- package/types/src/routing/anvil.types.d.ts +0 -119
- package/types/src/routing/decorators/argument.decorator.d.ts +0 -11
- package/types/src/routing/decorators/auth.decorator.d.ts +0 -30
- package/types/src/routing/decorators/controller.decorator.d.ts +0 -3
- package/types/src/routing/decorators/field.decorator.d.ts +0 -2
- package/types/src/routing/decorators/request.decorator.d.ts +0 -8
- package/types/src/routing/decorators/response.decorator.d.ts +0 -4
- package/types/src/routing/decorators/route.decorator.d.ts +0 -7
- package/types/src/routing/filters/exception-filter-host.d.ts +0 -19
- package/types/src/routing/filters/exception-filter.d.ts +0 -16
- package/types/src/routing/middleware/http-access-log.middleware.d.ts +0 -8
- package/types/src/routing/middleware/middleware.d.ts +0 -10
- package/types/src/routing/middleware/request-id.d.ts +0 -6
- package/types/src/routing/path.util.d.ts +0 -9
- package/types/src/routing/pipes/pipes.d.ts +0 -21
- package/types/src/routing/services/route-argument-resolver.d.ts +0 -10
- package/types/src/routing/services/route-registry.d.ts +0 -20
- package/types/src/routing/services/router.d.ts +0 -22
- package/types/src/sse/sse.decorator.d.ts +0 -17
- package/types/src/sse/sse.encoder.d.ts +0 -16
- package/types/src/sse/sse.types.d.ts +0 -37
- package/types/src/sse/sse.wire.d.ts +0 -9
- package/types/src/testing/boot-module.d.ts +0 -34
- package/types/src/testing/sse-reader.d.ts +0 -29
- /package/{types/src/app/index.d.ts → src/app/index.ts} +0 -0
- /package/{types/src/auth/index.d.ts → src/auth/index.ts} +0 -0
- /package/{types/src/builder/index.d.ts → src/builder/index.ts} +0 -0
- /package/{types/src/health/index.d.ts → src/health/index.ts} +0 -0
- /package/{types/src/hosting/index.d.ts → src/hosting/index.ts} +0 -0
- /package/{types/src/http/index.d.ts → src/http/index.ts} +0 -0
- /package/{types/src/providers/index.d.ts → src/providers/index.ts} +0 -0
- /package/{types/src/routing/decorators/index.d.ts → src/routing/decorators/index.ts} +0 -0
- /package/{types/src/routing/index.d.ts → src/routing/index.ts} +0 -0
- /package/{types/src/routing/pipes/index.d.ts → src/routing/pipes/index.ts} +0 -0
- /package/{types/src/routing/services/index.d.ts → src/routing/services/index.ts} +0 -0
- /package/{types/src/sse/index.d.ts → src/sse/index.ts} +0 -0
- /package/{types/src/testing/index.d.ts → src/testing/index.ts} +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { IContainer } from "@decohere/di";
|
|
2
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
3
|
+
import { scribe } from "@decohere/scribe";
|
|
4
|
+
import type { IReadinessCheck } from "../health/readiness.types.js";
|
|
5
|
+
import type { HostedService } from "./hosted-service.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Starts and stops the app's {@link HostedService}s. One per app; `Builder.withHostedService`
|
|
9
|
+
* adds the types and `App` drives it.
|
|
10
|
+
*/
|
|
11
|
+
export class HostedServiceHost {
|
|
12
|
+
private readonly logger = scribe.logger(HostedServiceHost.name);
|
|
13
|
+
private readonly types: Constructable<HostedService>[] = [];
|
|
14
|
+
private readonly controller = new AbortController();
|
|
15
|
+
/** In start order; stopped in reverse. */
|
|
16
|
+
private readonly started: HostedService[] = [];
|
|
17
|
+
private starting: Promise<void> | undefined;
|
|
18
|
+
private stopping: Promise<void> | undefined;
|
|
19
|
+
|
|
20
|
+
public add(...types: Constructable<HostedService>[]) {
|
|
21
|
+
this.types.push(...types);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public get isEmpty(): boolean {
|
|
25
|
+
return this.types.length === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Start every service in registration order. If one throws, the ones already started are
|
|
30
|
+
* stopped and the error is rethrown. Stops launching more once {@link stop} has begun.
|
|
31
|
+
*/
|
|
32
|
+
public start(container: IContainer): Promise<void> {
|
|
33
|
+
this.starting ??= this.startAll(container);
|
|
34
|
+
return this.starting;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Abort the services' signal: background work should wind down. {@link stop} does this too. */
|
|
38
|
+
public abort() {
|
|
39
|
+
this.controller.abort();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Abort the signal, wait for a start still in progress, then stop the started services in
|
|
44
|
+
* reverse order. Every `stop()` runs even if one throws; the failures are rethrown after.
|
|
45
|
+
*/
|
|
46
|
+
public stop(): Promise<void> {
|
|
47
|
+
this.stopping ??= this.stopAll();
|
|
48
|
+
return this.stopping;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The started services that report readiness, for `GET /ready`. */
|
|
52
|
+
public readinessChecks(): IReadinessCheck[] {
|
|
53
|
+
return this.started
|
|
54
|
+
.filter((service) => typeof service.check === "function")
|
|
55
|
+
.map((service) => ({
|
|
56
|
+
name: service.constructor.name,
|
|
57
|
+
check: () => service.check?.() ?? Promise.resolve(),
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async startAll(container: IContainer) {
|
|
62
|
+
const { signal } = this.controller;
|
|
63
|
+
for (const type of this.types) {
|
|
64
|
+
if (signal.aborted) return;
|
|
65
|
+
const service = container.resolve(type);
|
|
66
|
+
try {
|
|
67
|
+
await service.start(signal);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.logger.error(`Hosted service ${type.name} failed to start`, error);
|
|
70
|
+
// Not stop(): it waits for this start to settle.
|
|
71
|
+
this.controller.abort();
|
|
72
|
+
await this.stopStarted().catch(() => {});
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
this.started.push(service);
|
|
76
|
+
this.logger.debug(`Hosted service ${type.name} started`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async stopAll() {
|
|
81
|
+
this.controller.abort();
|
|
82
|
+
await Promise.allSettled([this.starting]);
|
|
83
|
+
await this.stopStarted();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Stop what is started, newest first. Takes each service off the list, so it runs once. */
|
|
87
|
+
private async stopStarted() {
|
|
88
|
+
const failures: unknown[] = [];
|
|
89
|
+
for (let service = this.started.pop(); service != null; service = this.started.pop()) {
|
|
90
|
+
try {
|
|
91
|
+
await service.stop?.();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.logger.error(`Hosted service ${service.constructor.name} failed to stop`, error);
|
|
94
|
+
failures.push(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (failures.length === 1) throw failures[0];
|
|
98
|
+
if (failures.length > 1) {
|
|
99
|
+
throw new AggregateError(failures, `${failures.length} hosted services failed to stop`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work that runs for the life of the app outside a request: a feed, a poller, a queue consumer.
|
|
3
|
+
* Register it with `builder.withHostedService(Type)`; it is a singleton, so other services can
|
|
4
|
+
* inject it by its class.
|
|
5
|
+
*
|
|
6
|
+
* The app starts hosted services in registration order before the HTTP server listens, and stops
|
|
7
|
+
* them in reverse order when it stops (`app.dispose()`, SIGTERM or SIGINT, a Flux reboot).
|
|
8
|
+
*/
|
|
9
|
+
export interface HostedService {
|
|
10
|
+
/**
|
|
11
|
+
* Called once when the app starts, and awaited: a throw fails `app.start()`. Do what must be
|
|
12
|
+
* ready before traffic, then start long work (a first load that takes minutes, a timer) in
|
|
13
|
+
* the background and return. Waiting for it here keeps the HTTP server dark.
|
|
14
|
+
*
|
|
15
|
+
* `signal` is aborted as soon as the app starts stopping, before `stop()` is called, also
|
|
16
|
+
* while `start()` itself is still running. Stop background work and clear timers on it.
|
|
17
|
+
*/
|
|
18
|
+
start(signal: AbortSignal): Promise<void> | void;
|
|
19
|
+
/**
|
|
20
|
+
* Called once when the app stops, after the HTTP server has drained and before the container
|
|
21
|
+
* is disposed, so the services it uses (a DataSource) are still open. Wait here for the
|
|
22
|
+
* background work to finish. Optional: a service with nothing to wait for can rely on the
|
|
23
|
+
* signal, or on `Symbol.asyncDispose`, which the container calls later.
|
|
24
|
+
*/
|
|
25
|
+
stop?(): Promise<void> | void;
|
|
26
|
+
/**
|
|
27
|
+
* Readiness while the service is started but still warming up: throw until it can serve, and
|
|
28
|
+
* `GET /ready` answers 503 (`withHealth()`). Optional.
|
|
29
|
+
*/
|
|
30
|
+
check?(): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response compression (`builder.withCompression`). Provider-neutral options and defaults;
|
|
3
|
+
* the HTTP provider hands them to its compression plugin and the plugin owns the policy
|
|
4
|
+
* (which responses are compressed, `Vary`, `HEAD`, streams). See ADR 0008.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** A content coding the HTTP package can offer. */
|
|
8
|
+
export type CompressionEncoding = "br" | "gzip" | "deflate";
|
|
9
|
+
|
|
10
|
+
/** Bodies smaller than this many bytes are sent as they are (not applied to streams, such as files). */
|
|
11
|
+
export const DEFAULT_COMPRESSION_THRESHOLD = 1024;
|
|
12
|
+
|
|
13
|
+
/** The codings offered by default, in order of preference. `deflate` is left out on purpose. */
|
|
14
|
+
export const DEFAULT_COMPRESSION_ENCODINGS: readonly CompressionEncoding[] = ["br", "gzip"];
|
|
15
|
+
|
|
16
|
+
/** Options for {@link Builder.withCompression}. `false` turns compression off. */
|
|
17
|
+
export type CompressionOptions =
|
|
18
|
+
| false
|
|
19
|
+
| {
|
|
20
|
+
/** Smallest body, in bytes, that is compressed. Defaults to 1024. Not applied to streamed bodies. */
|
|
21
|
+
threshold?: number;
|
|
22
|
+
/** The codings to offer, in order of preference. Defaults to `["br", "gzip"]`. */
|
|
23
|
+
encodings?: CompressionEncoding[];
|
|
24
|
+
/**
|
|
25
|
+
* Content types to compress, tested against the `Content-Type` header. Passed to the
|
|
26
|
+
* plugin as `customTypes`: it replaces the plugin's default pattern (text, JSON, XML,
|
|
27
|
+
* `octet-stream`; never `text/event-stream`) and anything the MIME database marks
|
|
28
|
+
* compressible is still compressed. Use it to add a type; do not match
|
|
29
|
+
* `text/event-stream` unless you want streams buffered.
|
|
30
|
+
*/
|
|
31
|
+
types?: RegExp | ((contentType: string) => boolean);
|
|
32
|
+
/**
|
|
33
|
+
* Fastify-specific. Any other `@fastify/compress` option (`brotliOptions`, `zlibOptions`,
|
|
34
|
+
* `onUnsupportedEncoding`, `inflateIfDeflated`, ...), merged last so it overrides the
|
|
35
|
+
* values above. Not portable to another HTTP provider.
|
|
36
|
+
*/
|
|
37
|
+
providerOptions?: Record<string, unknown>;
|
|
38
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ContainerBuilder, IContainer } from "@decohere/di";
|
|
2
|
+
import { extension } from "@decohere/core/extensibility";
|
|
3
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
4
|
+
import { scribe } from "@decohere/scribe";
|
|
5
|
+
import { PrincipalProvider } from "../auth/principal-provider.js";
|
|
6
|
+
import { Builder } from "../builder/builder.js";
|
|
7
|
+
import {
|
|
8
|
+
ExceptionFilterHostToken,
|
|
9
|
+
HttpOptionsToken,
|
|
10
|
+
HttpProviderToken,
|
|
11
|
+
IPrincipalProviderToken,
|
|
12
|
+
RouteRegistryToken,
|
|
13
|
+
RouterToken,
|
|
14
|
+
} from "../const.js";
|
|
15
|
+
import type { OpenApiOptions } from "../openapi/openapi.types.js";
|
|
16
|
+
import type { CorsOptions, HttpProvider } from "../routing/anvil.types.js";
|
|
17
|
+
import type { ExceptionFilter } from "../routing/filters/exception-filter.js";
|
|
18
|
+
import { ExceptionFilterHost } from "../routing/filters/exception-filter-host.js";
|
|
19
|
+
import { HttpAccessLogMiddleware } from "../routing/middleware/http-access-log.middleware.js";
|
|
20
|
+
import type { Middleware } from "../routing/middleware/middleware.js";
|
|
21
|
+
import { RouteRegistry } from "../routing/services/route-registry.js";
|
|
22
|
+
import { Router } from "../routing/services/router.js";
|
|
23
|
+
import type { WithHttpOptions } from "./http.options.js";
|
|
24
|
+
import { getOrCreateHttpOptions } from "./http.options.store.js";
|
|
25
|
+
import type { CompressionOptions } from "./http.compression.js";
|
|
26
|
+
import type { HelmetOptions, RateLimitOptions } from "./http.security.js";
|
|
27
|
+
import { type StaticOptions, staticFiles } from "./http.static.js";
|
|
28
|
+
|
|
29
|
+
const httpProviderStore = new WeakMap<Builder, Constructable<HttpProvider>>();
|
|
30
|
+
const controllersStore = new WeakMap<Builder, Constructable[]>();
|
|
31
|
+
|
|
32
|
+
declare module "@decohere/http" {
|
|
33
|
+
interface Builder {
|
|
34
|
+
withHttpProvider<Thttp extends Constructable<HttpProvider>>(type: Thttp): Builder;
|
|
35
|
+
/**
|
|
36
|
+
* HTTP provider plus scaffold defaults (request id + Scribe access log).
|
|
37
|
+
* Prefer this over {@link Builder.withHttpProvider} for application entrypoints.
|
|
38
|
+
*/
|
|
39
|
+
withHttp<Thttp extends Constructable<HttpProvider>>(
|
|
40
|
+
type: Thttp,
|
|
41
|
+
options?: WithHttpOptions,
|
|
42
|
+
): Builder;
|
|
43
|
+
withControllers(...controllers: Constructable[]): Builder;
|
|
44
|
+
/**
|
|
45
|
+
* Prefix every controller route (`/api` makes `/lp/portfolio` into `/api/lp/portfolio`).
|
|
46
|
+
* It covers controller routes only: not static mounts (`withStatic`), not the OpenAPI
|
|
47
|
+
* document, and not a controller declared with `@Controller({ globalPrefix: false })`,
|
|
48
|
+
* such as the `withHealth()` probes, which platforms expect at the root. With a prefix
|
|
49
|
+
* set, the static fallback treats everything under it as the API.
|
|
50
|
+
*/
|
|
51
|
+
withGlobalPrefix(prefix: string): Builder;
|
|
52
|
+
withCors(options: CorsOptions): Builder;
|
|
53
|
+
/**
|
|
54
|
+
* Override or disable default `@fastify/helmet` headers.
|
|
55
|
+
* Defaults are on with `withHttpProvider`; pass `false` to disable.
|
|
56
|
+
*/
|
|
57
|
+
withHelmet(options?: HelmetOptions): Builder;
|
|
58
|
+
/**
|
|
59
|
+
* Override or disable default `@fastify/rate-limit` (100 req / 1 minute).
|
|
60
|
+
* Pass `false` to disable.
|
|
61
|
+
*/
|
|
62
|
+
withRateLimit(options?: RateLimitOptions): Builder;
|
|
63
|
+
/**
|
|
64
|
+
* Compress responses with `br` or `gzip` when the client accepts it, using `@fastify/compress`
|
|
65
|
+
* with best-practice defaults (on by default, 1 KB threshold, no `deflate`). Pass `false` to
|
|
66
|
+
* turn it off, or `{ threshold, encodings, types, providerOptions }` to override. The plugin
|
|
67
|
+
* does not compress `text/event-stream` by default. Compressing a response that mixes a secret
|
|
68
|
+
* with input an attacker controls can leak the secret over HTTPS (BREACH); see ADR 0008.
|
|
69
|
+
*/
|
|
70
|
+
withCompression(options?: CompressionOptions): Builder;
|
|
71
|
+
/**
|
|
72
|
+
* Override the default request body size limit (1 MiB).
|
|
73
|
+
*/
|
|
74
|
+
withBodyLimit(bytes: number): Builder;
|
|
75
|
+
withMiddleware(...middlewares: Constructable<Middleware>[]): Builder;
|
|
76
|
+
withExceptionFilters(...filters: Constructable<ExceptionFilter>[]): Builder;
|
|
77
|
+
withOpenApi(options: OpenApiOptions): Builder;
|
|
78
|
+
/**
|
|
79
|
+
* Serve a built UI (a folder of static files) from this API, for example
|
|
80
|
+
* `.withStatic("/ui/", "ui")`. The directory is resolved from the process working
|
|
81
|
+
* directory, like `./config`. The files are public: they are served before auth,
|
|
82
|
+
* validation and middleware, and outside the rate limit. An unknown path under the prefix
|
|
83
|
+
* serves `index.html` to a browser (`Accept: text/html`) so client routes survive a reload. `"/"` mounts it at the root. Hashed assets are cached for a
|
|
84
|
+
* year, `index.html` is not cached. See {@link StaticOptions}.
|
|
85
|
+
*/
|
|
86
|
+
withStatic(prefix: string, directory: string, options?: StaticOptions): Builder;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class BuilderExtensions {
|
|
91
|
+
@extension(Builder)
|
|
92
|
+
public static withHttpProvider<Thttp extends Constructable<HttpProvider>>(
|
|
93
|
+
builder: Builder,
|
|
94
|
+
type: Thttp,
|
|
95
|
+
) {
|
|
96
|
+
httpProviderStore.set(builder, type);
|
|
97
|
+
const httpOptions = getOrCreateHttpOptions(builder);
|
|
98
|
+
|
|
99
|
+
builder.withServices((containerBuilder: ContainerBuilder) => {
|
|
100
|
+
const logger = scribe.logger("BuilderExtensions");
|
|
101
|
+
logger.trace("Configuring Http Provider");
|
|
102
|
+
|
|
103
|
+
const http = httpProviderStore.get(builder);
|
|
104
|
+
if (!http) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
containerBuilder.registerSingleton(HttpOptionsToken, httpOptions);
|
|
109
|
+
containerBuilder.registerSingleton(HttpProviderToken, http);
|
|
110
|
+
containerBuilder.registerSingleton(RouterToken, Router);
|
|
111
|
+
containerBuilder.registerSingleton(RouteRegistryToken, RouteRegistry);
|
|
112
|
+
containerBuilder.registerSingleton(ExceptionFilterHostToken, ExceptionFilterHost);
|
|
113
|
+
containerBuilder.registerRequestScoped(IPrincipalProviderToken, PrincipalProvider);
|
|
114
|
+
|
|
115
|
+
for (const middlewareType of httpOptions.middlewareTypes) {
|
|
116
|
+
containerBuilder.registerSingleton(middlewareType);
|
|
117
|
+
}
|
|
118
|
+
for (const filterType of httpOptions.filterTypes) {
|
|
119
|
+
containerBuilder.registerSingleton(filterType);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const controllers = controllersStore.get(builder) || [];
|
|
123
|
+
for (const controllerFn of controllers) {
|
|
124
|
+
containerBuilder.registerRequestScoped(controllerFn);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
builder.registerPostBuildStep((container: IContainer) => {
|
|
129
|
+
const logger = scribe.logger("BuilderExtensions");
|
|
130
|
+
const registry = container.resolve<RouteRegistry>(RouteRegistryToken);
|
|
131
|
+
const controllers = controllersStore.get(builder) || [];
|
|
132
|
+
for (const controllerFn of controllers) {
|
|
133
|
+
logger.trace(`Add routes from ${controllerFn.name}`);
|
|
134
|
+
registry.addController(controllerFn);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return builder;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@extension(Builder)
|
|
142
|
+
public static withHttp<Thttp extends Constructable<HttpProvider>>(
|
|
143
|
+
builder: Builder,
|
|
144
|
+
type: Thttp,
|
|
145
|
+
options?: WithHttpOptions,
|
|
146
|
+
) {
|
|
147
|
+
const accessLog = options?.accessLog !== false;
|
|
148
|
+
if (accessLog) {
|
|
149
|
+
const httpOptions = getOrCreateHttpOptions(builder);
|
|
150
|
+
if (!httpOptions.middlewareTypes.includes(HttpAccessLogMiddleware)) {
|
|
151
|
+
httpOptions.middlewareTypes = [
|
|
152
|
+
...httpOptions.middlewareTypes,
|
|
153
|
+
HttpAccessLogMiddleware,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return BuilderExtensions.withHttpProvider(builder, type);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@extension(Builder)
|
|
161
|
+
public static withControllers(builder: Builder, ...controllers: Constructable[]) {
|
|
162
|
+
const existing = controllersStore.get(builder) || [];
|
|
163
|
+
controllersStore.set(builder, [...existing, ...controllers]);
|
|
164
|
+
return builder;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@extension(Builder)
|
|
168
|
+
public static withGlobalPrefix(builder: Builder, prefix: string) {
|
|
169
|
+
getOrCreateHttpOptions(builder).globalPrefix = prefix;
|
|
170
|
+
return builder;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@extension(Builder)
|
|
174
|
+
public static withCors(builder: Builder, options: CorsOptions) {
|
|
175
|
+
getOrCreateHttpOptions(builder).cors = options;
|
|
176
|
+
return builder;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@extension(Builder)
|
|
180
|
+
public static withHelmet(builder: Builder, options: HelmetOptions = {}) {
|
|
181
|
+
getOrCreateHttpOptions(builder).helmet = options;
|
|
182
|
+
return builder;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@extension(Builder)
|
|
186
|
+
public static withRateLimit(builder: Builder, options: RateLimitOptions = {}) {
|
|
187
|
+
getOrCreateHttpOptions(builder).rateLimit = options;
|
|
188
|
+
return builder;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@extension(Builder)
|
|
192
|
+
public static withCompression(builder: Builder, options: CompressionOptions = {}) {
|
|
193
|
+
getOrCreateHttpOptions(builder).compression = options;
|
|
194
|
+
return builder;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@extension(Builder)
|
|
198
|
+
public static withBodyLimit(builder: Builder, bytes: number) {
|
|
199
|
+
getOrCreateHttpOptions(builder).bodyLimit = bytes;
|
|
200
|
+
return builder;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@extension(Builder)
|
|
204
|
+
public static withMiddleware(builder: Builder, ...middlewares: Constructable<Middleware>[]) {
|
|
205
|
+
const httpOptions = getOrCreateHttpOptions(builder);
|
|
206
|
+
httpOptions.middlewareTypes = [...httpOptions.middlewareTypes, ...middlewares];
|
|
207
|
+
return builder;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@extension(Builder)
|
|
211
|
+
public static withExceptionFilters(
|
|
212
|
+
builder: Builder,
|
|
213
|
+
...filters: Constructable<ExceptionFilter>[]
|
|
214
|
+
) {
|
|
215
|
+
const httpOptions = getOrCreateHttpOptions(builder);
|
|
216
|
+
httpOptions.filterTypes = [...httpOptions.filterTypes, ...filters];
|
|
217
|
+
return builder;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@extension(Builder)
|
|
221
|
+
public static withOpenApi(builder: Builder, options: OpenApiOptions) {
|
|
222
|
+
getOrCreateHttpOptions(builder).openapi = {
|
|
223
|
+
validateRequests: true,
|
|
224
|
+
path: "/openapi.json",
|
|
225
|
+
...options,
|
|
226
|
+
};
|
|
227
|
+
return builder;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@extension(Builder)
|
|
231
|
+
public static withStatic(
|
|
232
|
+
builder: Builder,
|
|
233
|
+
prefix: string,
|
|
234
|
+
directory: string,
|
|
235
|
+
options?: StaticOptions,
|
|
236
|
+
) {
|
|
237
|
+
const httpOptions = getOrCreateHttpOptions(builder);
|
|
238
|
+
httpOptions.statics = [
|
|
239
|
+
...httpOptions.statics,
|
|
240
|
+
staticFiles.toMount(prefix, directory, options),
|
|
241
|
+
];
|
|
242
|
+
return builder;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { Builder } from "../builder/builder.js";
|
|
2
2
|
import { HttpOptions } from "./http.options.js";
|
|
3
|
+
|
|
4
|
+
const httpOptionsStore = new WeakMap<Builder, HttpOptions>();
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* The {@link HttpOptions} a builder collects through its extensions. Internal: lets extensions
|
|
5
8
|
* outside `http.extensions.ts` (`withHealth`) set an option without a public builder method.
|
|
6
9
|
*/
|
|
7
|
-
export
|
|
10
|
+
export function getOrCreateHttpOptions(builder: Builder): HttpOptions {
|
|
11
|
+
let options = httpOptionsStore.get(builder);
|
|
12
|
+
if (options == null) {
|
|
13
|
+
options = new HttpOptions();
|
|
14
|
+
httpOptionsStore.set(builder, options);
|
|
15
|
+
}
|
|
16
|
+
return options;
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
2
|
+
import type { OpenApiOptions } from "../openapi/openapi.types.js";
|
|
3
|
+
import type { CorsOptions } from "../routing/anvil.types.js";
|
|
4
|
+
import type { ExceptionFilter } from "../routing/filters/exception-filter.js";
|
|
5
|
+
import type { Middleware } from "../routing/middleware/middleware.js";
|
|
6
|
+
import type { CompressionOptions } from "./http.compression.js";
|
|
7
|
+
import type { StaticMount } from "./http.static.js";
|
|
8
|
+
import type { HelmetOptions, RateLimitOptions } from "./http.security.js";
|
|
9
|
+
|
|
10
|
+
/** Options for {@link Builder.withHttp} (request-id / access-log defaults, etc.). */
|
|
11
|
+
export type WithHttpOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* Register {@link HttpAccessLogMiddleware} (request id echo + Scribe access log).
|
|
14
|
+
* Defaults to `true`.
|
|
15
|
+
*/
|
|
16
|
+
accessLog?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** App-level HTTP options set via builder extensions (`withCors`, `withGlobalPrefix`, …). */
|
|
20
|
+
export class HttpOptions {
|
|
21
|
+
public globalPrefix = "";
|
|
22
|
+
/**
|
|
23
|
+
* Controllers put under the global prefix although they declare `globalPrefix: false`
|
|
24
|
+
* (`withHealth({ underGlobalPrefix: true })`).
|
|
25
|
+
*/
|
|
26
|
+
public readonly underGlobalPrefix = new Set<Constructable>();
|
|
27
|
+
public cors?: CorsOptions;
|
|
28
|
+
public openapi?: OpenApiOptions;
|
|
29
|
+
/** `undefined` = helmet on with defaults; `false` = off; object = plugin options. */
|
|
30
|
+
public helmet?: HelmetOptions;
|
|
31
|
+
/** `undefined` = rate-limit on with defaults; `false` = off; object = plugin options. */
|
|
32
|
+
public rateLimit?: RateLimitOptions;
|
|
33
|
+
/** `undefined` = compression on with defaults; `false` = off; object = options. See `withCompression`. */
|
|
34
|
+
public compression?: CompressionOptions;
|
|
35
|
+
/** Request body size limit in bytes; `undefined` uses the 1 MiB default. */
|
|
36
|
+
public bodyLimit?: number;
|
|
37
|
+
/** Built UIs served from this API, see `withStatic`. */
|
|
38
|
+
public statics: StaticMount[] = [];
|
|
39
|
+
public middlewareTypes: Constructable<Middleware>[] = [];
|
|
40
|
+
public filterTypes: Constructable<ExceptionFilter>[] = [];
|
|
41
|
+
}
|
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
* `withHttpProvider` is used. Override via `withHelmet` / `withRateLimit` /
|
|
4
4
|
* `withBodyLimit` (pass `false` to disable helmet or rate-limit).
|
|
5
5
|
*/
|
|
6
|
+
|
|
6
7
|
/** Fastify body size limit (bytes). Matches Fastify's built-in default, made explicit and overridable. */
|
|
7
|
-
export
|
|
8
|
+
export const DEFAULT_HTTP_BODY_LIMIT = 1_048_576;
|
|
9
|
+
|
|
8
10
|
/** Global request rate limit applied unless disabled or customized. */
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
11
|
+
export const DEFAULT_HTTP_RATE_LIMIT = {
|
|
12
|
+
max: 100,
|
|
13
|
+
timeWindow: "1 minute",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
13
16
|
/** `false` disables the plugin; an object customizes `@fastify/helmet`. */
|
|
14
17
|
export type HelmetOptions = false | Record<string, unknown>;
|
|
18
|
+
|
|
15
19
|
/** `false` disables the plugin; an object customizes `@fastify/rate-limit`. */
|
|
16
|
-
export type RateLimitOptions =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
export type RateLimitOptions =
|
|
21
|
+
| false
|
|
22
|
+
| {
|
|
23
|
+
max?: number;
|
|
24
|
+
timeWindow?: number | string;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|