@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.
Files changed (293) hide show
  1. package/README.md +2 -2
  2. package/package.json +4 -13
  3. package/src/app/app.ts +147 -0
  4. package/src/app/module.ts +19 -0
  5. package/src/auth/ability.ts +56 -0
  6. package/src/auth/auth.config.ts +34 -0
  7. package/src/auth/auth.extensions.ts +115 -0
  8. package/src/auth/auth.types.ts +124 -0
  9. package/src/auth/casl-authorization.service.ts +62 -0
  10. package/{types/src/auth/principal-provider.d.ts → src/auth/principal-provider.ts} +13 -5
  11. package/src/auth/safe-outbound-url.ts +117 -0
  12. package/src/auth/token-service.ts +192 -0
  13. package/src/builder/builder.ts +118 -0
  14. package/{es6/src/const.js → src/const.ts} +25 -17
  15. package/src/health/health.controller.ts +34 -0
  16. package/src/health/health.extensions.ts +53 -0
  17. package/src/health/readiness.service.ts +33 -0
  18. package/{types/src/health/readiness.types.d.ts → src/health/readiness.types.ts} +4 -8
  19. package/src/hosting/hosted-service-host.ts +102 -0
  20. package/src/hosting/hosted-service.ts +31 -0
  21. package/src/http/http.compression.ts +38 -0
  22. package/src/http/http.extensions.ts +244 -0
  23. package/{types/src/http/http.options.store.d.ts → src/http/http.options.store.ts} +11 -1
  24. package/src/http/http.options.ts +41 -0
  25. package/{types/src/http/http.security.d.ts → src/http/http.security.ts} +16 -10
  26. package/src/http/http.static.ts +359 -0
  27. package/{es6/src/index.js → src/index.ts} +1 -0
  28. package/src/openapi/index.ts +22 -0
  29. package/src/openapi/openapi.generator.ts +78 -0
  30. package/src/openapi/openapi.types.ts +60 -0
  31. package/src/openapi/request-schema.ts +155 -0
  32. package/src/openapi/request-validator.ts +95 -0
  33. package/src/providers/fastify.provider.ts +454 -0
  34. package/src/routing/anvil.types.ts +150 -0
  35. package/src/routing/decorators/argument.decorator.ts +33 -0
  36. package/src/routing/decorators/auth.decorator.ts +79 -0
  37. package/src/routing/decorators/controller.decorator.ts +9 -0
  38. package/src/routing/decorators/field.decorator.ts +15 -0
  39. package/src/routing/decorators/request.decorator.ts +34 -0
  40. package/src/routing/decorators/response.decorator.ts +18 -0
  41. package/src/routing/decorators/route.decorator.ts +52 -0
  42. package/src/routing/filters/exception-filter-host.ts +90 -0
  43. package/src/routing/filters/exception-filter.ts +29 -0
  44. package/src/routing/middleware/http-access-log.middleware.ts +36 -0
  45. package/src/routing/middleware/middleware.ts +26 -0
  46. package/src/routing/middleware/request-id.ts +18 -0
  47. package/src/routing/path.util.ts +38 -0
  48. package/src/routing/pipes/pipes.ts +132 -0
  49. package/src/routing/services/route-argument-resolver.ts +92 -0
  50. package/src/routing/services/route-registry.ts +229 -0
  51. package/src/routing/services/router.ts +166 -0
  52. package/src/sse/sse.decorator.ts +41 -0
  53. package/src/sse/sse.encoder.ts +86 -0
  54. package/src/sse/sse.types.ts +47 -0
  55. package/src/sse/sse.wire.ts +34 -0
  56. package/src/testing/boot-module.ts +63 -0
  57. package/src/testing/sse-reader.ts +120 -0
  58. package/tests/app.hosted-services.spec.ts +292 -0
  59. package/tests/app.shutdown.integration.spec.ts +160 -0
  60. package/tests/app.spec.ts +132 -0
  61. package/tests/auth.ability.spec.ts +48 -0
  62. package/tests/auth.decorators.spec.ts +20 -0
  63. package/tests/auth.fail-closed.pentest.spec.ts +77 -0
  64. package/tests/auth.integration.spec.ts +234 -0
  65. package/tests/auth.jwks-cache.spec.ts +52 -0
  66. package/tests/auth.missing-auth.pentest.spec.ts +56 -0
  67. package/tests/auth.oidc-ssrf.pentest.spec.ts +72 -0
  68. package/tests/auth.oidc.integration.spec.ts +161 -0
  69. package/tests/auth.token-service.spec.ts +251 -0
  70. package/tests/builder.spec.ts +12 -0
  71. package/tests/health.integration.spec.ts +111 -0
  72. package/tests/helpers/listen-ephemeral.ts +13 -0
  73. package/tests/helpers/local-oidc-server.ts +100 -0
  74. package/tests/http.access-log.integration.spec.ts +111 -0
  75. package/tests/http.boolean-query.integration.spec.ts +59 -0
  76. package/tests/http.compression.integration.spec.ts +604 -0
  77. package/tests/http.date-query.integration.spec.ts +71 -0
  78. package/tests/http.global-prefix.integration.spec.ts +284 -0
  79. package/tests/http.number-query.integration.spec.ts +65 -0
  80. package/tests/http.pino-roll-hmr.integration.spec.ts +79 -0
  81. package/tests/http.pipeline-order.integration.spec.ts +95 -0
  82. package/tests/http.request-cleanup.integration.spec.ts +57 -0
  83. package/tests/http.request-id-scope.pentest.spec.ts +50 -0
  84. package/tests/http.security.integration.spec.ts +106 -0
  85. package/tests/http.static.integration.spec.ts +847 -0
  86. package/tests/http.static.spec.ts +111 -0
  87. package/tests/http.surface.integration.spec.ts +167 -0
  88. package/tests/openapi.integration.spec.ts +193 -0
  89. package/tests/openapi.validator-cache.integration.spec.ts +55 -0
  90. package/tests/routing.decorators.spec.ts +15 -0
  91. package/tests/routing.registry.spec.ts +44 -0
  92. package/tests/routing.resolver.spec.ts +92 -0
  93. package/tests/sse.integration.spec.ts +421 -0
  94. package/tests/sse.wire.spec.ts +33 -0
  95. package/LICENSE +0 -15
  96. package/commonjs/src/app/app.js +0 -166
  97. package/commonjs/src/app/index.js +0 -19
  98. package/commonjs/src/app/module.js +0 -17
  99. package/commonjs/src/auth/ability.js +0 -54
  100. package/commonjs/src/auth/auth.config.js +0 -23
  101. package/commonjs/src/auth/auth.extensions.js +0 -106
  102. package/commonjs/src/auth/auth.types.js +0 -65
  103. package/commonjs/src/auth/casl-authorization.service.js +0 -36
  104. package/commonjs/src/auth/index.js +0 -23
  105. package/commonjs/src/auth/principal-provider.js +0 -19
  106. package/commonjs/src/auth/safe-outbound-url.js +0 -102
  107. package/commonjs/src/auth/token-service.js +0 -151
  108. package/commonjs/src/builder/builder.js +0 -95
  109. package/commonjs/src/builder/index.js +0 -18
  110. package/commonjs/src/const.js +0 -87
  111. package/commonjs/src/health/health.controller.js +0 -75
  112. package/commonjs/src/health/health.extensions.js +0 -50
  113. package/commonjs/src/health/index.js +0 -21
  114. package/commonjs/src/health/readiness.service.js +0 -67
  115. package/commonjs/src/health/readiness.types.js +0 -7
  116. package/commonjs/src/hosting/hosted-service-host.js +0 -88
  117. package/commonjs/src/hosting/hosted-service.js +0 -11
  118. package/commonjs/src/hosting/index.js +0 -19
  119. package/commonjs/src/http/http.compression.js +0 -27
  120. package/commonjs/src/http/http.extensions.js +0 -269
  121. package/commonjs/src/http/http.options.js +0 -26
  122. package/commonjs/src/http/http.options.store.js +0 -20
  123. package/commonjs/src/http/http.security.js +0 -27
  124. package/commonjs/src/http/http.static.js +0 -248
  125. package/commonjs/src/http/index.js +0 -22
  126. package/commonjs/src/index.js +0 -27
  127. package/commonjs/src/openapi/index.js +0 -45
  128. package/commonjs/src/openapi/openapi.generator.js +0 -103
  129. package/commonjs/src/openapi/openapi.types.js +0 -4
  130. package/commonjs/src/openapi/request-schema.js +0 -187
  131. package/commonjs/src/openapi/request-validator.js +0 -85
  132. package/commonjs/src/providers/fastify.provider.js +0 -391
  133. package/commonjs/src/providers/index.js +0 -18
  134. package/commonjs/src/routing/anvil.types.js +0 -99
  135. package/commonjs/src/routing/decorators/argument.decorator.js +0 -49
  136. package/commonjs/src/routing/decorators/auth.decorator.js +0 -70
  137. package/commonjs/src/routing/decorators/controller.decorator.js +0 -16
  138. package/commonjs/src/routing/decorators/field.decorator.js +0 -25
  139. package/commonjs/src/routing/decorators/index.js +0 -24
  140. package/commonjs/src/routing/decorators/request.decorator.js +0 -30
  141. package/commonjs/src/routing/decorators/response.decorator.js +0 -34
  142. package/commonjs/src/routing/decorators/route.decorator.js +0 -67
  143. package/commonjs/src/routing/filters/exception-filter-host.js +0 -145
  144. package/commonjs/src/routing/filters/exception-filter.js +0 -28
  145. package/commonjs/src/routing/index.js +0 -25
  146. package/commonjs/src/routing/middleware/http-access-log.middleware.js +0 -47
  147. package/commonjs/src/routing/middleware/middleware.js +0 -20
  148. package/commonjs/src/routing/middleware/request-id.js +0 -30
  149. package/commonjs/src/routing/path.util.js +0 -51
  150. package/commonjs/src/routing/pipes/index.js +0 -18
  151. package/commonjs/src/routing/pipes/pipes.js +0 -145
  152. package/commonjs/src/routing/services/index.js +0 -18
  153. package/commonjs/src/routing/services/route-argument-resolver.js +0 -77
  154. package/commonjs/src/routing/services/route-registry.js +0 -194
  155. package/commonjs/src/routing/services/router.js +0 -158
  156. package/commonjs/src/sse/index.js +0 -26
  157. package/commonjs/src/sse/sse.decorator.js +0 -42
  158. package/commonjs/src/sse/sse.encoder.js +0 -73
  159. package/commonjs/src/sse/sse.types.js +0 -36
  160. package/commonjs/src/sse/sse.wire.js +0 -40
  161. package/commonjs/src/testing/boot-module.js +0 -38
  162. package/commonjs/src/testing/index.js +0 -20
  163. package/commonjs/src/testing/sse-reader.js +0 -98
  164. package/es6/src/app/app.js +0 -148
  165. package/es6/src/app/index.js +0 -2
  166. package/es6/src/app/module.js +0 -7
  167. package/es6/src/auth/ability.js +0 -45
  168. package/es6/src/auth/auth.config.js +0 -19
  169. package/es6/src/auth/auth.extensions.js +0 -83
  170. package/es6/src/auth/auth.types.js +0 -35
  171. package/es6/src/auth/casl-authorization.service.js +0 -40
  172. package/es6/src/auth/index.js +0 -6
  173. package/es6/src/auth/principal-provider.js +0 -14
  174. package/es6/src/auth/safe-outbound-url.js +0 -99
  175. package/es6/src/auth/token-service.js +0 -141
  176. package/es6/src/builder/builder.js +0 -85
  177. package/es6/src/builder/index.js +0 -1
  178. package/es6/src/health/health.controller.js +0 -65
  179. package/es6/src/health/health.extensions.js +0 -40
  180. package/es6/src/health/index.js +0 -4
  181. package/es6/src/health/readiness.service.js +0 -57
  182. package/es6/src/health/readiness.types.js +0 -4
  183. package/es6/src/hosting/hosted-service-host.js +0 -81
  184. package/es6/src/hosting/hosted-service.js +0 -8
  185. package/es6/src/hosting/index.js +0 -2
  186. package/es6/src/http/http.compression.js +0 -9
  187. package/es6/src/http/http.extensions.js +0 -259
  188. package/es6/src/http/http.options.js +0 -16
  189. package/es6/src/http/http.options.store.js +0 -13
  190. package/es6/src/http/http.security.js +0 -9
  191. package/es6/src/http/http.static.js +0 -224
  192. package/es6/src/http/index.js +0 -5
  193. package/es6/src/openapi/index.js +0 -3
  194. package/es6/src/openapi/openapi.generator.js +0 -82
  195. package/es6/src/openapi/openapi.types.js +0 -1
  196. package/es6/src/openapi/request-schema.js +0 -160
  197. package/es6/src/openapi/request-validator.js +0 -71
  198. package/es6/src/providers/fastify.provider.js +0 -376
  199. package/es6/src/providers/index.js +0 -1
  200. package/es6/src/routing/anvil.types.js +0 -57
  201. package/es6/src/routing/decorators/argument.decorator.js +0 -28
  202. package/es6/src/routing/decorators/auth.decorator.js +0 -58
  203. package/es6/src/routing/decorators/controller.decorator.js +0 -6
  204. package/es6/src/routing/decorators/field.decorator.js +0 -15
  205. package/es6/src/routing/decorators/index.js +0 -7
  206. package/es6/src/routing/decorators/request.decorator.js +0 -24
  207. package/es6/src/routing/decorators/response.decorator.js +0 -16
  208. package/es6/src/routing/decorators/route.decorator.js +0 -37
  209. package/es6/src/routing/filters/exception-filter-host.js +0 -127
  210. package/es6/src/routing/filters/exception-filter.js +0 -10
  211. package/es6/src/routing/index.js +0 -8
  212. package/es6/src/routing/middleware/http-access-log.middleware.js +0 -37
  213. package/es6/src/routing/middleware/middleware.js +0 -10
  214. package/es6/src/routing/middleware/request-id.js +0 -14
  215. package/es6/src/routing/path.util.js +0 -33
  216. package/es6/src/routing/pipes/index.js +0 -1
  217. package/es6/src/routing/pipes/pipes.js +0 -118
  218. package/es6/src/routing/services/index.js +0 -1
  219. package/es6/src/routing/services/route-argument-resolver.js +0 -67
  220. package/es6/src/routing/services/route-registry.js +0 -184
  221. package/es6/src/routing/services/router.js +0 -148
  222. package/es6/src/sse/index.js +0 -2
  223. package/es6/src/sse/sse.decorator.js +0 -31
  224. package/es6/src/sse/sse.encoder.js +0 -67
  225. package/es6/src/sse/sse.types.js +0 -22
  226. package/es6/src/sse/sse.wire.js +0 -30
  227. package/es6/src/testing/boot-module.js +0 -35
  228. package/es6/src/testing/index.js +0 -2
  229. package/es6/src/testing/sse-reader.js +0 -91
  230. package/types/src/app/app.d.ts +0 -41
  231. package/types/src/app/module.d.ts +0 -15
  232. package/types/src/auth/ability.d.ts +0 -19
  233. package/types/src/auth/auth.config.d.ts +0 -28
  234. package/types/src/auth/auth.extensions.d.ts +0 -53
  235. package/types/src/auth/auth.types.d.ts +0 -73
  236. package/types/src/auth/casl-authorization.service.d.ts +0 -28
  237. package/types/src/auth/safe-outbound-url.d.ts +0 -9
  238. package/types/src/auth/token-service.d.ts +0 -31
  239. package/types/src/builder/builder.d.ts +0 -36
  240. package/types/src/const.d.ts +0 -32
  241. package/types/src/health/health.controller.d.ts +0 -16
  242. package/types/src/health/health.extensions.d.ts +0 -31
  243. package/types/src/health/readiness.service.d.ts +0 -9
  244. package/types/src/hosting/hosted-service-host.d.ts +0 -37
  245. package/types/src/hosting/hosted-service.d.ts +0 -31
  246. package/types/src/http/http.compression.d.ts +0 -32
  247. package/types/src/http/http.extensions.d.ts +0 -79
  248. package/types/src/http/http.options.d.ts +0 -39
  249. package/types/src/http/http.static.d.ts +0 -160
  250. package/types/src/index.d.ts +0 -10
  251. package/types/src/openapi/index.d.ts +0 -6
  252. package/types/src/openapi/openapi.generator.d.ts +0 -6
  253. package/types/src/openapi/openapi.types.d.ts +0 -58
  254. package/types/src/openapi/request-schema.d.ts +0 -21
  255. package/types/src/openapi/request-validator.d.ts +0 -9
  256. package/types/src/providers/fastify.provider.d.ts +0 -56
  257. package/types/src/routing/anvil.types.d.ts +0 -119
  258. package/types/src/routing/decorators/argument.decorator.d.ts +0 -11
  259. package/types/src/routing/decorators/auth.decorator.d.ts +0 -30
  260. package/types/src/routing/decorators/controller.decorator.d.ts +0 -3
  261. package/types/src/routing/decorators/field.decorator.d.ts +0 -2
  262. package/types/src/routing/decorators/request.decorator.d.ts +0 -8
  263. package/types/src/routing/decorators/response.decorator.d.ts +0 -4
  264. package/types/src/routing/decorators/route.decorator.d.ts +0 -7
  265. package/types/src/routing/filters/exception-filter-host.d.ts +0 -19
  266. package/types/src/routing/filters/exception-filter.d.ts +0 -16
  267. package/types/src/routing/middleware/http-access-log.middleware.d.ts +0 -8
  268. package/types/src/routing/middleware/middleware.d.ts +0 -10
  269. package/types/src/routing/middleware/request-id.d.ts +0 -6
  270. package/types/src/routing/path.util.d.ts +0 -9
  271. package/types/src/routing/pipes/pipes.d.ts +0 -21
  272. package/types/src/routing/services/route-argument-resolver.d.ts +0 -10
  273. package/types/src/routing/services/route-registry.d.ts +0 -20
  274. package/types/src/routing/services/router.d.ts +0 -22
  275. package/types/src/sse/sse.decorator.d.ts +0 -17
  276. package/types/src/sse/sse.encoder.d.ts +0 -16
  277. package/types/src/sse/sse.types.d.ts +0 -37
  278. package/types/src/sse/sse.wire.d.ts +0 -9
  279. package/types/src/testing/boot-module.d.ts +0 -34
  280. package/types/src/testing/sse-reader.d.ts +0 -29
  281. /package/{types/src/app/index.d.ts → src/app/index.ts} +0 -0
  282. /package/{types/src/auth/index.d.ts → src/auth/index.ts} +0 -0
  283. /package/{types/src/builder/index.d.ts → src/builder/index.ts} +0 -0
  284. /package/{types/src/health/index.d.ts → src/health/index.ts} +0 -0
  285. /package/{types/src/hosting/index.d.ts → src/hosting/index.ts} +0 -0
  286. /package/{types/src/http/index.d.ts → src/http/index.ts} +0 -0
  287. /package/{types/src/providers/index.d.ts → src/providers/index.ts} +0 -0
  288. /package/{types/src/routing/decorators/index.d.ts → src/routing/decorators/index.ts} +0 -0
  289. /package/{types/src/routing/index.d.ts → src/routing/index.ts} +0 -0
  290. /package/{types/src/routing/pipes/index.d.ts → src/routing/pipes/index.ts} +0 -0
  291. /package/{types/src/routing/services/index.d.ts → src/routing/services/index.ts} +0 -0
  292. /package/{types/src/sse/index.d.ts → src/sse/index.ts} +0 -0
  293. /package/{types/src/testing/index.d.ts → src/testing/index.ts} +0 -0
