@decohere/http 0.0.1-dev.125 → 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.
Files changed (292) hide show
  1. package/package.json +4 -13
  2. package/src/app/app.ts +147 -0
  3. package/src/app/module.ts +19 -0
  4. package/src/auth/ability.ts +56 -0
  5. package/src/auth/auth.config.ts +34 -0
  6. package/src/auth/auth.extensions.ts +115 -0
  7. package/src/auth/auth.types.ts +124 -0
  8. package/src/auth/casl-authorization.service.ts +62 -0
  9. package/{types/src/auth/principal-provider.d.ts → src/auth/principal-provider.ts} +13 -5
  10. package/src/auth/safe-outbound-url.ts +117 -0
  11. package/src/auth/token-service.ts +192 -0
  12. package/src/builder/builder.ts +118 -0
  13. package/{es6/src/const.js → src/const.ts} +25 -17
  14. package/src/health/health.controller.ts +34 -0
  15. package/src/health/health.extensions.ts +53 -0
  16. package/src/health/readiness.service.ts +33 -0
  17. package/{types/src/health/readiness.types.d.ts → src/health/readiness.types.ts} +4 -8
  18. package/src/hosting/hosted-service-host.ts +102 -0
  19. package/src/hosting/hosted-service.ts +31 -0
  20. package/src/http/http.compression.ts +38 -0
  21. package/src/http/http.extensions.ts +244 -0
  22. package/{types/src/http/http.options.store.d.ts → src/http/http.options.store.ts} +11 -1
  23. package/src/http/http.options.ts +41 -0
  24. package/{types/src/http/http.security.d.ts → src/http/http.security.ts} +16 -10
  25. package/src/http/http.static.ts +359 -0
  26. package/{es6/src/index.js → src/index.ts} +1 -0
  27. package/src/openapi/index.ts +22 -0
  28. package/src/openapi/openapi.generator.ts +78 -0
  29. package/src/openapi/openapi.types.ts +60 -0
  30. package/src/openapi/request-schema.ts +155 -0
  31. package/src/openapi/request-validator.ts +95 -0
  32. package/src/providers/fastify.provider.ts +454 -0
  33. package/src/routing/anvil.types.ts +150 -0
  34. package/src/routing/decorators/argument.decorator.ts +33 -0
  35. package/src/routing/decorators/auth.decorator.ts +79 -0
  36. package/src/routing/decorators/controller.decorator.ts +9 -0
  37. package/src/routing/decorators/field.decorator.ts +15 -0
  38. package/src/routing/decorators/request.decorator.ts +34 -0
  39. package/src/routing/decorators/response.decorator.ts +18 -0
  40. package/src/routing/decorators/route.decorator.ts +52 -0
  41. package/src/routing/filters/exception-filter-host.ts +90 -0
  42. package/src/routing/filters/exception-filter.ts +29 -0
  43. package/src/routing/middleware/http-access-log.middleware.ts +36 -0
  44. package/src/routing/middleware/middleware.ts +26 -0
  45. package/src/routing/middleware/request-id.ts +18 -0
  46. package/src/routing/path.util.ts +38 -0
  47. package/src/routing/pipes/pipes.ts +132 -0
  48. package/src/routing/services/route-argument-resolver.ts +92 -0
  49. package/src/routing/services/route-registry.ts +229 -0
  50. package/src/routing/services/router.ts +166 -0
  51. package/src/sse/sse.decorator.ts +41 -0
  52. package/src/sse/sse.encoder.ts +86 -0
  53. package/src/sse/sse.types.ts +47 -0
  54. package/src/sse/sse.wire.ts +34 -0
  55. package/src/testing/boot-module.ts +63 -0
  56. package/src/testing/sse-reader.ts +120 -0
  57. package/tests/app.hosted-services.spec.ts +292 -0
  58. package/tests/app.shutdown.integration.spec.ts +160 -0
  59. package/tests/app.spec.ts +132 -0
  60. package/tests/auth.ability.spec.ts +48 -0
  61. package/tests/auth.decorators.spec.ts +20 -0
  62. package/tests/auth.fail-closed.pentest.spec.ts +77 -0
  63. package/tests/auth.integration.spec.ts +234 -0
  64. package/tests/auth.jwks-cache.spec.ts +52 -0
  65. package/tests/auth.missing-auth.pentest.spec.ts +56 -0
  66. package/tests/auth.oidc-ssrf.pentest.spec.ts +72 -0
  67. package/tests/auth.oidc.integration.spec.ts +161 -0
  68. package/tests/auth.token-service.spec.ts +251 -0
  69. package/tests/builder.spec.ts +12 -0
  70. package/tests/health.integration.spec.ts +111 -0
  71. package/tests/helpers/listen-ephemeral.ts +13 -0
  72. package/tests/helpers/local-oidc-server.ts +100 -0
  73. package/tests/http.access-log.integration.spec.ts +111 -0
  74. package/tests/http.boolean-query.integration.spec.ts +59 -0
  75. package/tests/http.compression.integration.spec.ts +604 -0
  76. package/tests/http.date-query.integration.spec.ts +71 -0
  77. package/tests/http.global-prefix.integration.spec.ts +284 -0
  78. package/tests/http.number-query.integration.spec.ts +65 -0
  79. package/tests/http.pino-roll-hmr.integration.spec.ts +79 -0
  80. package/tests/http.pipeline-order.integration.spec.ts +95 -0
  81. package/tests/http.request-cleanup.integration.spec.ts +57 -0
  82. package/tests/http.request-id-scope.pentest.spec.ts +50 -0
  83. package/tests/http.security.integration.spec.ts +106 -0
  84. package/tests/http.static.integration.spec.ts +847 -0
  85. package/tests/http.static.spec.ts +111 -0
  86. package/tests/http.surface.integration.spec.ts +167 -0
  87. package/tests/openapi.integration.spec.ts +193 -0
  88. package/tests/openapi.validator-cache.integration.spec.ts +55 -0
  89. package/tests/routing.decorators.spec.ts +15 -0
  90. package/tests/routing.registry.spec.ts +44 -0
  91. package/tests/routing.resolver.spec.ts +92 -0
  92. package/tests/sse.integration.spec.ts +421 -0
  93. package/tests/sse.wire.spec.ts +33 -0
  94. package/LICENSE +0 -15
  95. package/commonjs/src/app/app.js +0 -166
  96. package/commonjs/src/app/index.js +0 -19
  97. package/commonjs/src/app/module.js +0 -17
  98. package/commonjs/src/auth/ability.js +0 -54
  99. package/commonjs/src/auth/auth.config.js +0 -23
  100. package/commonjs/src/auth/auth.extensions.js +0 -106
  101. package/commonjs/src/auth/auth.types.js +0 -65
  102. package/commonjs/src/auth/casl-authorization.service.js +0 -36
  103. package/commonjs/src/auth/index.js +0 -23
  104. package/commonjs/src/auth/principal-provider.js +0 -19
  105. package/commonjs/src/auth/safe-outbound-url.js +0 -102
  106. package/commonjs/src/auth/token-service.js +0 -151
  107. package/commonjs/src/builder/builder.js +0 -95
  108. package/commonjs/src/builder/index.js +0 -18
  109. package/commonjs/src/const.js +0 -87
  110. package/commonjs/src/health/health.controller.js +0 -75
  111. package/commonjs/src/health/health.extensions.js +0 -50
  112. package/commonjs/src/health/index.js +0 -21
  113. package/commonjs/src/health/readiness.service.js +0 -67
  114. package/commonjs/src/health/readiness.types.js +0 -7
  115. package/commonjs/src/hosting/hosted-service-host.js +0 -88
  116. package/commonjs/src/hosting/hosted-service.js +0 -11
  117. package/commonjs/src/hosting/index.js +0 -19
  118. package/commonjs/src/http/http.compression.js +0 -27
  119. package/commonjs/src/http/http.extensions.js +0 -269
  120. package/commonjs/src/http/http.options.js +0 -26
  121. package/commonjs/src/http/http.options.store.js +0 -20
  122. package/commonjs/src/http/http.security.js +0 -27
  123. package/commonjs/src/http/http.static.js +0 -248
  124. package/commonjs/src/http/index.js +0 -22
  125. package/commonjs/src/index.js +0 -27
  126. package/commonjs/src/openapi/index.js +0 -45
  127. package/commonjs/src/openapi/openapi.generator.js +0 -103
  128. package/commonjs/src/openapi/openapi.types.js +0 -4
  129. package/commonjs/src/openapi/request-schema.js +0 -187
  130. package/commonjs/src/openapi/request-validator.js +0 -85
  131. package/commonjs/src/providers/fastify.provider.js +0 -391
  132. package/commonjs/src/providers/index.js +0 -18
  133. package/commonjs/src/routing/anvil.types.js +0 -99
  134. package/commonjs/src/routing/decorators/argument.decorator.js +0 -49
  135. package/commonjs/src/routing/decorators/auth.decorator.js +0 -70
  136. package/commonjs/src/routing/decorators/controller.decorator.js +0 -16
  137. package/commonjs/src/routing/decorators/field.decorator.js +0 -25
  138. package/commonjs/src/routing/decorators/index.js +0 -24
  139. package/commonjs/src/routing/decorators/request.decorator.js +0 -30
  140. package/commonjs/src/routing/decorators/response.decorator.js +0 -34
  141. package/commonjs/src/routing/decorators/route.decorator.js +0 -67
  142. package/commonjs/src/routing/filters/exception-filter-host.js +0 -145
  143. package/commonjs/src/routing/filters/exception-filter.js +0 -28
  144. package/commonjs/src/routing/index.js +0 -25
  145. package/commonjs/src/routing/middleware/http-access-log.middleware.js +0 -47
  146. package/commonjs/src/routing/middleware/middleware.js +0 -20
  147. package/commonjs/src/routing/middleware/request-id.js +0 -30
  148. package/commonjs/src/routing/path.util.js +0 -51
  149. package/commonjs/src/routing/pipes/index.js +0 -18
  150. package/commonjs/src/routing/pipes/pipes.js +0 -145
  151. package/commonjs/src/routing/services/index.js +0 -18
  152. package/commonjs/src/routing/services/route-argument-resolver.js +0 -77
  153. package/commonjs/src/routing/services/route-registry.js +0 -194
  154. package/commonjs/src/routing/services/router.js +0 -158
  155. package/commonjs/src/sse/index.js +0 -26
  156. package/commonjs/src/sse/sse.decorator.js +0 -42
  157. package/commonjs/src/sse/sse.encoder.js +0 -73
  158. package/commonjs/src/sse/sse.types.js +0 -36
  159. package/commonjs/src/sse/sse.wire.js +0 -40
  160. package/commonjs/src/testing/boot-module.js +0 -38
  161. package/commonjs/src/testing/index.js +0 -20
  162. package/commonjs/src/testing/sse-reader.js +0 -98
  163. package/es6/src/app/app.js +0 -148
  164. package/es6/src/app/index.js +0 -2
  165. package/es6/src/app/module.js +0 -7
  166. package/es6/src/auth/ability.js +0 -45
  167. package/es6/src/auth/auth.config.js +0 -19
  168. package/es6/src/auth/auth.extensions.js +0 -83
  169. package/es6/src/auth/auth.types.js +0 -35
  170. package/es6/src/auth/casl-authorization.service.js +0 -40
  171. package/es6/src/auth/index.js +0 -6
  172. package/es6/src/auth/principal-provider.js +0 -14
  173. package/es6/src/auth/safe-outbound-url.js +0 -99
  174. package/es6/src/auth/token-service.js +0 -141
  175. package/es6/src/builder/builder.js +0 -85
  176. package/es6/src/builder/index.js +0 -1
  177. package/es6/src/health/health.controller.js +0 -65
  178. package/es6/src/health/health.extensions.js +0 -40
  179. package/es6/src/health/index.js +0 -4
  180. package/es6/src/health/readiness.service.js +0 -57
  181. package/es6/src/health/readiness.types.js +0 -4
  182. package/es6/src/hosting/hosted-service-host.js +0 -81
  183. package/es6/src/hosting/hosted-service.js +0 -8
  184. package/es6/src/hosting/index.js +0 -2
  185. package/es6/src/http/http.compression.js +0 -9
  186. package/es6/src/http/http.extensions.js +0 -259
  187. package/es6/src/http/http.options.js +0 -16
  188. package/es6/src/http/http.options.store.js +0 -13
  189. package/es6/src/http/http.security.js +0 -9
  190. package/es6/src/http/http.static.js +0 -224
  191. package/es6/src/http/index.js +0 -5
  192. package/es6/src/openapi/index.js +0 -3
  193. package/es6/src/openapi/openapi.generator.js +0 -82
  194. package/es6/src/openapi/openapi.types.js +0 -1
  195. package/es6/src/openapi/request-schema.js +0 -160
  196. package/es6/src/openapi/request-validator.js +0 -71
  197. package/es6/src/providers/fastify.provider.js +0 -376
  198. package/es6/src/providers/index.js +0 -1
  199. package/es6/src/routing/anvil.types.js +0 -57
  200. package/es6/src/routing/decorators/argument.decorator.js +0 -28
  201. package/es6/src/routing/decorators/auth.decorator.js +0 -58
  202. package/es6/src/routing/decorators/controller.decorator.js +0 -6
  203. package/es6/src/routing/decorators/field.decorator.js +0 -15
  204. package/es6/src/routing/decorators/index.js +0 -7
  205. package/es6/src/routing/decorators/request.decorator.js +0 -24
  206. package/es6/src/routing/decorators/response.decorator.js +0 -16
  207. package/es6/src/routing/decorators/route.decorator.js +0 -37
  208. package/es6/src/routing/filters/exception-filter-host.js +0 -127
  209. package/es6/src/routing/filters/exception-filter.js +0 -10
  210. package/es6/src/routing/index.js +0 -8
  211. package/es6/src/routing/middleware/http-access-log.middleware.js +0 -37
  212. package/es6/src/routing/middleware/middleware.js +0 -10
  213. package/es6/src/routing/middleware/request-id.js +0 -14
  214. package/es6/src/routing/path.util.js +0 -33
  215. package/es6/src/routing/pipes/index.js +0 -1
  216. package/es6/src/routing/pipes/pipes.js +0 -118
  217. package/es6/src/routing/services/index.js +0 -1
  218. package/es6/src/routing/services/route-argument-resolver.js +0 -67
  219. package/es6/src/routing/services/route-registry.js +0 -184
  220. package/es6/src/routing/services/router.js +0 -148
  221. package/es6/src/sse/index.js +0 -2
  222. package/es6/src/sse/sse.decorator.js +0 -31
  223. package/es6/src/sse/sse.encoder.js +0 -67
  224. package/es6/src/sse/sse.types.js +0 -22
  225. package/es6/src/sse/sse.wire.js +0 -30
  226. package/es6/src/testing/boot-module.js +0 -35
  227. package/es6/src/testing/index.js +0 -2
  228. package/es6/src/testing/sse-reader.js +0 -91
  229. package/types/src/app/app.d.ts +0 -41
  230. package/types/src/app/module.d.ts +0 -15
  231. package/types/src/auth/ability.d.ts +0 -19
  232. package/types/src/auth/auth.config.d.ts +0 -28
  233. package/types/src/auth/auth.extensions.d.ts +0 -53
  234. package/types/src/auth/auth.types.d.ts +0 -73
  235. package/types/src/auth/casl-authorization.service.d.ts +0 -28
  236. package/types/src/auth/safe-outbound-url.d.ts +0 -9
  237. package/types/src/auth/token-service.d.ts +0 -31
  238. package/types/src/builder/builder.d.ts +0 -36
  239. package/types/src/const.d.ts +0 -32
  240. package/types/src/health/health.controller.d.ts +0 -16
  241. package/types/src/health/health.extensions.d.ts +0 -31
  242. package/types/src/health/readiness.service.d.ts +0 -9
  243. package/types/src/hosting/hosted-service-host.d.ts +0 -37
  244. package/types/src/hosting/hosted-service.d.ts +0 -31
  245. package/types/src/http/http.compression.d.ts +0 -32
  246. package/types/src/http/http.extensions.d.ts +0 -79
  247. package/types/src/http/http.options.d.ts +0 -39
  248. package/types/src/http/http.static.d.ts +0 -160
  249. package/types/src/index.d.ts +0 -10
  250. package/types/src/openapi/index.d.ts +0 -6
  251. package/types/src/openapi/openapi.generator.d.ts +0 -6
  252. package/types/src/openapi/openapi.types.d.ts +0 -58
  253. package/types/src/openapi/request-schema.d.ts +0 -21
  254. package/types/src/openapi/request-validator.d.ts +0 -9
  255. package/types/src/providers/fastify.provider.d.ts +0 -56
  256. package/types/src/routing/anvil.types.d.ts +0 -119
  257. package/types/src/routing/decorators/argument.decorator.d.ts +0 -11
  258. package/types/src/routing/decorators/auth.decorator.d.ts +0 -30
  259. package/types/src/routing/decorators/controller.decorator.d.ts +0 -3
  260. package/types/src/routing/decorators/field.decorator.d.ts +0 -2
  261. package/types/src/routing/decorators/request.decorator.d.ts +0 -8
  262. package/types/src/routing/decorators/response.decorator.d.ts +0 -4
  263. package/types/src/routing/decorators/route.decorator.d.ts +0 -7
  264. package/types/src/routing/filters/exception-filter-host.d.ts +0 -19
  265. package/types/src/routing/filters/exception-filter.d.ts +0 -16
  266. package/types/src/routing/middleware/http-access-log.middleware.d.ts +0 -8
  267. package/types/src/routing/middleware/middleware.d.ts +0 -10
  268. package/types/src/routing/middleware/request-id.d.ts +0 -6
  269. package/types/src/routing/path.util.d.ts +0 -9
  270. package/types/src/routing/pipes/pipes.d.ts +0 -21
  271. package/types/src/routing/services/route-argument-resolver.d.ts +0 -10
  272. package/types/src/routing/services/route-registry.d.ts +0 -20
  273. package/types/src/routing/services/router.d.ts +0 -22
  274. package/types/src/sse/sse.decorator.d.ts +0 -17
  275. package/types/src/sse/sse.encoder.d.ts +0 -16
  276. package/types/src/sse/sse.types.d.ts +0 -37
  277. package/types/src/sse/sse.wire.d.ts +0 -9
  278. package/types/src/testing/boot-module.d.ts +0 -34
  279. package/types/src/testing/sse-reader.d.ts +0 -29
  280. /package/{types/src/app/index.d.ts → src/app/index.ts} +0 -0
  281. /package/{types/src/auth/index.d.ts → src/auth/index.ts} +0 -0
  282. /package/{types/src/builder/index.d.ts → src/builder/index.ts} +0 -0
  283. /package/{types/src/health/index.d.ts → src/health/index.ts} +0 -0
  284. /package/{types/src/hosting/index.d.ts → src/hosting/index.ts} +0 -0
  285. /package/{types/src/http/index.d.ts → src/http/index.ts} +0 -0
  286. /package/{types/src/providers/index.d.ts → src/providers/index.ts} +0 -0
  287. /package/{types/src/routing/decorators/index.d.ts → src/routing/decorators/index.ts} +0 -0
  288. /package/{types/src/routing/index.d.ts → src/routing/index.ts} +0 -0
  289. /package/{types/src/routing/pipes/index.d.ts → src/routing/pipes/index.ts} +0 -0
  290. /package/{types/src/routing/services/index.d.ts → src/routing/services/index.ts} +0 -0
  291. /package/{types/src/sse/index.d.ts → src/sse/index.ts} +0 -0
  292. /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 declare function getOrCreateHttpOptions(builder: Builder): HttpOptions;
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 declare const DEFAULT_HTTP_BODY_LIMIT = 1048576;
8
+ export const DEFAULT_HTTP_BODY_LIMIT = 1_048_576;
9
+
8
10
  /** Global request rate limit applied unless disabled or customized. */
9
- export declare const DEFAULT_HTTP_RATE_LIMIT: {
10
- readonly max: 100;
11
- readonly timeWindow: "1 minute";
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 = false | {
17
- max?: number;
18
- timeWindow?: number | string;
19
- [key: string]: unknown;
20
- };
20
+ export type RateLimitOptions =
21
+ | false
22
+ | {
23
+ max?: number;
24
+ timeWindow?: number | string;
25
+ [key: string]: unknown;
26
+ };