@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
@@ -1,184 +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 { inject, injectable } from "@decohere/crucible/decorators";
16
- import { getArgumentMeta, getFields, getFunctionArgs, getFunctions, getMeta, isConstructable, isPrimitiveOrDateType } from "@decohere/core/reflection";
17
- import { HttpOptionsToken, MetadataKeys } from "../../const.js";
18
- import { InjectedParts, RouteConfigurationError } from "../anvil.types.js";
19
- import { applyGlobalPrefix, joinRoutePath, normalizePrefix } from "../path.util.js";
20
- import { BooleanPipe, DatePipe, JsonPipe, NumberPipe } from "../pipes/pipes.js";
21
- export class RouteRegistry {
22
- routeMap = new Map();
23
- httpOptions;
24
- constructor(httpOptions){
25
- this.httpOptions = httpOptions;
26
- }
27
- addController(controller) {
28
- const funcs = getFunctions(controller);
29
- for (const func of funcs){
30
- this.addControllerRoute(controller, func);
31
- }
32
- }
33
- get routes() {
34
- return this.routeMap.values();
35
- }
36
- getRoute(key) {
37
- return this.routeMap.get(key);
38
- }
39
- addControllerRoute(controller, prop) {
40
- const func = prop.value;
41
- const handler = func.name;
42
- const route = getMeta(MetadataKeys.route, func);
43
- if (route == null) {
44
- return;
45
- }
46
- const globalPrefix = this.takesGlobalPrefix(controller) ? normalizePrefix(this.httpOptions?.globalPrefix) : "";
47
- const controllerPrefix = this.getControllerPrefix(controller);
48
- const path = applyGlobalPrefix(globalPrefix, joinRoutePath(controllerPrefix, route.path));
49
- const { method } = route;
50
- const key = `${method}|${path}`;
51
- const existing = this.routeMap.get(key);
52
- if (existing != null) {
53
- // Last one used to win silently, leaving the other handler unreachable (#503).
54
- throw new RouteConfigurationError(`Duplicate route ${method.toUpperCase()} ${path}: ${existing.controller.name}.${existing.handler} and ${controller.name}.${handler}`);
55
- }
56
- const routeArgs = this.getRouteArguments(controller, func);
57
- const httpCode = getMeta(MetadataKeys.httpCode, func);
58
- const headers = getMeta(MetadataKeys.headers, func);
59
- const sse = getMeta(MetadataKeys.sse, func);
60
- this.routeMap.set(key, {
61
- key,
62
- controller,
63
- handler,
64
- path,
65
- method,
66
- routeArgs,
67
- httpCode,
68
- headers,
69
- sse
70
- });
71
- }
72
- /** `@Controller({ globalPrefix: false })` opts out, unless the app put it back under. */ takesGlobalPrefix(controller) {
73
- const meta = getMeta(MetadataKeys.controller, controller.prototype);
74
- return meta?.globalPrefix !== false || this.httpOptions?.underGlobalPrefix.has(controller) === true;
75
- }
76
- getControllerPrefix(controller) {
77
- const meta = getMeta(MetadataKeys.controller, controller.prototype);
78
- if (meta?.prefix) {
79
- return normalizePrefix(meta.prefix);
80
- }
81
- const controllerName = controller.name.toLowerCase().replace("controller", "");
82
- return controllerName !== "" ? `/${controllerName}` : "";
83
- }
84
- getRouteArguments(controller, handler) {
85
- const types = getFunctionArgs(controller.prototype, handler) ?? [];
86
- const args = [];
87
- for (const [argumentIndex, argumentType] of types.entries()){
88
- const argMeta = getArgumentMeta(MetadataKeys.parameter, handler, argumentIndex);
89
- if (argMeta == null && !isConstructable(argumentType)) {
90
- continue;
91
- }
92
- args.push(this.getRouteArgument(argumentType, argumentIndex, handler));
93
- }
94
- return args;
95
- }
96
- getRouteArgument(argumentType, argumentIndex, handler) {
97
- const argMeta = getArgumentMeta(MetadataKeys.parameter, handler, argumentIndex);
98
- // @CurrentUser() / interface params are Object (or other non-DTO types) — no field map.
99
- if (isInjectedPart(argMeta?.argumentLocation) || !isConstructable(argumentType)) {
100
- return {
101
- ...argMeta,
102
- pipes: [],
103
- argumentIndex,
104
- argumentType: isConstructable(argumentType) ? argumentType : Object,
105
- fieldMap: new Map()
106
- };
107
- }
108
- const pipes = argMeta != null ? this.getDefaultPipes(argumentType) : [];
109
- const fieldMap = this.getFields(argumentType, argumentIndex, argMeta);
110
- const routeArg = {
111
- ...argMeta,
112
- pipes,
113
- argumentIndex,
114
- argumentType,
115
- fieldMap
116
- };
117
- return routeArg;
118
- }
119
- getFields(argumentType, argumentIndex, argMeta) {
120
- const inst = new argumentType();
121
- const fields = getFields(inst);
122
- const fieldMap = getMeta(MetadataKeys.fieldMap, argumentType.prototype) ?? new Map();
123
- for (const field of fields){
124
- const fieldKey = field.key;
125
- const fieldMeta = fieldMap.get(field.key);
126
- if (fieldMeta && fieldMeta.fieldLocation == null && argMeta?.argumentLocation == null) {
127
- throw new RouteConfigurationError(`field ${fieldMeta.fieldKey} must specify a location because location was not specified for the route argument at idx ${argumentIndex}`);
128
- }
129
- const enriched = this.enrichField(field, fieldMeta, argMeta);
130
- fieldMap.set(fieldKey, enriched);
131
- }
132
- return fieldMap;
133
- }
134
- enrichField(field, fieldMeta, argMeta) {
135
- const fieldLocation = fieldMeta?.fieldLocation ?? argMeta.argumentLocation;
136
- const pipes = this.getDefaultPipes(fieldMeta?.fieldType);
137
- return {
138
- fieldKey: field.key,
139
- mappingKey: field.key,
140
- ...fieldMeta,
141
- fieldLocation,
142
- pipes
143
- };
144
- }
145
- getDefaultPipes(argumentType) {
146
- if (argumentType == null) {
147
- return [];
148
- }
149
- let pipe;
150
- if (!isPrimitiveOrDateType(argumentType)) {
151
- pipe = JsonPipe;
152
- return [
153
- pipe
154
- ];
155
- }
156
- switch(argumentType.name){
157
- case "Number":
158
- pipe = NumberPipe;
159
- break;
160
- case "Boolean":
161
- pipe = BooleanPipe;
162
- break;
163
- case "Date":
164
- pipe = DatePipe;
165
- break;
166
- default:
167
- return [];
168
- }
169
- return [
170
- pipe
171
- ];
172
- }
173
- }
174
- RouteRegistry = _ts_decorate([
175
- injectable(),
176
- _ts_param(0, inject(HttpOptionsToken)),
177
- _ts_metadata("design:type", Function),
178
- _ts_metadata("design:paramtypes", [
179
- typeof HttpOptions === "undefined" ? Object : HttpOptions
180
- ])
181
- ], RouteRegistry);
182
- function isInjectedPart(location) {
183
- return location != null && InjectedParts.includes(location);
184
- }
@@ -1,148 +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 { LifetimeScopeToken } from "@decohere/crucible";
16
- import { inject, injectable, optional } from "@decohere/crucible/decorators";
17
- import { scribe } from "@decohere/scribe";
18
- import { AuthorizationServiceToken, HttpOptionsToken, IPrincipalProviderToken, RouteRegistryToken } from "../../const.js";
19
- import { validateRequest } from "../../openapi/request-validator.js";
20
- import { SseResponse } from "../../sse/sse.types.js";
21
- import { RoutingError } from "../anvil.types.js";
22
- import { getAuthorizeOptions, isAllowAnonymous } from "../decorators/auth.decorator.js";
23
- import { composeMiddleware } from "../middleware/middleware.js";
24
- import { RouteArgumentResolve } from "./route-argument-resolver.js";
25
- import { RouteRegistry } from "./route-registry.js";
26
- export class Router {
27
- argResolver = new RouteArgumentResolve();
28
- lifetimeScope;
29
- routeRegistry;
30
- authService;
31
- httpOptions;
32
- logger = scribe.logger(Router.name);
33
- constructor(routeRegistry, lifetimeScope, authService, httpOptions){
34
- this.routeRegistry = routeRegistry;
35
- this.lifetimeScope = lifetimeScope;
36
- this.authService = authService;
37
- this.httpOptions = httpOptions;
38
- this.assertAuthConfiguredForProtectedRoutes();
39
- }
40
- /**
41
- * Without an authorization service nothing checks tokens, so a route marked @Authorize
42
- * would be served to anyone. Refuse to start instead of failing open (#500).
43
- */ assertAuthConfiguredForProtectedRoutes() {
44
- if (this.authService != null) {
45
- return;
46
- }
47
- const protectedRoutes = [
48
- ...this.routeRegistry.routes
49
- ].filter(({ controller, handler })=>getAuthorizeOptions(controller.prototype, controller.prototype[handler])).map(({ controller, handler })=>`${controller.name}.${String(handler)}`);
50
- if (protectedRoutes.length > 0) {
51
- throw new RoutingError(`Routes use @Authorize but auth is not configured, so they would be open to anyone: ${protectedRoutes.join(", ")}. Add .withAuth() to the builder, or remove @Authorize.`);
52
- }
53
- }
54
- async route(routeKey, httpRequest) {
55
- const routeConfig = this.routeRegistry.getRoute(routeKey);
56
- if (!routeConfig) {
57
- throw new RoutingError(`Route was not configured for key ${routeKey}`);
58
- }
59
- const { controller, handler, routeArgs } = routeConfig;
60
- // Unnamed: the request id comes from the client, so it is not unique (#497).
61
- // Disposed before we respond, so async cleanup (e.g. rolling back a DB transaction)
62
- // finishes first. An SSE route is the exception: its scope lives as long as the stream.
63
- const scope = this.lifetimeScope.beginScope();
64
- let scopeHandedOff = false;
65
- // Runs inside the middleware chain so rejections (401/403/400) reach middleware such as
66
- // the access log. Auth comes first so anonymous callers learn nothing about the body (#501).
67
- const handle = async ()=>{
68
- const principal = await this.authorize(routeConfig, httpRequest);
69
- if (this.httpOptions?.openapi?.validateRequests !== false && this.httpOptions?.openapi) {
70
- validateRequest(routeConfig, httpRequest);
71
- }
72
- const provider = scope.resolve(IPrincipalProviderToken);
73
- provider.setPrincipal(principal);
74
- const controllerInstance = scope.resolve(controller);
75
- const func = controllerInstance[handler];
76
- const argValues = this.argResolver.resolveArguments(httpRequest, routeArgs, provider);
77
- try {
78
- const result = await func.apply(controllerInstance, argValues);
79
- if (routeConfig.sse == null) {
80
- return result;
81
- }
82
- if (typeof result?.[Symbol.asyncIterator] !== "function") {
83
- throw new RoutingError(`${controller.name}.${String(handler)} is marked @Sse() so it must return an AsyncIterable (an async generator method works)`);
84
- }
85
- scopeHandedOff = true;
86
- return new SseResponse(result, ()=>scope[Symbol.asyncDispose]());
87
- } catch (error) {
88
- this.logger.error(error.message, error.stack);
89
- throw error;
90
- }
91
- };
92
- try {
93
- const middlewares = this.resolveMiddlewares(scope);
94
- if (middlewares.length === 0) {
95
- return await handle();
96
- }
97
- return await composeMiddleware(middlewares, handle, {
98
- request: httpRequest,
99
- route: routeConfig
100
- });
101
- } catch (error) {
102
- // A middleware failed after the handler returned: nobody will consume the stream.
103
- scopeHandedOff = false;
104
- throw error;
105
- } finally{
106
- if (!scopeHandedOff) {
107
- await scope[Symbol.asyncDispose]();
108
- }
109
- }
110
- }
111
- resolveMiddlewares(scope) {
112
- const types = this.httpOptions?.middlewareTypes ?? [];
113
- return types.map((type)=>scope.resolve(type));
114
- }
115
- async authorize(routeConfig, httpRequest) {
116
- // Optional DI may resolve as null when auth is not registered.
117
- if (this.authService == null) {
118
- return;
119
- }
120
- const { controller, handler } = routeConfig;
121
- const handlerFn = controller.prototype[handler];
122
- if (isAllowAnonymous(controller.prototype, handlerFn)) {
123
- return;
124
- }
125
- const authOptions = getAuthorizeOptions(controller.prototype, handlerFn);
126
- if (authOptions === undefined) {
127
- // Auth is enabled but the route has no @Authorize — require a valid
128
- // token (fail closed) without checking a specific ability.
129
- return this.authService.authenticateRequest(httpRequest);
130
- }
131
- return this.authService.authorizeRequest(httpRequest, authOptions.action, authOptions.subject);
132
- }
133
- }
134
- Router = _ts_decorate([
135
- injectable(),
136
- _ts_param(0, inject(RouteRegistryToken)),
137
- _ts_param(1, inject(LifetimeScopeToken)),
138
- _ts_param(2, inject(AuthorizationServiceToken)),
139
- _ts_param(2, optional()),
140
- _ts_param(3, inject(HttpOptionsToken)),
141
- _ts_metadata("design:type", Function),
142
- _ts_metadata("design:paramtypes", [
143
- typeof RouteRegistry === "undefined" ? Object : RouteRegistry,
144
- typeof LifetimeScope === "undefined" ? Object : LifetimeScope,
145
- typeof IAuthorizationService === "undefined" ? Object : IAuthorizationService,
146
- typeof HttpOptions === "undefined" ? Object : HttpOptions
147
- ])
148
- ], Router);
@@ -1,2 +0,0 @@
1
- export { LastEventId, Signal, Sse } from "./sse.decorator.js";
2
- export { SseResponse } from "./sse.types.js";
@@ -1,31 +0,0 @@
1
- import { defineArgumentMeta, defineMeta } from "@decohere/core/reflection";
2
- import { MetadataKeys } from "../const.js";
3
- /**
4
- * Mark a route as a server-sent event stream. The method returns an `AsyncIterable` of
5
- * {@link SseMessage}s (an `async *` method is the usual shape) and the HTTP package writes it as
6
- * `text/event-stream`. Auth, validation and middleware run first, exactly as on any route,
7
- * so an unauthorised caller gets a normal 401/403 and no event is ever sent.
8
- *
9
- * Combine with `@Get()` (EventSource only issues GET).
10
- */ export function Sse(options = {}) {
11
- return (_target, _methodName, descriptor)=>{
12
- defineMeta(MetadataKeys.sse, options, descriptor.value);
13
- };
14
- }
15
- function injected(location) {
16
- return ()=>(target, propertyKey, parameterIndex)=>{
17
- const fn = target[propertyKey];
18
- const meta = {
19
- argumentIndex: parameterIndex,
20
- argumentLocation: location,
21
- mappingKey: undefined,
22
- pipes: []
23
- };
24
- defineArgumentMeta(MetadataKeys.parameter, meta, fn, parameterIndex);
25
- };
26
- }
27
- /**
28
- * Inject an `AbortSignal` that aborts when the client disconnects or the app stops. Pass it to
29
- * whatever the producer waits on (timers, subscriptions, queries) so it stops promptly.
30
- */ export const Signal = injected("signal");
31
- /** Inject the `Last-Event-ID` header the browser sends when it reconnects, or `undefined`. */ export const LastEventId = injected("lastEventId");
@@ -1,67 +0,0 @@
1
- import { scribe } from "@decohere/scribe";
2
- import { sseWire } from "./sse.wire.js";
3
- const logger = scribe.logger("SseEncoder");
4
- /**
5
- * Turn the handler's events into SSE text. Writes an opening comment so the browser sees the
6
- * response headers at once, a heartbeat whenever the producer is quiet, and stops as soon as the
7
- * signal aborts, without waiting for a producer that is stuck.
8
- */ export async function* encodeSse(source, options) {
9
- const { signal, heartbeatMs } = options;
10
- const iterator = source[Symbol.asyncIterator]();
11
- let onAbort;
12
- const abortStep = new Promise((resolve)=>{
13
- onAbort = ()=>resolve({
14
- kind: "abort"
15
- });
16
- if (signal.aborted) onAbort();
17
- else signal.addEventListener("abort", onAbort, {
18
- once: true
19
- });
20
- });
21
- let pending;
22
- try {
23
- yield sseWire.formatComment("open");
24
- while(!signal.aborted){
25
- pending ??= iterator.next().then((result)=>({
26
- kind: "item",
27
- result
28
- }));
29
- let timer;
30
- const steps = [
31
- pending,
32
- abortStep
33
- ];
34
- if (heartbeatMs > 0) {
35
- steps.push(new Promise((resolve)=>{
36
- timer = setTimeout(()=>resolve({
37
- kind: "heartbeat"
38
- }), heartbeatMs);
39
- }));
40
- }
41
- let step;
42
- try {
43
- step = await Promise.race(steps);
44
- } finally{
45
- clearTimeout(timer);
46
- }
47
- if (step.kind === "abort") return;
48
- if (step.kind === "heartbeat") {
49
- yield sseWire.formatComment("heartbeat");
50
- continue;
51
- }
52
- pending = undefined;
53
- if (step.result.done) return;
54
- yield sseWire.formatMessage(step.result.value);
55
- }
56
- } catch (error) {
57
- // Headers are already sent, so the only honest thing left is to log and end the stream.
58
- if (!signal.aborted) logger.error("SSE producer failed", error);
59
- } finally{
60
- if (onAbort) signal.removeEventListener("abort", onAbort);
61
- // If the producer has not answered its last `next()`, `return()` waits behind it. That is
62
- // why handlers must pass the signal to whatever they wait on.
63
- options.onProducerStopped(Promise.resolve().then(()=>iterator.return?.()).then(()=>undefined, (error)=>{
64
- if (!signal.aborted) logger.error("SSE producer failed while stopping", error);
65
- }));
66
- }
67
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * One server-sent event. A bare `string` yielded by a handler is shorthand for `{ data }`.
3
- *
4
- * `data` is written as-is when it is a string and as JSON otherwise.
5
- */ export const DEFAULT_SSE_HEARTBEAT_MS = 15_000;
6
- /**
7
- * What the router hands the HTTP provider for an `@Sse()` route. The request-scoped services the
8
- * handler uses must stay alive for as long as the stream runs, so the provider calls `release()`
9
- * once the stream is over, instead of the router disposing them as soon as the handler returns.
10
- */ export class SseResponse {
11
- events;
12
- onRelease;
13
- released;
14
- constructor(events, onRelease){
15
- this.events = events;
16
- this.onRelease = onRelease;
17
- }
18
- /** Safe to call more than once. */ release() {
19
- this.released ??= this.onRelease();
20
- return this.released;
21
- }
22
- }
@@ -1,30 +0,0 @@
1
- /** Line breaks would end a field early, so a client-controlled `id` could inject extra fields. */ function singleLine(value) {
2
- return value.replace(/[\r\n]+/g, " ");
3
- }
4
- function toMessage(item) {
5
- if (typeof item === "object" && item !== null && "data" in item) {
6
- return item;
7
- }
8
- return {
9
- data: item
10
- };
11
- }
12
- /** Encode one event as the text a browser's `EventSource` parses. */ function formatMessage(item) {
13
- const { data, event, id, retry } = toMessage(item);
14
- let frame = "";
15
- if (id != null) frame += `id: ${singleLine(String(id))}\n`;
16
- if (event != null) frame += `event: ${singleLine(event)}\n`;
17
- if (retry != null && Number.isFinite(retry)) frame += `retry: ${Math.trunc(retry)}\n`;
18
- const text = typeof data === "string" ? data : JSON.stringify(data) ?? "";
19
- for (const line of text.split(/\r\n|\r|\n/)){
20
- frame += `data: ${line}\n`;
21
- }
22
- return `${frame}\n`;
23
- }
24
- /** A comment line: ignored by `EventSource`, but it is traffic on the wire. */ function formatComment(text) {
25
- return `: ${singleLine(text)}\n\n`;
26
- }
27
- export const sseWire = {
28
- formatMessage,
29
- formatComment
30
- };
@@ -1,35 +0,0 @@
1
- import { Builder } from "../builder/builder.js";
2
- import "../http/http.extensions.js";
3
- import { FastifyProvider } from "../providers/fastify.provider.js";
4
- import { readEventStream } from "./sse-reader.js";
5
- /**
6
- * Boot the given controllers as a real app on a free loopback port, so a test can call it over
7
- * HTTP. The app runs the full pipeline: routing, auth, middleware, and the real provider.
8
- *
9
- * @example
10
- * await using api = await bootModule({ controllers: [PriceController] });
11
- * const { events } = await api.readEvents("/prices/stream", { count: 3 });
12
- */ export async function bootModule(options) {
13
- let builder = Builder.create().withHttp(options.provider ?? FastifyProvider).withControllers(...options.controllers);
14
- if (options.services) builder = builder.withServices(options.services);
15
- options.configure?.(builder);
16
- const app = await builder.build();
17
- await app.start({
18
- port: 0,
19
- host: "127.0.0.1",
20
- shutdownSignals: false
21
- });
22
- const baseUrl = app.addresses[0];
23
- if (baseUrl == null) {
24
- await app.dispose();
25
- throw new Error("app.start({ port: 0 }) advertised no IPv4 address");
26
- }
27
- const stop = ()=>app.dispose();
28
- return {
29
- app,
30
- baseUrl,
31
- readEvents: (path, readOptions)=>readEventStream(`${baseUrl}${path}`, readOptions),
32
- stop,
33
- [Symbol.asyncDispose]: stop
34
- };
35
- }
@@ -1,2 +0,0 @@
1
- export { bootModule } from "./boot-module.js";
2
- export { readEventStream } from "./sse-reader.js";
@@ -1,91 +0,0 @@
1
- /** Incremental parser for the `text/event-stream` format. */ class SseParser {
2
- buffer = "";
3
- data = [];
4
- current = {
5
- data: ""
6
- };
7
- hasFields = false;
8
- events = [];
9
- comments = [];
10
- push(chunk) {
11
- this.buffer += chunk;
12
- let newline;
13
- while((newline = this.buffer.search(/\r\n|\r|\n/)) !== -1){
14
- const line = this.buffer.slice(0, newline);
15
- const eol = this.buffer.slice(newline).startsWith("\r\n") ? 2 : 1;
16
- this.buffer = this.buffer.slice(newline + eol);
17
- this.line(line);
18
- }
19
- }
20
- line(line) {
21
- if (line === "") {
22
- if (this.hasFields) {
23
- this.events.push({
24
- ...this.current,
25
- data: this.data.join("\n")
26
- });
27
- }
28
- this.data = [];
29
- this.current = {
30
- data: ""
31
- };
32
- this.hasFields = false;
33
- return;
34
- }
35
- if (line.startsWith(":")) {
36
- this.comments.push(line.slice(1).trim());
37
- return;
38
- }
39
- const colon = line.indexOf(":");
40
- const field = colon === -1 ? line : line.slice(0, colon);
41
- let value = colon === -1 ? "" : line.slice(colon + 1);
42
- if (value.startsWith(" ")) value = value.slice(1);
43
- this.hasFields = true;
44
- if (field === "data") this.data.push(value);
45
- else if (field === "event") this.current.event = value;
46
- else if (field === "id") this.current.id = value;
47
- else if (field === "retry") this.current.retry = Number(value);
48
- }
49
- }
50
- /**
51
- * GET `url` as an `EventSource` would and collect events until `count` have arrived or the
52
- * server ends the stream. A non-200 answer (401, 403, ...) returns at once with no events.
53
- */ export async function readEventStream(url, options = {}) {
54
- const count = options.count ?? 1;
55
- const abort = new AbortController();
56
- const timer = setTimeout(()=>abort.abort(new Error(`No ${count} events from ${url} in time`)), options.timeoutMs ?? 5000);
57
- try {
58
- const response = await fetch(url, {
59
- headers: {
60
- accept: "text/event-stream",
61
- ...options.headers
62
- },
63
- signal: abort.signal
64
- });
65
- const parser = new SseParser();
66
- let ended = false;
67
- if (response.status === 200 && response.body != null) {
68
- const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
69
- while(parser.events.length < count){
70
- const { done, value } = await reader.read();
71
- if (done) {
72
- ended = true;
73
- break;
74
- }
75
- parser.push(value);
76
- }
77
- await reader.cancel();
78
- } else {
79
- await response.body?.cancel();
80
- }
81
- return {
82
- status: response.status,
83
- headers: response.headers,
84
- events: parser.events.slice(0, count),
85
- comments: parser.comments,
86
- ended
87
- };
88
- } finally{
89
- clearTimeout(timer);
90
- }
91
- }
@@ -1,41 +0,0 @@
1
- import { type LifetimeScope } from "@decohere/crucible";
2
- import { type IConfigurationService } from "@decohere/core/configuration";
3
- import type { HostedServiceHost } from "../hosting/hosted-service-host.js";
4
- import type { HttpProvider, StartResponse } from "../routing/anvil.types.js";
5
- export declare class ApiConfiguration {
6
- port: number;
7
- }
8
- export type StartOptions = {
9
- port: number;
10
- host?: string;
11
- /**
12
- * Signals that stop the app gracefully: stop accepting connections, let in-flight
13
- * requests finish, stop hosted services, then dispose services. Default SIGTERM and SIGINT; `false` to
14
- * handle signals yourself.
15
- */
16
- shutdownSignals?: readonly NodeJS.Signals[] | false;
17
- /** How long a signalled shutdown may take before the process exits with code 1. Default 10s. */
18
- shutdownTimeoutMs?: number;
19
- };
20
- export declare class App {
21
- private readonly container;
22
- private configurationService?;
23
- private httpProvider?;
24
- private hostedServices?;
25
- private _addresses;
26
- private logger;
27
- private disposing;
28
- private disposingContainer;
29
- private signalHandler;
30
- private shutdownSignals;
31
- constructor(container: LifetimeScope, configurationService?: IConfigurationService, httpProvider?: HttpProvider, hostedServices?: HostedServiceHost);
32
- get addresses(): readonly string[];
33
- start(options?: StartOptions): Promise<StartResponse>;
34
- [Symbol.asyncDispose](): Promise<void>;
35
- /** Stop the app. Calling it again (or from a signal) returns the same shutdown. */
36
- dispose(): Promise<void>;
37
- private stop;
38
- private stopOnSignals;
39
- private removeSignalHandlers;
40
- private getPort;
41
- }
@@ -1,15 +0,0 @@
1
- import type { ContainerBuilder } from "@decohere/crucible";
2
- import type { Constructable } from "@decohere/core/reflection";
3
- export type ModuleDefinition = {
4
- name: string;
5
- entities?: Constructable[];
6
- repositories?: Constructable[];
7
- registerServices?(containerBuilder: ContainerBuilder): void;
8
- };
9
- export declare abstract class Module {
10
- abstract name: string;
11
- constructor(data: Partial<Module>);
12
- readonly entities?: readonly Constructable[];
13
- readonly repositories?: readonly Constructable[];
14
- abstract registerServices?(containerBuilder: ContainerBuilder): void;
15
- }