@@ -1,160 +0,0 @@
1
- import { isPrimitiveOrDateType } from "@decohere/core/reflection";
2
- import { InjectedParts } from "../routing/anvil.types.js";
3
- export function buildRouteRequestSchemas(route) {
4
- const queryParams = [];
5
- const pathParams = [];
6
- let bodySchema;
7
- for (const routeArg of route.routeArgs ?? []){
8
- if (routeArg.argumentLocation != null && InjectedParts.includes(routeArg.argumentLocation)) {
9
- continue;
10
- }
11
- if (isPrimitiveOrDateType(routeArg.argumentType)) {
12
- const location = routeArg.argumentLocation;
13
- if (location === "query" || location === "params") {
14
- const param = {
15
- name: routeArg.mappingKey ?? "value",
16
- in: location === "params" ? "path" : "query",
17
- required: location === "params",
18
- schema: typeToSchema(routeArg.argumentType.name)
19
- };
20
- if (location === "params") {
21
- pathParams.push(param);
22
- } else {
23
- queryParams.push(param);
24
- }
25
- }
26
- continue;
27
- }
28
- const parts = objectArgToParts(routeArg);
29
- queryParams.push(...parts.queryParams);
30
- pathParams.push(...parts.pathParams);
31
- if (parts.body != null) {
32
- bodySchema = mergeObjectSchemas(bodySchema, parts.body);
33
- }
34
- }
35
- return {
36
- queryParams,
37
- pathParams,
38
- body: bodySchema
39
- };
40
- }
41
- export function objectArgToParts(routeArg) {
42
- const queryParams = [];
43
- const pathParams = [];
44
- const bodyProperties = {};
45
- const bodyRequired = [];
46
- const argLocation = routeArg.argumentLocation;
47
- for (const [, meta] of routeArg.fieldMap?.entries() ?? []){
48
- const location = meta.fieldLocation ?? argLocation;
49
- const name = meta.mappingKey || meta.fieldKey;
50
- const schema = typeToSchema(meta.fieldType?.name);
51
- if (location === "query") {
52
- queryParams.push({
53
- name,
54
- in: "query",
55
- required: false,
56
- schema
57
- });
58
- } else if (location === "params") {
59
- pathParams.push({
60
- name,
61
- in: "path",
62
- required: true,
63
- schema
64
- });
65
- } else if (location === "body" || location == null && argLocation === "body") {
66
- bodyProperties[name] = schema;
67
- bodyRequired.push(name);
68
- } else if (location == null && argLocation == null) {
69
- // Unlocated object fields default to body for OpenAPI (common DTO pattern).
70
- bodyProperties[name] = schema;
71
- bodyRequired.push(name);
72
- }
73
- }
74
- // Whole-object body argument with no fields still advertises an object body.
75
- if (argLocation === "body" && Object.keys(bodyProperties).length === 0) {
76
- return {
77
- queryParams,
78
- pathParams,
79
- body: {
80
- type: "object",
81
- additionalProperties: true
82
- }
83
- };
84
- }
85
- const body = Object.keys(bodyProperties).length > 0 ? {
86
- type: "object",
87
- properties: bodyProperties,
88
- required: bodyRequired,
89
- additionalProperties: false
90
- } : undefined;
91
- return {
92
- queryParams,
93
- pathParams,
94
- body
95
- };
96
- }
97
- export function mergeObjectSchemas(a, b) {
98
- if (a == null) {
99
- return b;
100
- }
101
- return {
102
- type: "object",
103
- properties: {
104
- ...a.properties,
105
- ...b.properties
106
- },
107
- required: [
108
- ...new Set([
109
- ...a.required ?? [],
110
- ...b.required ?? []
111
- ])
112
- ],
113
- additionalProperties: false
114
- };
115
- }
116
- export function typeToSchema(typeName) {
117
- switch(typeName){
118
- case "Number":
119
- return {
120
- type: "number"
121
- };
122
- case "Boolean":
123
- return {
124
- type: "boolean"
125
- };
126
- case "Date":
127
- return {
128
- type: "string",
129
- format: "date-time"
130
- };
131
- case "String":
132
- return {
133
- type: "string"
134
- };
135
- default:
136
- return {};
137
- }
138
- }
139
- /** JSON Schema object for required path params (HTTP path values are always strings). */ export function pathParamsToSchema(params) {
140
- if (params.length === 0) {
141
- return undefined;
142
- }
143
- const properties = {};
144
- const required = [];
145
- for (const param of params){
146
- properties[param.name] = {
147
- type: "string",
148
- minLength: 1
149
- };
150
- if (param.required !== false) {
151
- required.push(param.name);
152
- }
153
- }
154
- return {
155
- type: "object",
156
- properties,
157
- required,
158
- additionalProperties: true
159
- };
160
- }
@@ -1,71 +0,0 @@
1
- import { Ajv } from "ajv";
2
- import { ValidationError } from "../routing/anvil.types.js";
3
- import { buildRouteRequestSchemas, pathParamsToSchema } from "./request-schema.js";
4
- export { ValidationError };
5
- /**
6
- * Fail-closed request validation from route / OpenAPI / `@Field` metadata.
7
- * Uses AJV against the same schemas as the OpenAPI generator (no type coercion).
8
- */ export function validateRequest(route, request) {
9
- const { params, body } = getValidators(route);
10
- const issues = [];
11
- if (params != null) {
12
- collectIssues(params, request.params ?? {}, "/params", issues);
13
- }
14
- if (body != null) {
15
- collectIssues(body, request.body, "/body", issues);
16
- }
17
- if (issues.length > 0) {
18
- throw new ValidationError("Validation failed", issues);
19
- }
20
- }
21
- const ajv = new Ajv({
22
- allErrors: true,
23
- coerceTypes: false,
24
- removeAdditional: false,
25
- useDefaults: false,
26
- strict: false
27
- });
28
- /**
29
- * Compiled once per route and reused. Compiling per request was slow, and AJV keeps every
30
- * schema it compiles, so memory grew with traffic.
31
- */ const validatorsByRoute = new WeakMap();
32
- function getValidators(route) {
33
- let validators = validatorsByRoute.get(route);
34
- if (validators == null) {
35
- const schemas = buildRouteRequestSchemas(route);
36
- const paramsSchema = pathParamsToSchema(schemas.pathParams);
37
- validators = {
38
- params: paramsSchema != null ? ajv.compile(paramsSchema) : undefined,
39
- body: schemas.body != null ? ajv.compile(schemas.body) : undefined
40
- };
41
- validatorsByRoute.set(route, validators);
42
- }
43
- return validators;
44
- }
45
- function collectIssues(validate, data, rootPath, issues) {
46
- if (validate(data)) {
47
- return;
48
- }
49
- for (const err of validate.errors ?? []){
50
- issues.push(toIssue(err, rootPath));
51
- }
52
- }
53
- function toIssue(err, rootPath) {
54
- const instance = err.instancePath ?? "";
55
- const path = instance.length > 0 ? `${rootPath}${instance}` : rootPath;
56
- const message = err.message ?? "is invalid";
57
- return {
58
- path,
59
- message: formatAjvMessage(err, message),
60
- keyword: err.keyword
61
- };
62
- }
63
- function formatAjvMessage(err, fallback) {
64
- if (err.keyword === "required" && typeof err.params?.missingProperty === "string") {
65
- return `must have required property '${err.params.missingProperty}'`;
66
- }
67
- if (err.keyword === "additionalProperties" && typeof err.params?.additionalProperty === "string") {
68
- return `must NOT have additional property '${err.params.additionalProperty}'`;
69
- }
70
- return fallback;
71
- }
@@ -1,376 +0,0 @@
1
- function _ts_decorate(decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- }
7
- function _ts_metadata(k, v) {
8
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
- }
10
- function _ts_param(paramIndex, decorator) {
11
- return function(target, key) {
12
- decorator(target, key, paramIndex);
13
- };
14
- }
15
- import { randomUUID } from "node:crypto";
16
- import { stat } from "node:fs/promises";
17
- import path from "node:path";
18
- import { Readable } from "node:stream";
19
- import { inject } from "@decohere/crucible/decorators";
20
- import { scribe } from "@decohere/scribe";
21
- import compress from "@fastify/compress";
22
- import cors from "@fastify/cors";
23
- import helmet from "@fastify/helmet";
24
- import rateLimit from "@fastify/rate-limit";
25
- import fastifyStatic from "@fastify/static";
26
- import Fastify from "fastify";
27
- import { ExceptionFilterHostToken, HttpOptionsToken, RouteRegistryToken, RouterToken } from "../const.js";
28
- import { DEFAULT_HTTP_BODY_LIMIT, DEFAULT_HTTP_RATE_LIMIT } from "../http/http.security.js";
29
- import { DEFAULT_COMPRESSION_ENCODINGS, DEFAULT_COMPRESSION_THRESHOLD } from "../http/http.compression.js";
30
- import { staticFiles } from "../http/http.static.js";
31
- import { generateOpenApiDocument, resolveOpenApiPath } from "../openapi/openapi.generator.js";
32
- import { HttpRequest } from "../routing/anvil.types.js";
33
- import { DefaultHttpExceptionFilter } from "../routing/filters/exception-filter-host.js";
34
- import { REQUEST_ID_HEADER, resolveRequestId } from "../routing/middleware/request-id.js";
35
- import { normalizePrefix } from "../routing/path.util.js";
36
- import { encodeSse } from "../sse/sse.encoder.js";
37
- import { DEFAULT_SSE_HEARTBEAT_MS, SseResponse } from "../sse/sse.types.js";
38
- export class FastifyProvider {
39
- provider;
40
- router;
41
- routeRegistry;
42
- httpOptions;
43
- exceptionFilterHost;
44
- closing;
45
- logger = scribe.logger(FastifyProvider.name);
46
- /** One per live `@Sse()` request, so stopping the app can end the streams. */ openStreams = new Set();
47
- constructor(routeRegistry, router, httpOptions, exceptionFilterHost){
48
- this.routeRegistry = routeRegistry;
49
- this.router = router;
50
- this.httpOptions = httpOptions;
51
- this.exceptionFilterHost = exceptionFilterHost;
52
- this.provider = Fastify({
53
- logger: false,
54
- requestIdHeader: REQUEST_ID_HEADER,
55
- genReqId: ()=>randomUUID(),
56
- bodyLimit: httpOptions?.bodyLimit ?? DEFAULT_HTTP_BODY_LIMIT
57
- });
58
- }
59
- async start(options) {
60
- const statics = await this.resolveStatics();
61
- await this.registerHelmet(this.httpOptions?.helmet);
62
- await this.registerRateLimit(this.httpOptions?.rateLimit, statics);
63
- await this.registerCors(this.httpOptions?.cors);
64
- await this.registerCompression(this.httpOptions?.compression);
65
- this.registerOpenApi(this.httpOptions);
66
- this.registerAllRoutes(this.routeRegistry);
67
- for (const mount of statics){
68
- await this.registerStatic(mount, statics);
69
- }
70
- this.closeConnectionsWhileStopping();
71
- await this.provider.listen(options);
72
- return {
73
- addresses: this.addresses
74
- };
75
- }
76
- async stop() {
77
- // App stops HTTP before disposing the container, which calls this again.
78
- // A stream never finishes on its own, and close() waits for open responses, so end them first.
79
- for (const stream of this.openStreams){
80
- stream.abort(new Error("The app is stopping"));
81
- }
82
- this.closing ??= this.provider.close();
83
- await this.closing;
84
- return {
85
- message: "closed"
86
- };
87
- }
88
- async [Symbol.asyncDispose]() {
89
- await this.stop();
90
- }
91
- /**
92
- * Once stopping, tell clients to drop the connection after their response. Otherwise
93
- * a request that was in flight leaves a kept-alive socket that holds `close()` open
94
- * until Node's keep-alive timeout (5s).
95
- */ closeConnectionsWhileStopping() {
96
- this.provider.addHook("onSend", async (_request, reply, payload)=>{
97
- if (this.closing) reply.header("connection", "close");
98
- return payload;
99
- });
100
- }
101
- get addresses() {
102
- return this.provider.addresses().filter((a)=>a.family === "IPv4").map((a)=>`http://${a.address}:${a.port}`);
103
- }
104
- async registerHelmet(helmetOptions) {
105
- if (helmetOptions === false) {
106
- return;
107
- }
108
- await this.provider.register(helmet, helmetOptions ?? {});
109
- }
110
- async registerRateLimit(rateLimitOptions, statics) {
111
- if (rateLimitOptions === false) {
112
- return;
113
- }
114
- // Static files are outside the rate limit (ADR 0007). Matched on the route the request
115
- // resolved to, not on its URL, so only requests the static plugin serves are exempt.
116
- const staticRoutes = new Set(statics.map((mount)=>`${mount.prefix}*`));
117
- const userAllowList = rateLimitOptions?.allowList;
118
- await this.provider.register(rateLimit, {
119
- ...DEFAULT_HTTP_RATE_LIMIT,
120
- ...rateLimitOptions,
121
- allowList: (request, key)=>{
122
- if (staticRoutes.has(request.routeOptions.url ?? "")) return true;
123
- if (typeof userAllowList === "function") return userAllowList(request, key);
124
- return Array.isArray(userAllowList) && userAllowList.includes(key);
125
- }
126
- });
127
- }
128
- async resolveStatics() {
129
- return staticFiles.resolve(this.httpOptions?.statics ?? [], {
130
- nodeEnv: process.env.NODE_ENV,
131
- directoryExists: async (directory)=>(await stat(directory).catch(()=>undefined))?.isDirectory() === true,
132
- warn: (message)=>this.logger.warn(message)
133
- });
134
- }
135
- /**
136
- * One encapsulated context per mount: its not-found handler (the SPA fallback) applies to
137
- * paths under the prefix and nowhere else, so an API 404 is never answered with the UI.
138
- */ async registerStatic(mount, mounts) {
139
- const withoutSlash = mount.prefix.slice(0, -1);
140
- const isRoot = mount.prefix === "/";
141
- const excluded = staticFiles.excluder(mount.exclude);
142
- const belongsToApi = this.apiBoundary();
143
- // Requests that already fell back once: a missing index must be a 404, not a loop.
144
- const fellBack = new WeakSet();
145
- await this.provider.register(async (scope)=>{
146
- await scope.register(fastifyStatic, {
147
- root: mount.root,
148
- prefix: "/",
149
- index: mount.index,
150
- dotfiles: "ignore",
151
- list: false,
152
- redirect: false,
153
- etag: true,
154
- lastModified: true,
155
- // The header is set in setHeaders, per file.
156
- cacheControl: false,
157
- setHeaders: (reply, filePath)=>{
158
- reply.header("cache-control", staticFiles.cacheControlFor(mount, path.relative(mount.root, filePath)));
159
- }
160
- });
161
- // Excluded paths are not served at all: not as a file, not as the fallback.
162
- scope.addHook("onRequest", async (request, reply)=>{
163
- if (excluded(staticFiles.normalizePathname(request.url))) {
164
- return reply.callNotFound();
165
- }
166
- });
167
- scope.setNotFoundHandler((request, reply)=>{
168
- const [pathname = "", query] = request.url.split("?");
169
- const readOnly = request.method === "GET" || request.method === "HEAD";
170
- // `/ui` goes to `/ui/`. A request for it reaches the root mount's handler instead
171
- // when there is one, so that handler redirects for the other mounts too.
172
- const redirectTo = mounts.find((other)=>other.prefix !== "/" && other.prefix.slice(0, -1) === pathname && (isRoot || other === mount));
173
- if (readOnly && redirectTo != null) {
174
- return reply.redirect(query == null ? redirectTo.prefix : `${redirectTo.prefix}?${query}`, 301);
175
- }
176
- const normalized = staticFiles.normalizePathname(request.url);
177
- if (mount.spaFallback && !fellBack.has(request) && !excluded(normalized) && staticFiles.shouldFallBack({
178
- method: request.method,
179
- pathname: normalized,
180
- accept: request.headers.accept
181
- }, belongsToApi)) {
182
- fellBack.add(request);
183
- return reply.sendFile(mount.index);
184
- }
185
- return reply.code(404).send({
186
- message: `Route ${request.method}:${pathname} not found`,
187
- error: "Not Found",
188
- statusCode: 404
189
- });
190
- });
191
- }, {
192
- prefix: withoutSlash
193
- });
194
- }
195
- /** What the static fallback leaves to the API: the global prefix, else the registered routes. ADR 0007. */ apiBoundary() {
196
- return staticFiles.apiBoundary({
197
- globalPrefix: normalizePrefix(this.httpOptions?.globalPrefix),
198
- openApiPath: this.httpOptions?.openapi == null ? undefined : resolveOpenApiPath(this.httpOptions.openapi),
199
- routePaths: [
200
- ...this.routeRegistry.routes
201
- ].map((route)=>route.path)
202
- });
203
- }
204
- /**
205
- * `@fastify/compress` with best-practice defaults; the plugin owns the policy. Registered
206
- * before any route so every response goes through it, errors and static files included.
207
- * `providerOptions` is merged last, so a user can override any plugin option.
208
- */ async registerCompression(compressionOptions) {
209
- if (compressionOptions === false) {
210
- return;
211
- }
212
- await this.provider.register(compress, {
213
- global: true,
214
- threshold: compressionOptions?.threshold ?? DEFAULT_COMPRESSION_THRESHOLD,
215
- encodings: [
216
- ...compressionOptions?.encodings ?? DEFAULT_COMPRESSION_ENCODINGS
217
- ],
218
- ...compressionOptions?.types != null && {
219
- customTypes: compressionOptions.types
220
- },
221
- ...compressionOptions?.providerOptions
222
- });
223
- }
224
- async registerCors(corsOptions) {
225
- if (corsOptions == null) {
226
- return;
227
- }
228
- await this.provider.register(cors, {
229
- origin: corsOptions.origin,
230
- methods: corsOptions.methods,
231
- allowedHeaders: corsOptions.allowedHeaders,
232
- exposedHeaders: corsOptions.exposedHeaders,
233
- credentials: corsOptions.credentials,
234
- maxAge: corsOptions.maxAge
235
- });
236
- }
237
- registerOpenApi(httpOptions) {
238
- const openapi = httpOptions?.openapi;
239
- if (openapi == null) {
240
- return;
241
- }
242
- const path = resolveOpenApiPath(openapi);
243
- const document = generateOpenApiDocument(this.routeRegistry, openapi);
244
- this.provider.get(path, async (_request, reply)=>{
245
- reply.header("content-type", "application/json; charset=utf-8");
246
- return document;
247
- });
248
- }
249
- registerAllRoutes(registry) {
250
- for (const route of registry.routes){
251
- const { key, method, path } = route;
252
- this.provider.route({
253
- method,
254
- url: path,
255
- handler: (request, reply)=>this.routeRequest(route, key, request, reply)
256
- });
257
- }
258
- }
259
- async routeRequest(route, key, request, reply) {
260
- const httpRequest = this.fastifyToRequest(request);
261
- const disconnect = this.abortOnDisconnect(reply, httpRequest, route.sse != null);
262
- try {
263
- const response = await this.router.route(key, httpRequest);
264
- if (route.httpCode != null) {
265
- reply.status(route.httpCode);
266
- }
267
- if (route.headers != null) {
268
- for (const [name, value] of Object.entries(route.headers)){
269
- reply.header(name, value);
270
- }
271
- }
272
- this.applyResponseHeaders(reply, httpRequest);
273
- if (response instanceof SseResponse) {
274
- return this.sendEventStream(route, reply, response, disconnect);
275
- }
276
- return response;
277
- } catch (error) {
278
- const result = this.exceptionFilterHost ? await this.exceptionFilterHost.handle(error, {
279
- request: httpRequest
280
- }) : new DefaultHttpExceptionFilter().catch(error, {
281
- request: httpRequest
282
- });
283
- if (result.headers) {
284
- for (const [name, value] of Object.entries(result.headers)){
285
- reply.header(name, value);
286
- }
287
- }
288
- this.applyResponseHeaders(reply, httpRequest);
289
- // Returned so Fastify waits for the async onSend hooks instead of sending an empty reply of its own.
290
- return reply.status(result.status).send(result.body);
291
- }
292
- }
293
- /**
294
- * Gives the handler a signal that aborts when the client goes away before the response
295
- * finished. Streams are also tracked so that stopping the app can abort them.
296
- */ abortOnDisconnect(reply, httpRequest, isStream) {
297
- const controller = new AbortController();
298
- httpRequest.signal = controller.signal;
299
- if (isStream) {
300
- if (this.closing) controller.abort(new Error("The app is stopping"));
301
- this.openStreams.add(controller);
302
- }
303
- reply.raw.once("close", ()=>{
304
- this.openStreams.delete(controller);
305
- if (!reply.raw.writableFinished) {
306
- controller.abort(new Error("The client disconnected"));
307
- }
308
- });
309
- return controller;
310
- }
311
- sendEventStream(route, reply, response, disconnect) {
312
- let started = false;
313
- const release = ()=>{
314
- response.release().catch((error)=>{
315
- this.logger.error("Releasing the request scope of an SSE stream failed", error);
316
- });
317
- };
318
- const frames = encodeSse(response.events, {
319
- signal: disconnect.signal,
320
- heartbeatMs: route.sse?.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS,
321
- onProducerStopped: (stopped)=>void stopped.then(release)
322
- });
323
- const guarded = async function*() {
324
- started = true;
325
- yield* frames;
326
- }();
327
- // Never pulled if the client left first, in which case nothing else would release the scope.
328
- reply.raw.once("close", ()=>{
329
- if (!started) release();
330
- });
331
- return reply.status(route.httpCode ?? 200).header("content-type", "text/event-stream; charset=utf-8").header("cache-control", "no-cache, no-transform").header("x-accel-buffering", "no")// A stream owns its connection; closing it afterwards lets app stop finish at once instead of waiting out keep-alive.
332
- .header("connection", "close").send(Readable.from(guarded));
333
- }
334
- applyResponseHeaders(reply, httpRequest) {
335
- reply.header(REQUEST_ID_HEADER, httpRequest.requestId);
336
- for (const [name, value] of Object.entries(httpRequest.responseHeaders)){
337
- reply.header(name, value);
338
- }
339
- }
340
- fastifyToRequest(fastifyRequest) {
341
- const method = fastifyRequest.method;
342
- const url = fastifyRequest.url;
343
- const params = fastifyRequest.params;
344
- const query = fastifyRequest.query;
345
- const body = fastifyRequest.body ?? {};
346
- const headers = {};
347
- for (const [key, value] of Object.entries(fastifyRequest.headers)){
348
- if (typeof value === "string") {
349
- headers[key.toLowerCase()] = value;
350
- }
351
- }
352
- const requestId = resolveRequestId(headers, String(fastifyRequest.id ?? ""));
353
- return new HttpRequest({
354
- requestId,
355
- url,
356
- method,
357
- params,
358
- query,
359
- body,
360
- headers
361
- });
362
- }
363
- }
364
- FastifyProvider = _ts_decorate([
365
- _ts_param(0, inject(RouteRegistryToken)),
366
- _ts_param(1, inject(RouterToken)),
367
- _ts_param(2, inject(HttpOptionsToken)),
368
- _ts_param(3, inject(ExceptionFilterHostToken)),
369
- _ts_metadata("design:type", Function),
370
- _ts_metadata("design:paramtypes", [
371
- typeof RouteRegistry === "undefined" ? Object : RouteRegistry,
372
- typeof Router === "undefined" ? Object : Router,
373
- typeof HttpOptions === "undefined" ? Object : HttpOptions,
374
- typeof ExceptionFilterHost === "undefined" ? Object : ExceptionFilterHost
375
- ])
376
- ], FastifyProvider);
@@ -1 +0,0 @@
1
- export * from "./fastify.provider.js";
@@ -1,57 +0,0 @@
1
- export const RequestParts = {
2
- query: "query",
3
- params: "params",
4
- body: "body",
5
- principal: "principal",
6
- signal: "signal",
7
- lastEventId: "lastEventId"
8
- };
9
- /** Argument locations filled by the HTTP package itself rather than read from the request payload. */ export const InjectedParts = [
10
- "principal",
11
- "signal",
12
- "lastEventId"
13
- ];
14
- export class HttpRequest {
15
- constructor(data){
16
- Object.assign(this, data);
17
- }
18
- requestId = "";
19
- url;
20
- method;
21
- params = {};
22
- query = {};
23
- body;
24
- headers = {};
25
- /** Aborts when the client disconnects or the app stops. Never aborts if the provider cannot tell. */ signal = new AbortController().signal;
26
- /** Outbound headers set by middleware; applied by the HTTP provider. */ responseHeaders = {};
27
- }
28
- export class RouteConfigurationError extends Error {
29
- }
30
- export class RoutingError extends Error {
31
- }
32
- export class TransformError extends Error {
33
- }
34
- export class HttpError extends Error {
35
- status;
36
- constructor(message, status){
37
- super(message);
38
- this.status = status;
39
- }
40
- }
41
- export class UnauthorizedError extends HttpError {
42
- constructor(message = "Unauthorized"){
43
- super(message, 401);
44
- }
45
- }
46
- export class ForbiddenError extends HttpError {
47
- constructor(message = "Forbidden"){
48
- super(message, 403);
49
- }
50
- }
51
- export class ValidationError extends HttpError {
52
- errors;
53
- constructor(message = "Bad Request", errors = []){
54
- super(message, 400);
55
- this.errors = errors;
56
- }
57
- }