@beignet/core 0.0.1

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 (331) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +288 -0
  3. package/dist/application/index.d.ts +260 -0
  4. package/dist/application/index.d.ts.map +1 -0
  5. package/dist/application/index.js +324 -0
  6. package/dist/application/index.js.map +1 -0
  7. package/dist/client/client.d.ts +241 -0
  8. package/dist/client/client.d.ts.map +1 -0
  9. package/dist/client/client.js +531 -0
  10. package/dist/client/client.js.map +1 -0
  11. package/dist/client/index.d.ts +10 -0
  12. package/dist/client/index.d.ts.map +1 -0
  13. package/dist/client/index.js +8 -0
  14. package/dist/client/index.js.map +1 -0
  15. package/dist/client/types.d.ts +139 -0
  16. package/dist/client/types.d.ts.map +1 -0
  17. package/dist/client/types.js +2 -0
  18. package/dist/client/types.js.map +1 -0
  19. package/dist/config/index.d.ts +122 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +216 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/contracts/contract-builder.d.ts +121 -0
  24. package/dist/contracts/contract-builder.d.ts.map +1 -0
  25. package/dist/contracts/contract-builder.js +346 -0
  26. package/dist/contracts/contract-builder.js.map +1 -0
  27. package/dist/contracts/contract-group.d.ts +106 -0
  28. package/dist/contracts/contract-group.d.ts.map +1 -0
  29. package/dist/contracts/contract-group.js +240 -0
  30. package/dist/contracts/contract-group.js.map +1 -0
  31. package/dist/contracts/contract-like.d.ts +21 -0
  32. package/dist/contracts/contract-like.d.ts.map +1 -0
  33. package/dist/contracts/contract-like.js +9 -0
  34. package/dist/contracts/contract-like.js.map +1 -0
  35. package/dist/contracts/index.d.ts +15 -0
  36. package/dist/contracts/index.d.ts.map +1 -0
  37. package/dist/contracts/index.js +11 -0
  38. package/dist/contracts/index.js.map +1 -0
  39. package/dist/contracts/openapi-meta.d.ts +23 -0
  40. package/dist/contracts/openapi-meta.d.ts.map +1 -0
  41. package/dist/contracts/openapi-meta.js +2 -0
  42. package/dist/contracts/openapi-meta.js.map +1 -0
  43. package/dist/contracts/path-template.d.ts +17 -0
  44. package/dist/contracts/path-template.d.ts.map +1 -0
  45. package/dist/contracts/path-template.js +50 -0
  46. package/dist/contracts/path-template.js.map +1 -0
  47. package/dist/contracts/rate-limit.d.ts +50 -0
  48. package/dist/contracts/rate-limit.d.ts.map +1 -0
  49. package/dist/contracts/rate-limit.js +2 -0
  50. package/dist/contracts/rate-limit.js.map +1 -0
  51. package/dist/contracts/types.d.ts +97 -0
  52. package/dist/contracts/types.d.ts.map +1 -0
  53. package/dist/contracts/types.js +54 -0
  54. package/dist/contracts/types.js.map +1 -0
  55. package/dist/contracts/utils.d.ts +3 -0
  56. package/dist/contracts/utils.d.ts.map +1 -0
  57. package/dist/contracts/utils.js +44 -0
  58. package/dist/contracts/utils.js.map +1 -0
  59. package/dist/domain/entity.d.ts +87 -0
  60. package/dist/domain/entity.d.ts.map +1 -0
  61. package/dist/domain/entity.js +155 -0
  62. package/dist/domain/entity.js.map +1 -0
  63. package/dist/domain/events.d.ts +41 -0
  64. package/dist/domain/events.d.ts.map +1 -0
  65. package/dist/domain/events.js +21 -0
  66. package/dist/domain/events.js.map +1 -0
  67. package/dist/domain/index.d.ts +14 -0
  68. package/dist/domain/index.d.ts.map +1 -0
  69. package/dist/domain/index.js +14 -0
  70. package/dist/domain/index.js.map +1 -0
  71. package/dist/domain/value-object.d.ts +60 -0
  72. package/dist/domain/value-object.d.ts.map +1 -0
  73. package/dist/domain/value-object.js +87 -0
  74. package/dist/domain/value-object.js.map +1 -0
  75. package/dist/errors/catalog.d.ts +71 -0
  76. package/dist/errors/catalog.d.ts.map +1 -0
  77. package/dist/errors/catalog.js +71 -0
  78. package/dist/errors/catalog.js.map +1 -0
  79. package/dist/errors/http.d.ts +77 -0
  80. package/dist/errors/http.d.ts.map +1 -0
  81. package/dist/errors/http.js +74 -0
  82. package/dist/errors/http.js.map +1 -0
  83. package/dist/errors/index.d.ts +10 -0
  84. package/dist/errors/index.d.ts.map +1 -0
  85. package/dist/errors/index.js +14 -0
  86. package/dist/errors/index.js.map +1 -0
  87. package/dist/errors/response.d.ts +26 -0
  88. package/dist/errors/response.d.ts.map +1 -0
  89. package/dist/errors/response.js +34 -0
  90. package/dist/errors/response.js.map +1 -0
  91. package/dist/errors/validation.d.ts +18 -0
  92. package/dist/errors/validation.d.ts.map +1 -0
  93. package/dist/errors/validation.js +21 -0
  94. package/dist/errors/validation.js.map +1 -0
  95. package/dist/events/index.d.ts +58 -0
  96. package/dist/events/index.d.ts.map +1 -0
  97. package/dist/events/index.js +102 -0
  98. package/dist/events/index.js.map +1 -0
  99. package/dist/jobs/index.d.ts +56 -0
  100. package/dist/jobs/index.d.ts.map +1 -0
  101. package/dist/jobs/index.js +89 -0
  102. package/dist/jobs/index.js.map +1 -0
  103. package/dist/mail/index.d.ts +75 -0
  104. package/dist/mail/index.d.ts.map +1 -0
  105. package/dist/mail/index.js +84 -0
  106. package/dist/mail/index.js.map +1 -0
  107. package/dist/openapi/index.d.ts +207 -0
  108. package/dist/openapi/index.d.ts.map +1 -0
  109. package/dist/openapi/index.js +449 -0
  110. package/dist/openapi/index.js.map +1 -0
  111. package/dist/openapi/schema-introspector.d.ts +38 -0
  112. package/dist/openapi/schema-introspector.d.ts.map +1 -0
  113. package/dist/openapi/schema-introspector.js +67 -0
  114. package/dist/openapi/schema-introspector.js.map +1 -0
  115. package/dist/ports/audit.d.ts +58 -0
  116. package/dist/ports/audit.d.ts.map +1 -0
  117. package/dist/ports/audit.js +74 -0
  118. package/dist/ports/audit.js.map +1 -0
  119. package/dist/ports/auth.d.ts +23 -0
  120. package/dist/ports/auth.d.ts.map +1 -0
  121. package/dist/ports/auth.js +31 -0
  122. package/dist/ports/auth.js.map +1 -0
  123. package/dist/ports/builder.d.ts +61 -0
  124. package/dist/ports/builder.d.ts.map +1 -0
  125. package/dist/ports/builder.js +48 -0
  126. package/dist/ports/builder.js.map +1 -0
  127. package/dist/ports/cache.d.ts +15 -0
  128. package/dist/ports/cache.d.ts.map +1 -0
  129. package/dist/ports/cache.js +57 -0
  130. package/dist/ports/cache.js.map +1 -0
  131. package/dist/ports/clock.d.ts +10 -0
  132. package/dist/ports/clock.d.ts.map +1 -0
  133. package/dist/ports/clock.js +21 -0
  134. package/dist/ports/clock.js.map +1 -0
  135. package/dist/ports/events.d.ts +71 -0
  136. package/dist/ports/events.d.ts.map +1 -0
  137. package/dist/ports/events.js +2 -0
  138. package/dist/ports/events.js.map +1 -0
  139. package/dist/ports/id-generator.d.ts +12 -0
  140. package/dist/ports/id-generator.d.ts.map +1 -0
  141. package/dist/ports/id-generator.js +22 -0
  142. package/dist/ports/id-generator.js.map +1 -0
  143. package/dist/ports/index.d.ts +98 -0
  144. package/dist/ports/index.d.ts.map +1 -0
  145. package/dist/ports/index.js +67 -0
  146. package/dist/ports/index.js.map +1 -0
  147. package/dist/ports/logger.d.ts +22 -0
  148. package/dist/ports/logger.d.ts.map +1 -0
  149. package/dist/ports/logger.js +34 -0
  150. package/dist/ports/logger.js.map +1 -0
  151. package/dist/ports/mailer.d.ts +6 -0
  152. package/dist/ports/mailer.d.ts.map +1 -0
  153. package/dist/ports/mailer.js +2 -0
  154. package/dist/ports/mailer.js.map +1 -0
  155. package/dist/ports/policy.d.ts +53 -0
  156. package/dist/ports/policy.d.ts.map +1 -0
  157. package/dist/ports/policy.js +81 -0
  158. package/dist/ports/policy.js.map +1 -0
  159. package/dist/ports/rate-limit.d.ts +41 -0
  160. package/dist/ports/rate-limit.d.ts.map +1 -0
  161. package/dist/ports/rate-limit.js +37 -0
  162. package/dist/ports/rate-limit.js.map +1 -0
  163. package/dist/ports/redaction.d.ts +26 -0
  164. package/dist/ports/redaction.d.ts.map +1 -0
  165. package/dist/ports/redaction.js +126 -0
  166. package/dist/ports/redaction.js.map +1 -0
  167. package/dist/ports/schedules.d.ts +9 -0
  168. package/dist/ports/schedules.d.ts.map +1 -0
  169. package/dist/ports/schedules.js +2 -0
  170. package/dist/ports/schedules.js.map +1 -0
  171. package/dist/ports/storage.d.ts +47 -0
  172. package/dist/ports/storage.d.ts.map +1 -0
  173. package/dist/ports/storage.js +185 -0
  174. package/dist/ports/storage.js.map +1 -0
  175. package/dist/ports/testing.d.ts +73 -0
  176. package/dist/ports/testing.d.ts.map +1 -0
  177. package/dist/ports/testing.js +105 -0
  178. package/dist/ports/testing.js.map +1 -0
  179. package/dist/ports/unit-of-work.d.ts +56 -0
  180. package/dist/ports/unit-of-work.d.ts.map +1 -0
  181. package/dist/ports/unit-of-work.js +64 -0
  182. package/dist/ports/unit-of-work.js.map +1 -0
  183. package/dist/providers/index.d.ts +8 -0
  184. package/dist/providers/index.d.ts.map +1 -0
  185. package/dist/providers/index.js +8 -0
  186. package/dist/providers/index.js.map +1 -0
  187. package/dist/providers/instrumentation.d.ts +91 -0
  188. package/dist/providers/instrumentation.d.ts.map +1 -0
  189. package/dist/providers/instrumentation.js +93 -0
  190. package/dist/providers/instrumentation.js.map +1 -0
  191. package/dist/providers/provider.d.ts +146 -0
  192. package/dist/providers/provider.d.ts.map +1 -0
  193. package/dist/providers/provider.js +31 -0
  194. package/dist/providers/provider.js.map +1 -0
  195. package/dist/schedules/index.d.ts +105 -0
  196. package/dist/schedules/index.d.ts.map +1 -0
  197. package/dist/schedules/index.js +178 -0
  198. package/dist/schedules/index.js.map +1 -0
  199. package/dist/server/contract-like.d.ts +5 -0
  200. package/dist/server/contract-like.d.ts.map +1 -0
  201. package/dist/server/contract-like.js +5 -0
  202. package/dist/server/contract-like.js.map +1 -0
  203. package/dist/server/health.d.ts +41 -0
  204. package/dist/server/health.d.ts.map +1 -0
  205. package/dist/server/health.js +46 -0
  206. package/dist/server/health.js.map +1 -0
  207. package/dist/server/hooks/auth.d.ts +42 -0
  208. package/dist/server/hooks/auth.d.ts.map +1 -0
  209. package/dist/server/hooks/auth.js +61 -0
  210. package/dist/server/hooks/auth.js.map +1 -0
  211. package/dist/server/hooks/cors.d.ts +13 -0
  212. package/dist/server/hooks/cors.d.ts.map +1 -0
  213. package/dist/server/hooks/cors.js +70 -0
  214. package/dist/server/hooks/cors.js.map +1 -0
  215. package/dist/server/hooks/errors.d.ts +66 -0
  216. package/dist/server/hooks/errors.d.ts.map +1 -0
  217. package/dist/server/hooks/errors.js +83 -0
  218. package/dist/server/hooks/errors.js.map +1 -0
  219. package/dist/server/hooks/index.d.ts +12 -0
  220. package/dist/server/hooks/index.d.ts.map +1 -0
  221. package/dist/server/hooks/index.js +12 -0
  222. package/dist/server/hooks/index.js.map +1 -0
  223. package/dist/server/hooks/logging.d.ts +33 -0
  224. package/dist/server/hooks/logging.d.ts.map +1 -0
  225. package/dist/server/hooks/logging.js +90 -0
  226. package/dist/server/hooks/logging.js.map +1 -0
  227. package/dist/server/hooks/rate-limit.d.ts +29 -0
  228. package/dist/server/hooks/rate-limit.d.ts.map +1 -0
  229. package/dist/server/hooks/rate-limit.js +93 -0
  230. package/dist/server/hooks/rate-limit.js.map +1 -0
  231. package/dist/server/hooks/utils.d.ts +9 -0
  232. package/dist/server/hooks/utils.d.ts.map +1 -0
  233. package/dist/server/hooks/utils.js +16 -0
  234. package/dist/server/hooks/utils.js.map +1 -0
  235. package/dist/server/hooks.d.ts +2 -0
  236. package/dist/server/hooks.d.ts.map +1 -0
  237. package/dist/server/hooks.js +2 -0
  238. package/dist/server/hooks.js.map +1 -0
  239. package/dist/server/http.d.ts +124 -0
  240. package/dist/server/http.d.ts.map +1 -0
  241. package/dist/server/http.js +2 -0
  242. package/dist/server/http.js.map +1 -0
  243. package/dist/server/index.d.ts +19 -0
  244. package/dist/server/index.d.ts.map +1 -0
  245. package/dist/server/index.js +15 -0
  246. package/dist/server/index.js.map +1 -0
  247. package/dist/server/openapi.d.ts +32 -0
  248. package/dist/server/openapi.d.ts.map +1 -0
  249. package/dist/server/openapi.js +43 -0
  250. package/dist/server/openapi.js.map +1 -0
  251. package/dist/server/providers/index.d.ts +4 -0
  252. package/dist/server/providers/index.d.ts.map +1 -0
  253. package/dist/server/providers/index.js +4 -0
  254. package/dist/server/providers/index.js.map +1 -0
  255. package/dist/server/providers/loadProviderConfig.d.ts +7 -0
  256. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -0
  257. package/dist/server/providers/loadProviderConfig.js +42 -0
  258. package/dist/server/providers/loadProviderConfig.js.map +1 -0
  259. package/dist/server/server.d.ts +86 -0
  260. package/dist/server/server.d.ts.map +1 -0
  261. package/dist/server/server.js +1031 -0
  262. package/dist/server/server.js.map +1 -0
  263. package/dist/server/types.d.ts +3 -0
  264. package/dist/server/types.d.ts.map +1 -0
  265. package/dist/server/types.js +3 -0
  266. package/dist/server/types.js.map +1 -0
  267. package/package.json +129 -0
  268. package/src/application/index.ts +747 -0
  269. package/src/client/client.ts +1105 -0
  270. package/src/client/index.ts +45 -0
  271. package/src/client/types.ts +305 -0
  272. package/src/config/index.ts +497 -0
  273. package/src/contracts/contract-builder.ts +583 -0
  274. package/src/contracts/contract-group.ts +502 -0
  275. package/src/contracts/contract-like.ts +29 -0
  276. package/src/contracts/index.ts +53 -0
  277. package/src/contracts/openapi-meta.ts +22 -0
  278. package/src/contracts/path-template.ts +91 -0
  279. package/src/contracts/rate-limit.ts +50 -0
  280. package/src/contracts/types.ts +207 -0
  281. package/src/contracts/utils.ts +56 -0
  282. package/src/domain/entity.ts +256 -0
  283. package/src/domain/events.ts +52 -0
  284. package/src/domain/index.ts +18 -0
  285. package/src/domain/value-object.ts +135 -0
  286. package/src/errors/catalog.ts +149 -0
  287. package/src/errors/http.ts +80 -0
  288. package/src/errors/index.ts +28 -0
  289. package/src/errors/response.ts +54 -0
  290. package/src/errors/validation.ts +35 -0
  291. package/src/events/index.ts +246 -0
  292. package/src/jobs/index.ts +211 -0
  293. package/src/mail/index.ts +177 -0
  294. package/src/openapi/index.ts +865 -0
  295. package/src/openapi/schema-introspector.ts +107 -0
  296. package/src/ports/audit.ts +176 -0
  297. package/src/ports/auth.ts +76 -0
  298. package/src/ports/builder.ts +97 -0
  299. package/src/ports/cache.ts +94 -0
  300. package/src/ports/clock.ts +34 -0
  301. package/src/ports/events.ts +100 -0
  302. package/src/ports/id-generator.ts +36 -0
  303. package/src/ports/index.ts +221 -0
  304. package/src/ports/logger.ts +67 -0
  305. package/src/ports/policy.ts +242 -0
  306. package/src/ports/rate-limit.ts +91 -0
  307. package/src/ports/redaction.ts +199 -0
  308. package/src/ports/storage.ts +282 -0
  309. package/src/ports/testing.ts +234 -0
  310. package/src/ports/unit-of-work.ts +134 -0
  311. package/src/providers/index.ts +40 -0
  312. package/src/providers/instrumentation.ts +248 -0
  313. package/src/providers/provider.ts +191 -0
  314. package/src/schedules/index.ts +442 -0
  315. package/src/server/contract-like.ts +8 -0
  316. package/src/server/health.ts +82 -0
  317. package/src/server/hooks/auth.ts +147 -0
  318. package/src/server/hooks/cors.ts +87 -0
  319. package/src/server/hooks/errors.ts +126 -0
  320. package/src/server/hooks/index.ts +43 -0
  321. package/src/server/hooks/logging.ts +121 -0
  322. package/src/server/hooks/rate-limit.ts +171 -0
  323. package/src/server/hooks/utils.ts +16 -0
  324. package/src/server/hooks.ts +1 -0
  325. package/src/server/http.ts +189 -0
  326. package/src/server/index.ts +35 -0
  327. package/src/server/openapi.ts +72 -0
  328. package/src/server/providers/index.ts +3 -0
  329. package/src/server/providers/loadProviderConfig.ts +72 -0
  330. package/src/server/server.ts +1521 -0
  331. package/src/server/types.ts +2 -0
@@ -0,0 +1,1521 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import {
3
+ BEIGNET_ERROR_OWNER_HEADER,
4
+ type ContractErrorDefinition,
5
+ getContractHeaderSchemas,
6
+ type HttpContractConfig,
7
+ methodSupportsRequestBody,
8
+ parsePathTemplate,
9
+ type StandardSchema,
10
+ } from "../contracts";
11
+ import {
12
+ createErrorResponseBody,
13
+ isAppError,
14
+ isErrorResponseBody,
15
+ toErrorResponseBody,
16
+ } from "../errors";
17
+ import type { AnyPorts } from "../ports";
18
+ import { AuthUnauthorizedError, GateAuthorizationError } from "../ports";
19
+ import type {
20
+ ProvidedPortsOfList,
21
+ ProviderSetupResult,
22
+ ServiceProvider,
23
+ } from "../providers";
24
+ import type { ContractLike, ResolveContract } from "./contract-like";
25
+ import { resolveContract } from "./contract-like";
26
+ import { getRequestIdFromContext } from "./hooks/utils";
27
+ import type {
28
+ Handler,
29
+ HandlerArgs,
30
+ HttpRequestLike,
31
+ HttpResponse,
32
+ HttpResponseLike,
33
+ ResolvedRoute,
34
+ ServerCaughtErrorHook,
35
+ ServerHook,
36
+ ServerUnhandledErrorMapper,
37
+ } from "./http";
38
+ import {
39
+ loadProviderConfig,
40
+ parseStandardSchema,
41
+ SchemaValidationError,
42
+ } from "./providers";
43
+
44
+ /**
45
+ * Route definition
46
+ */
47
+ export type RouteDef<Ctx, CLike extends ContractLike = ContractLike> = {
48
+ contract: CLike;
49
+ handle: Handler<Ctx, ResolveContract<CLike>>;
50
+ };
51
+
52
+ const ROUTE_GROUP_KIND = "beignet.route-group";
53
+
54
+ export type RouteGroup<
55
+ Ctx,
56
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
57
+ Routes extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
58
+ > = {
59
+ kind: typeof ROUTE_GROUP_KIND;
60
+ name: string;
61
+ routes: Routes;
62
+ };
63
+
64
+ type RouteInput<Ctx> =
65
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
66
+ | RouteDef<Ctx, any>
67
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
68
+ | RouteGroup<Ctx, readonly RouteDef<Ctx, any>[]>;
69
+
70
+ type RoutesFromInput<Input> =
71
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
72
+ Input extends RouteGroup<any, infer Routes>
73
+ ? Routes
74
+ : // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
75
+ Input extends RouteDef<any, any>
76
+ ? readonly [Input]
77
+ : readonly [];
78
+
79
+ type FlattenRouteInputs<Inputs extends readonly unknown[]> =
80
+ number extends Inputs["length"]
81
+ ? // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
82
+ readonly RouteDef<any, any>[]
83
+ : Inputs extends readonly [infer First, ...infer Rest]
84
+ ? readonly [...RoutesFromInput<First>, ...FlattenRouteInputs<Rest>]
85
+ : readonly [];
86
+
87
+ type ContractsFromRouteList<
88
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
89
+ Routes extends readonly RouteDef<any, any>[],
90
+ > = {
91
+ readonly [Index in keyof Routes]: Routes[Index] extends RouteDef<
92
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
93
+ any,
94
+ infer CLike
95
+ >
96
+ ? ResolveContract<CLike>
97
+ : never;
98
+ };
99
+
100
+ export type CreateServerOptions<
101
+ Ctx,
102
+ Ports extends AnyPorts,
103
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
104
+ Routes extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
105
+ Providers extends readonly ServiceProvider<
106
+ unknown,
107
+ // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
108
+ StandardSchemaV1<any, any>,
109
+ AnyPorts
110
+ >[] = readonly [],
111
+ > = {
112
+ ports: Ports;
113
+ providers?: Providers;
114
+
115
+ // config sources
116
+ providerEnv?: Record<string, string | undefined>;
117
+ providerConfig?: Record<string, unknown>;
118
+
119
+ createContext: (args: {
120
+ req: HttpRequestLike;
121
+ ports: Ports & ProvidedPortsOfList<Providers>;
122
+ contract?: HttpContractConfig;
123
+ }) => Ctx | Promise<Ctx>;
124
+ hooks?: ServerHook<Ctx, Ports & ProvidedPortsOfList<Providers>>[];
125
+ routes?: Routes;
126
+ onCaughtError?: ServerCaughtErrorHook<Ctx>;
127
+ mapUnhandledError?: ServerUnhandledErrorMapper<Ctx>;
128
+ };
129
+
130
+ interface RouteBuilder<Ctx, C extends HttpContractConfig> {
131
+ handle: (
132
+ fn: Handler<Ctx, C>,
133
+ ) => (req: HttpRequestLike) => Promise<HttpResponse>;
134
+ }
135
+
136
+ export interface ServerInstance<Ctx, Ports extends AnyPorts = AnyPorts> {
137
+ api: (req: HttpRequestLike) => Promise<HttpResponse>;
138
+ route: <CLike extends ContractLike>(
139
+ contractLike: CLike,
140
+ ) => RouteBuilder<Ctx, ResolveContract<CLike>>;
141
+ contracts: readonly HttpContractConfig[];
142
+ stop: () => Promise<void>;
143
+ ports: Ports;
144
+ }
145
+
146
+ type ExecutionResult<Ctx> = {
147
+ ctx?: Ctx;
148
+ response: HttpResponse;
149
+ error?: unknown;
150
+ owner?: ResponseOwner;
151
+ };
152
+
153
+ type ResponseOwner = "route" | "framework" | "transport";
154
+
155
+ type CompiledPath = {
156
+ keys: string[];
157
+ pattern: RegExp;
158
+ segments: ReturnType<typeof parsePathTemplate>["segments"];
159
+ normalizedPath: string;
160
+ shapeKey: string;
161
+ };
162
+
163
+ class PathDecodeError extends Error {
164
+ constructor() {
165
+ super("Malformed URL path");
166
+ this.name = "PathDecodeError";
167
+ }
168
+ }
169
+
170
+ function compilePath(path: string) {
171
+ const parsed = parsePathTemplate(path);
172
+ const regexParts = parsed.segments.map((segment) =>
173
+ segment.kind === "dynamic"
174
+ ? "([^/]+)"
175
+ : segment.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
176
+ );
177
+ const pattern = new RegExp(`^/${regexParts.join("/")}$`);
178
+ return { ...parsed, pattern };
179
+ }
180
+
181
+ function decodeMatchedParams(
182
+ keys: string[],
183
+ match: RegExpExecArray,
184
+ ): Record<string, string> {
185
+ const params: Record<string, string> = {};
186
+ try {
187
+ keys.forEach((key, index) => {
188
+ params[key] = decodeURIComponent(match[index + 1]);
189
+ });
190
+ } catch (error) {
191
+ if (error instanceof URIError) {
192
+ throw new PathDecodeError();
193
+ }
194
+ throw error;
195
+ }
196
+ return params;
197
+ }
198
+
199
+ function compareRouteSpecificity(a: CompiledPath, b: CompiledPath): number {
200
+ const maxLength = Math.max(a.segments.length, b.segments.length);
201
+ for (let index = 0; index < maxLength; index++) {
202
+ const aSegment = a.segments[index];
203
+ const bSegment = b.segments[index];
204
+
205
+ if (!aSegment) return 1;
206
+ if (!bSegment) return -1;
207
+
208
+ if (aSegment.kind === bSegment.kind) continue;
209
+ return aSegment.kind === "static" ? -1 : 1;
210
+ }
211
+
212
+ return 0;
213
+ }
214
+
215
+ function errorResponse(
216
+ status: number,
217
+ code: string,
218
+ message: string,
219
+ details?: unknown,
220
+ ): HttpResponseLike {
221
+ return {
222
+ status,
223
+ body: createErrorResponseBody({ code, message, details }),
224
+ };
225
+ }
226
+
227
+ function normalizeResponse(res: HttpResponseLike): HttpResponseLike {
228
+ return {
229
+ status: res.status,
230
+ headers: res.headers,
231
+ body: res.body,
232
+ };
233
+ }
234
+
235
+ function isWebResponse(value: unknown): value is Response {
236
+ return typeof Response !== "undefined" && value instanceof Response;
237
+ }
238
+
239
+ function normalizeHttpResponse(res: HttpResponse): HttpResponse {
240
+ return isWebResponse(res) ? res : normalizeResponse(res);
241
+ }
242
+
243
+ function withFrameworkErrorOwnerHeader(
244
+ res: HttpResponseLike,
245
+ owner: ResponseOwner,
246
+ ): HttpResponseLike {
247
+ if (
248
+ owner !== "framework" ||
249
+ res.status < 400 ||
250
+ !isErrorResponseBody(res.body)
251
+ ) {
252
+ return res;
253
+ }
254
+
255
+ return {
256
+ ...res,
257
+ headers: {
258
+ ...(res.headers ?? {}),
259
+ [BEIGNET_ERROR_OWNER_HEADER]: "framework",
260
+ },
261
+ };
262
+ }
263
+
264
+ function responseOwnerFor(
265
+ res: HttpResponse,
266
+ owner?: ResponseOwner,
267
+ ): ResponseOwner {
268
+ if (isWebResponse(res)) return "transport";
269
+ return owner ?? "route";
270
+ }
271
+
272
+ function headersToRecord(headers: Headers): Record<string, string> {
273
+ const record: Record<string, string> = {};
274
+ headers.forEach((value, key) => {
275
+ record[key] = value;
276
+ });
277
+ return record;
278
+ }
279
+
280
+ function requestHeadersToRecord(headers: Headers): Record<string, string> {
281
+ const record: Record<string, string> = {};
282
+ headers.forEach((value, key) => {
283
+ record[key.toLowerCase()] = value;
284
+ });
285
+ return record;
286
+ }
287
+
288
+ async function parseHeaderSchemas(
289
+ schemas: readonly StandardSchema[],
290
+ rawHeaders: Record<string, string>,
291
+ ): Promise<Record<string, unknown>> {
292
+ let parsedHeaders: Record<string, unknown> = rawHeaders;
293
+
294
+ for (const schema of schemas) {
295
+ const parsed = await parseStandardSchema(schema, rawHeaders);
296
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
297
+ parsedHeaders = {
298
+ ...parsedHeaders,
299
+ ...(parsed as Record<string, unknown>),
300
+ };
301
+ } else {
302
+ parsedHeaders = parsed as Record<string, unknown>;
303
+ }
304
+ }
305
+
306
+ return parsedHeaders;
307
+ }
308
+
309
+ function responseForHooks(res: HttpResponse): HttpResponseLike {
310
+ if (!isWebResponse(res)) {
311
+ return normalizeResponse(res);
312
+ }
313
+
314
+ return {
315
+ status: res.status,
316
+ headers: headersToRecord(res.headers),
317
+ };
318
+ }
319
+
320
+ function isHttpResponseLike(value: unknown): value is HttpResponseLike {
321
+ return (
322
+ !isWebResponse(value) &&
323
+ typeof value === "object" &&
324
+ value !== null &&
325
+ "status" in value &&
326
+ typeof (value as { status?: unknown }).status === "number"
327
+ );
328
+ }
329
+
330
+ class ResponseContractViolationError extends Error {
331
+ readonly code: "RESPONSE_VALIDATION_ERROR" | "UNDECLARED_RESPONSE_STATUS";
332
+ readonly details?: unknown;
333
+
334
+ constructor(args: {
335
+ code: "RESPONSE_VALIDATION_ERROR" | "UNDECLARED_RESPONSE_STATUS";
336
+ message: string;
337
+ details?: unknown;
338
+ }) {
339
+ super(args.message);
340
+ this.name = "ResponseContractViolationError";
341
+ this.code = args.code;
342
+ this.details = args.details;
343
+ }
344
+ }
345
+
346
+ function getDeclaredCatalogErrorsForStatus(
347
+ contract: HttpContractConfig,
348
+ status: number,
349
+ ): ContractErrorDefinition[] {
350
+ const errors = contract.metadata?.errors;
351
+ if (typeof errors !== "object" || errors === null) return [];
352
+
353
+ return Object.values(errors).filter(
354
+ (error): error is ContractErrorDefinition =>
355
+ typeof error === "object" &&
356
+ error !== null &&
357
+ typeof (error as { code?: unknown }).code === "string" &&
358
+ typeof (error as { status?: unknown }).status === "number" &&
359
+ typeof (error as { message?: unknown }).message === "string" &&
360
+ (error as { status: number }).status === status,
361
+ );
362
+ }
363
+
364
+ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
365
+ contract: C,
366
+ res: HttpResponseLike,
367
+ ): Promise<void> {
368
+ const body = res.body;
369
+ if (res.status < 400 || !isErrorResponseBody(body)) return;
370
+
371
+ const declaredErrors = getDeclaredCatalogErrorsForStatus(
372
+ contract,
373
+ res.status,
374
+ );
375
+ if (declaredErrors.length === 0) return;
376
+
377
+ const matchingError = declaredErrors.find(
378
+ (error) => error.code === body.code,
379
+ );
380
+ if (!matchingError) {
381
+ throw new ResponseContractViolationError({
382
+ code: "RESPONSE_VALIDATION_ERROR",
383
+ message:
384
+ `Response validation failed for ${contract.method} ${contract.path} ` +
385
+ `(status ${res.status}, contract: ${contract.name})`,
386
+ details: {
387
+ issues: [
388
+ {
389
+ message:
390
+ `Error response code "${body.code}" is not declared for status ${res.status}. ` +
391
+ `Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
392
+ },
393
+ ],
394
+ },
395
+ });
396
+ }
397
+
398
+ if (matchingError.details && body.details !== undefined) {
399
+ try {
400
+ await parseStandardSchema(matchingError.details, body.details);
401
+ } catch (error) {
402
+ if (error instanceof SchemaValidationError) {
403
+ throw new ResponseContractViolationError({
404
+ code: "RESPONSE_VALIDATION_ERROR",
405
+ message:
406
+ `Response validation failed for ${contract.method} ${contract.path} ` +
407
+ `(status ${res.status}, contract: ${contract.name})`,
408
+ details: { issues: error.issues },
409
+ });
410
+ }
411
+ throw error;
412
+ }
413
+ }
414
+ }
415
+
416
+ async function validateResponseAgainstContract<C extends HttpContractConfig>(
417
+ contract: C,
418
+ res: HttpResponseLike,
419
+ ): Promise<void> {
420
+ const statusKey = String(res.status);
421
+ const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
422
+
423
+ if (!hasDeclaredStatus) {
424
+ if (Object.keys(contract.responses).length === 0) return;
425
+
426
+ throw new ResponseContractViolationError({
427
+ code: "UNDECLARED_RESPONSE_STATUS",
428
+ message:
429
+ `Handler returned undeclared status ${res.status} for ` +
430
+ `${contract.method} ${contract.path} (contract: ${contract.name})`,
431
+ details: {
432
+ status: res.status,
433
+ body: res.body,
434
+ },
435
+ });
436
+ }
437
+
438
+ const responseSchema = contract.responses[res.status];
439
+ if (responseSchema === null) {
440
+ if (res.body !== undefined && res.body !== null) {
441
+ throw new ResponseContractViolationError({
442
+ code: "RESPONSE_VALIDATION_ERROR",
443
+ message:
444
+ `Response validation failed for ${contract.method} ${contract.path} ` +
445
+ `(status ${res.status}, contract: ${contract.name})`,
446
+ details: {
447
+ issues: [
448
+ {
449
+ message:
450
+ "Response body must be empty for a null response schema.",
451
+ },
452
+ ],
453
+ },
454
+ });
455
+ }
456
+ return;
457
+ }
458
+
459
+ if (!responseSchema) return;
460
+
461
+ try {
462
+ await parseStandardSchema(responseSchema, res.body);
463
+ await validateCatalogErrorResponse(contract, res);
464
+ } catch (error) {
465
+ if (error instanceof SchemaValidationError) {
466
+ throw new ResponseContractViolationError({
467
+ code: "RESPONSE_VALIDATION_ERROR",
468
+ message:
469
+ `Response validation failed for ${contract.method} ${contract.path} ` +
470
+ `(status ${res.status}, contract: ${contract.name})`,
471
+ details: { issues: error.issues },
472
+ });
473
+ }
474
+ throw error;
475
+ }
476
+ }
477
+
478
+ async function finalizeResponse<C extends HttpContractConfig>(
479
+ contract: C,
480
+ res: HttpResponseLike,
481
+ ): Promise<HttpResponseLike> {
482
+ const normalized = normalizeResponse(res);
483
+ await validateResponseAgainstContract(contract, normalized);
484
+ return normalized;
485
+ }
486
+
487
+ function toContractViolationResponse(
488
+ error: ResponseContractViolationError,
489
+ ): HttpResponseLike {
490
+ return {
491
+ status: 500,
492
+ body: createErrorResponseBody({
493
+ code: error.code,
494
+ message: error.message,
495
+ details: error.details,
496
+ }),
497
+ };
498
+ }
499
+
500
+ function defaultErrorResponse(err: unknown, ctx?: unknown): HttpResponseLike {
501
+ const requestId = getRequestIdFromContext(ctx);
502
+ return {
503
+ status: 500,
504
+ body: createErrorResponseBody({
505
+ code: "INTERNAL_SERVER_ERROR",
506
+ message: "Internal server error",
507
+ requestId,
508
+ details:
509
+ process.env.NODE_ENV !== "production" && err instanceof Error
510
+ ? {
511
+ error: {
512
+ message: err.message,
513
+ stack: err.stack,
514
+ },
515
+ }
516
+ : undefined,
517
+ }),
518
+ };
519
+ }
520
+
521
+ async function parseBody(req: HttpRequestLike): Promise<unknown> {
522
+ const method = req.method.toUpperCase() as HttpContractConfig["method"];
523
+ if (!methodSupportsRequestBody(method)) {
524
+ return undefined;
525
+ }
526
+
527
+ const bodyReq = req.clone?.() ?? req;
528
+ const contentType = req.headers.get("content-type") || "";
529
+ if (contentType.includes("application/json")) {
530
+ const text = await bodyReq.text();
531
+ if (text === "") return undefined;
532
+ return JSON.parse(text);
533
+ }
534
+
535
+ try {
536
+ const text = await bodyReq.text();
537
+ return text === "" ? undefined : text;
538
+ } catch {
539
+ return undefined;
540
+ }
541
+ }
542
+
543
+ function buildHandler<
544
+ Ctx,
545
+ Ports extends AnyPorts,
546
+ C extends HttpContractConfig,
547
+ Providers extends readonly ServiceProvider<
548
+ unknown,
549
+ // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
550
+ StandardSchemaV1<any, any>,
551
+ AnyPorts
552
+ >[] = readonly [],
553
+ FinalPorts extends Ports & ProvidedPortsOfList<Providers> = Ports &
554
+ ProvidedPortsOfList<Providers>,
555
+ >(
556
+ // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
557
+ options: CreateServerOptions<Ctx, Ports, any, Providers>,
558
+ finalPorts: FinalPorts,
559
+ contract: C,
560
+ userHandler: Handler<Ctx, C>,
561
+ hooks: ServerHook<Ctx, FinalPorts>[],
562
+ optionsOverrides?: {
563
+ skipRoutePreparation?: boolean;
564
+ },
565
+ ): (
566
+ req: HttpRequestLike,
567
+ preMatchedParams?: Record<string, string>,
568
+ ) => Promise<HttpResponse> {
569
+ const compiled = compilePath(contract.path);
570
+
571
+ return async (
572
+ req: HttpRequestLike,
573
+ preMatchedParams?: Record<string, string>,
574
+ ) => {
575
+ let baseCtx: Ctx | undefined;
576
+ let pathValue: HandlerArgs<Ctx, C>["path"] | undefined;
577
+ let queryValue: HandlerArgs<Ctx, C>["query"] | undefined;
578
+ let headersValue: HandlerArgs<Ctx, C>["headers"] | undefined;
579
+ let bodyValue: HandlerArgs<Ctx, C>["body"] | undefined;
580
+ const startedAt = Date.now();
581
+
582
+ const resolveErrorResult = async (
583
+ error: unknown,
584
+ ctx?: Ctx,
585
+ path?: HandlerArgs<Ctx, C>["path"],
586
+ query?: HandlerArgs<Ctx, C>["query"],
587
+ headers?: HandlerArgs<Ctx, C>["headers"],
588
+ body?: HandlerArgs<Ctx, C>["body"],
589
+ resultOptions?: {
590
+ owner?: ResponseOwner;
591
+ },
592
+ ): Promise<ExecutionResult<Ctx>> => {
593
+ let currentError = error;
594
+
595
+ const notifyCaughtError = async (caught: unknown) => {
596
+ const args = {
597
+ err: caught,
598
+ req,
599
+ ctx,
600
+ contract,
601
+ path,
602
+ query,
603
+ headers,
604
+ body,
605
+ };
606
+ for (const hook of hooks) {
607
+ if (!hook.onCaughtError) continue;
608
+ try {
609
+ await hook.onCaughtError(args);
610
+ } catch {
611
+ // Observers must not change response behavior.
612
+ }
613
+ }
614
+ if (options.onCaughtError) {
615
+ try {
616
+ await options.onCaughtError(args);
617
+ } catch {
618
+ // Observers must not change response behavior.
619
+ }
620
+ }
621
+ };
622
+
623
+ await notifyCaughtError(currentError);
624
+
625
+ if (currentError instanceof ResponseContractViolationError) {
626
+ return {
627
+ ctx,
628
+ response: toContractViolationResponse(currentError),
629
+ error: currentError,
630
+ owner: "framework",
631
+ };
632
+ }
633
+
634
+ if (isAppError(currentError)) {
635
+ return {
636
+ ctx,
637
+ response: {
638
+ status: currentError.status,
639
+ body: toErrorResponseBody(currentError),
640
+ },
641
+ error: currentError,
642
+ owner: resultOptions?.owner ?? "route",
643
+ };
644
+ }
645
+
646
+ if (currentError instanceof AuthUnauthorizedError) {
647
+ return {
648
+ ctx,
649
+ response: errorResponse(401, currentError.code, currentError.message),
650
+ error: currentError,
651
+ owner: "framework",
652
+ };
653
+ }
654
+
655
+ if (currentError instanceof GateAuthorizationError) {
656
+ return {
657
+ ctx,
658
+ response: errorResponse(
659
+ currentError.status,
660
+ currentError.code,
661
+ currentError.message,
662
+ currentError.details,
663
+ ),
664
+ error: currentError,
665
+ owner: "framework",
666
+ };
667
+ }
668
+
669
+ for (const hook of hooks) {
670
+ if (!hook.mapUnhandledError) continue;
671
+ try {
672
+ const handled = await hook.mapUnhandledError({
673
+ err: currentError,
674
+ req,
675
+ ctx,
676
+ contract,
677
+ path,
678
+ query,
679
+ headers,
680
+ body,
681
+ });
682
+ if (handled) {
683
+ const response = normalizeHttpResponse(handled);
684
+ return {
685
+ ctx,
686
+ response,
687
+ error: currentError,
688
+ owner: responseOwnerFor(response, "framework"),
689
+ };
690
+ }
691
+ } catch (hookError) {
692
+ currentError = hookError;
693
+ await notifyCaughtError(currentError);
694
+ }
695
+ }
696
+
697
+ if (options.mapUnhandledError) {
698
+ try {
699
+ const handled = await options.mapUnhandledError({
700
+ err: currentError,
701
+ req,
702
+ ctx,
703
+ contract,
704
+ path,
705
+ query,
706
+ headers,
707
+ body,
708
+ });
709
+ if (handled) {
710
+ const response = normalizeHttpResponse(handled);
711
+ return {
712
+ ctx,
713
+ response,
714
+ error: currentError,
715
+ owner: responseOwnerFor(response, "framework"),
716
+ };
717
+ }
718
+ } catch (hookError) {
719
+ currentError = hookError;
720
+ await notifyCaughtError(currentError);
721
+ }
722
+ }
723
+
724
+ return {
725
+ ctx,
726
+ response: defaultErrorResponse(currentError, ctx),
727
+ error: currentError,
728
+ owner: "framework",
729
+ };
730
+ };
731
+
732
+ try {
733
+ const url = new URL(req.url);
734
+
735
+ let matchedParams: Record<string, string>;
736
+ if (preMatchedParams) {
737
+ matchedParams = preMatchedParams;
738
+ } else {
739
+ const match = compiled.pattern.exec(url.pathname);
740
+ if (
741
+ !match ||
742
+ contract.method.toUpperCase() !== req.method.toUpperCase()
743
+ ) {
744
+ return errorResponse(404, "NOT_FOUND", "Not found");
745
+ }
746
+ try {
747
+ matchedParams = decodeMatchedParams(compiled.keys, match);
748
+ } catch (error) {
749
+ if (error instanceof PathDecodeError) {
750
+ return errorResponse(400, "INVALID_PATH", "Malformed URL path");
751
+ }
752
+ throw error;
753
+ }
754
+ }
755
+ const rawHeaders = requestHeadersToRecord(req.headers);
756
+
757
+ const applyTransformHooks = async (
758
+ initialResult: ExecutionResult<Ctx>,
759
+ allowRetry: boolean,
760
+ ): Promise<ExecutionResult<Ctx>> => {
761
+ if (isWebResponse(initialResult.response)) {
762
+ return initialResult;
763
+ }
764
+
765
+ try {
766
+ let transformed = normalizeResponse(initialResult.response);
767
+ for (const hook of hooks) {
768
+ if (!hook.beforeSend) continue;
769
+ const nextResponse = await hook.beforeSend({
770
+ req,
771
+ ctx: initialResult.ctx,
772
+ contract,
773
+ path: pathValue,
774
+ query: queryValue,
775
+ headers: headersValue,
776
+ body: bodyValue,
777
+ response: transformed,
778
+ error: initialResult.error,
779
+ });
780
+ if (nextResponse) {
781
+ transformed = normalizeResponse(nextResponse);
782
+ }
783
+ }
784
+ return {
785
+ ...initialResult,
786
+ response: transformed,
787
+ };
788
+ } catch (error) {
789
+ const mapped = await resolveErrorResult(
790
+ error,
791
+ initialResult.ctx,
792
+ pathValue,
793
+ queryValue,
794
+ headersValue,
795
+ bodyValue,
796
+ { owner: "framework" },
797
+ );
798
+ if (!allowRetry) {
799
+ return mapped;
800
+ }
801
+ return applyTransformHooks(mapped, false);
802
+ }
803
+ };
804
+
805
+ let result: ExecutionResult<Ctx> | undefined;
806
+
807
+ for (const hook of hooks) {
808
+ if (!hook.onRequest) continue;
809
+ try {
810
+ const hookResult = await hook.onRequest({
811
+ req,
812
+ ports: finalPorts,
813
+ contract,
814
+ params: matchedParams,
815
+ });
816
+ if (hookResult) {
817
+ const response = normalizeHttpResponse(hookResult);
818
+ result = {
819
+ response,
820
+ owner: responseOwnerFor(response, "framework"),
821
+ };
822
+ break;
823
+ }
824
+ } catch (error) {
825
+ result = await resolveErrorResult(
826
+ error,
827
+ undefined,
828
+ undefined,
829
+ undefined,
830
+ undefined,
831
+ undefined,
832
+ { owner: "framework" },
833
+ );
834
+ break;
835
+ }
836
+ }
837
+
838
+ if (!result) {
839
+ if (optionsOverrides?.skipRoutePreparation) {
840
+ let createdCtx!: Ctx;
841
+ try {
842
+ createdCtx = await options.createContext({
843
+ req,
844
+ ports: finalPorts,
845
+ contract,
846
+ });
847
+ baseCtx = createdCtx;
848
+ } catch (error) {
849
+ result = await resolveErrorResult(
850
+ error,
851
+ undefined,
852
+ undefined,
853
+ undefined,
854
+ undefined,
855
+ undefined,
856
+ { owner: "framework" },
857
+ );
858
+ }
859
+
860
+ if (!result) {
861
+ try {
862
+ result = {
863
+ ctx: createdCtx,
864
+ response: normalizeHttpResponse(
865
+ await userHandler({
866
+ req,
867
+ ctx: createdCtx,
868
+ contract,
869
+ path: {} as HandlerArgs<Ctx, C>["path"],
870
+ query: {} as HandlerArgs<Ctx, C>["query"],
871
+ headers: rawHeaders as HandlerArgs<Ctx, C>["headers"],
872
+ body: undefined as HandlerArgs<Ctx, C>["body"],
873
+ }),
874
+ ),
875
+ owner: "framework",
876
+ };
877
+ } catch (error) {
878
+ result = await resolveErrorResult(
879
+ error,
880
+ createdCtx,
881
+ undefined,
882
+ undefined,
883
+ undefined,
884
+ undefined,
885
+ { owner: "framework" },
886
+ );
887
+ }
888
+ }
889
+ } else {
890
+ const rawQuery: Record<string, string | string[]> = {};
891
+ for (const key of new Set(url.searchParams.keys())) {
892
+ const values = url.searchParams.getAll(key);
893
+ rawQuery[key] = values.length === 1 ? values[0] : values;
894
+ }
895
+
896
+ // biome-ignore lint/suspicious/noExplicitAny: type is narrowed by schema validation below
897
+ let query: any = rawQuery;
898
+ if (contract.query) {
899
+ try {
900
+ query = await parseStandardSchema(contract.query, query);
901
+ } catch (error) {
902
+ result =
903
+ error instanceof SchemaValidationError
904
+ ? {
905
+ response: errorResponse(
906
+ 422,
907
+ "VALIDATION_ERROR",
908
+ "Invalid query parameters",
909
+ { issues: error.issues },
910
+ ),
911
+ owner: "framework",
912
+ }
913
+ : {
914
+ response: errorResponse(
915
+ 422,
916
+ "VALIDATION_ERROR",
917
+ "Invalid query parameters",
918
+ (error as Error).message,
919
+ ),
920
+ owner: "framework",
921
+ };
922
+ }
923
+ }
924
+
925
+ // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
926
+ let path: any = matchedParams;
927
+ if (!result && contract.pathParams) {
928
+ try {
929
+ path = await parseStandardSchema(
930
+ contract.pathParams,
931
+ matchedParams,
932
+ );
933
+ } catch (error) {
934
+ result =
935
+ error instanceof SchemaValidationError
936
+ ? {
937
+ response: errorResponse(
938
+ 422,
939
+ "VALIDATION_ERROR",
940
+ "Invalid path parameters",
941
+ { issues: error.issues },
942
+ ),
943
+ owner: "framework",
944
+ }
945
+ : {
946
+ response: errorResponse(
947
+ 422,
948
+ "VALIDATION_ERROR",
949
+ "Invalid path parameters",
950
+ (error as Error).message,
951
+ ),
952
+ owner: "framework",
953
+ };
954
+ }
955
+ }
956
+
957
+ // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
958
+ let headers: any = rawHeaders;
959
+ const headerSchemas = getContractHeaderSchemas(contract.headers);
960
+ if (!result && headerSchemas.length > 0) {
961
+ try {
962
+ headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
963
+ } catch (error) {
964
+ result =
965
+ error instanceof SchemaValidationError
966
+ ? {
967
+ response: errorResponse(
968
+ 422,
969
+ "VALIDATION_ERROR",
970
+ "Invalid request headers",
971
+ { issues: error.issues },
972
+ ),
973
+ owner: "framework",
974
+ }
975
+ : {
976
+ response: errorResponse(
977
+ 422,
978
+ "VALIDATION_ERROR",
979
+ "Invalid request headers",
980
+ (error as Error).message,
981
+ ),
982
+ owner: "framework",
983
+ };
984
+ }
985
+ }
986
+
987
+ // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
988
+ let body: any;
989
+ if (!result) {
990
+ try {
991
+ body = await parseBody(req);
992
+ } catch {
993
+ result = {
994
+ response: errorResponse(400, "INVALID_BODY", "Malformed JSON"),
995
+ owner: "framework",
996
+ };
997
+ }
998
+ }
999
+
1000
+ if (!result && contract.body) {
1001
+ try {
1002
+ body = await parseStandardSchema(contract.body, body);
1003
+ } catch (error) {
1004
+ if (
1005
+ body === undefined &&
1006
+ error instanceof SchemaValidationError
1007
+ ) {
1008
+ result = {
1009
+ response: errorResponse(
1010
+ 400,
1011
+ "MISSING_BODY",
1012
+ "Request body is required",
1013
+ ),
1014
+ owner: "framework",
1015
+ };
1016
+ } else {
1017
+ result =
1018
+ error instanceof SchemaValidationError
1019
+ ? {
1020
+ response: errorResponse(
1021
+ 422,
1022
+ "VALIDATION_ERROR",
1023
+ "Invalid request body",
1024
+ { issues: error.issues },
1025
+ ),
1026
+ owner: "framework",
1027
+ }
1028
+ : {
1029
+ response: errorResponse(
1030
+ 422,
1031
+ "VALIDATION_ERROR",
1032
+ "Invalid request body",
1033
+ (error as Error).message,
1034
+ ),
1035
+ owner: "framework",
1036
+ };
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ if (!result) {
1042
+ pathValue = path;
1043
+ queryValue = query;
1044
+ headersValue = headers;
1045
+ bodyValue = body;
1046
+
1047
+ let createdCtx!: Ctx;
1048
+ try {
1049
+ createdCtx = await options.createContext({
1050
+ req,
1051
+ ports: finalPorts,
1052
+ contract,
1053
+ });
1054
+ baseCtx = createdCtx;
1055
+ } catch (error) {
1056
+ result = await resolveErrorResult(
1057
+ error,
1058
+ undefined,
1059
+ pathValue,
1060
+ queryValue,
1061
+ headersValue,
1062
+ bodyValue,
1063
+ { owner: "framework" },
1064
+ );
1065
+ }
1066
+
1067
+ if (!result) {
1068
+ const baseArgs: HandlerArgs<Ctx, C> = {
1069
+ req,
1070
+ ctx: createdCtx,
1071
+ contract,
1072
+ path,
1073
+ query,
1074
+ headers,
1075
+ body,
1076
+ };
1077
+
1078
+ let currentCtx = createdCtx;
1079
+ for (const hook of hooks) {
1080
+ if (!hook.beforeHandle) continue;
1081
+ try {
1082
+ const hookResult = await hook.beforeHandle({
1083
+ req,
1084
+ ctx: currentCtx,
1085
+ contract,
1086
+ path,
1087
+ query,
1088
+ headers,
1089
+ body,
1090
+ });
1091
+ if (isWebResponse(hookResult)) {
1092
+ result = {
1093
+ ctx: currentCtx,
1094
+ response: hookResult,
1095
+ owner: "transport",
1096
+ };
1097
+ break;
1098
+ }
1099
+ if (isHttpResponseLike(hookResult)) {
1100
+ result = {
1101
+ ctx: currentCtx,
1102
+ response: normalizeResponse(hookResult),
1103
+ owner: "framework",
1104
+ };
1105
+ break;
1106
+ }
1107
+ if (hookResult?.ctx !== undefined) {
1108
+ currentCtx = hookResult.ctx;
1109
+ }
1110
+ if (hookResult?.response) {
1111
+ const response = normalizeHttpResponse(hookResult.response);
1112
+ result = {
1113
+ ctx: currentCtx,
1114
+ response,
1115
+ owner: responseOwnerFor(response, "framework"),
1116
+ };
1117
+ break;
1118
+ }
1119
+ } catch (error) {
1120
+ result = await resolveErrorResult(
1121
+ error,
1122
+ currentCtx,
1123
+ pathValue,
1124
+ queryValue,
1125
+ headersValue,
1126
+ bodyValue,
1127
+ { owner: "framework" },
1128
+ );
1129
+ break;
1130
+ }
1131
+ }
1132
+
1133
+ if (!result) {
1134
+ try {
1135
+ result = {
1136
+ ctx: currentCtx,
1137
+ response: normalizeHttpResponse(
1138
+ await userHandler({ ...baseArgs, ctx: currentCtx }),
1139
+ ),
1140
+ };
1141
+ } catch (error) {
1142
+ result = await resolveErrorResult(
1143
+ error,
1144
+ currentCtx,
1145
+ pathValue,
1146
+ queryValue,
1147
+ headersValue,
1148
+ bodyValue,
1149
+ );
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ result = await applyTransformHooks(result, true);
1158
+
1159
+ let finalResponse = normalizeHttpResponse(result.response);
1160
+ let finalError = result.error;
1161
+ let finalOwner = responseOwnerFor(finalResponse, result.owner);
1162
+ if (finalOwner === "route" && !isWebResponse(finalResponse)) {
1163
+ try {
1164
+ finalResponse = await finalizeResponse(contract, finalResponse);
1165
+ } catch (error) {
1166
+ if (error instanceof ResponseContractViolationError) {
1167
+ result = await applyTransformHooks(
1168
+ {
1169
+ ctx: result.ctx,
1170
+ response: toContractViolationResponse(error),
1171
+ error,
1172
+ owner: "framework",
1173
+ },
1174
+ false,
1175
+ );
1176
+ finalResponse = normalizeHttpResponse(result.response);
1177
+ finalError = result.error;
1178
+ finalOwner = responseOwnerFor(finalResponse, result.owner);
1179
+ } else {
1180
+ throw error;
1181
+ }
1182
+ }
1183
+ }
1184
+ if (!isWebResponse(finalResponse)) {
1185
+ finalResponse = withFrameworkErrorOwnerHeader(
1186
+ finalResponse,
1187
+ finalOwner,
1188
+ );
1189
+ }
1190
+
1191
+ const durationMs = Date.now() - startedAt;
1192
+ for (const hook of hooks) {
1193
+ if (!hook.afterSend) continue;
1194
+ try {
1195
+ await hook.afterSend({
1196
+ req,
1197
+ ctx: result.ctx,
1198
+ contract,
1199
+ path: pathValue,
1200
+ query: queryValue,
1201
+ headers: headersValue,
1202
+ body: bodyValue,
1203
+ response: responseForHooks(finalResponse),
1204
+ error: finalError,
1205
+ durationMs,
1206
+ });
1207
+ } catch {
1208
+ // Ignore after-response hook failures; they should never change the response.
1209
+ }
1210
+ }
1211
+
1212
+ return finalResponse;
1213
+ } catch (error) {
1214
+ const result = await resolveErrorResult(
1215
+ error,
1216
+ baseCtx,
1217
+ pathValue,
1218
+ queryValue,
1219
+ headersValue,
1220
+ bodyValue,
1221
+ {
1222
+ owner: "framework",
1223
+ },
1224
+ );
1225
+ const response = normalizeHttpResponse(result.response);
1226
+ if (isWebResponse(response)) {
1227
+ return response;
1228
+ }
1229
+ return withFrameworkErrorOwnerHeader(
1230
+ response,
1231
+ responseOwnerFor(response, result.owner),
1232
+ );
1233
+ }
1234
+ };
1235
+ }
1236
+
1237
+ export async function createServer<
1238
+ Ctx,
1239
+ Ports extends AnyPorts,
1240
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
1241
+ Routes extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
1242
+ Providers extends readonly ServiceProvider<
1243
+ unknown,
1244
+ // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
1245
+ StandardSchemaV1<any, any>,
1246
+ AnyPorts
1247
+ >[] = readonly [],
1248
+ >(
1249
+ options: CreateServerOptions<Ctx, Ports, Routes, Providers>,
1250
+ ): Promise<ServerInstance<Ctx, Ports & ProvidedPortsOfList<Providers>>> {
1251
+ type RegisteredRoute = ResolvedRoute<Ctx, HttpContractConfig> & {
1252
+ compiled: CompiledPath;
1253
+ };
1254
+ const registry: RegisteredRoute[] = [];
1255
+ type FinalPorts = Ports & ProvidedPortsOfList<Providers>;
1256
+ const providers = (options.providers ?? []) as readonly ServiceProvider<
1257
+ unknown,
1258
+ // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
1259
+ StandardSchemaV1<any, any>,
1260
+ AnyPorts
1261
+ >[];
1262
+ const env = options.providerEnv ?? process.env;
1263
+ const overrides = options.providerConfig ?? {};
1264
+ const providerResults: ProviderSetupResult<AnyPorts>[] = [];
1265
+ const finalPorts = { ...options.ports } as FinalPorts;
1266
+ const hooks = [...((options.hooks ?? []) as ServerHook<Ctx, FinalPorts>[])];
1267
+ const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
1268
+
1269
+ let stopped = false;
1270
+ const stop = async () => {
1271
+ if (stopped) return;
1272
+ stopped = true;
1273
+ const errors: unknown[] = [];
1274
+ for (let i = providerResults.length - 1; i >= 0; i -= 1) {
1275
+ const result = providerResults[i];
1276
+ try {
1277
+ await result?.stop?.({
1278
+ ports: finalPorts,
1279
+ });
1280
+ } catch (err) {
1281
+ errors.push(err);
1282
+ }
1283
+ }
1284
+ if (errors.length) {
1285
+ throw new AggregateError(errors, "Provider shutdown errors");
1286
+ }
1287
+ };
1288
+
1289
+ const registeredPaths = new Set<string>();
1290
+ const registeredShapes = new Map<string, string>();
1291
+
1292
+ const registerRoute = <C extends HttpContractConfig>(
1293
+ contract: C,
1294
+ handler: Handler<Ctx, C>,
1295
+ ): void => {
1296
+ if (contract.body && !methodSupportsRequestBody(contract.method)) {
1297
+ throw new Error(
1298
+ `Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`,
1299
+ );
1300
+ }
1301
+ const compiled = compilePath(contract.path);
1302
+ const normalizedPath = compiled.normalizedPath;
1303
+ const routeKey = `${contract.method.toUpperCase()} ${normalizedPath}`;
1304
+ if (registeredPaths.has(routeKey)) {
1305
+ throw new Error(
1306
+ `Duplicate route: ${routeKey} is already registered. Each method + path combination must be unique.`,
1307
+ );
1308
+ }
1309
+ const shapeRouteKey = `${contract.method.toUpperCase()} ${compiled.shapeKey}`;
1310
+ const conflictingRoute = registeredShapes.get(shapeRouteKey);
1311
+ if (conflictingRoute) {
1312
+ throw new Error(
1313
+ `Ambiguous route: ${routeKey} conflicts with ${conflictingRoute}. Dynamic parameter names are ignored during routing, so each method + path shape must be unique.`,
1314
+ );
1315
+ }
1316
+ registeredPaths.add(routeKey);
1317
+ registeredShapes.set(shapeRouteKey, routeKey);
1318
+
1319
+ const builtHandler = buildHandler(
1320
+ options,
1321
+ finalPorts,
1322
+ contract,
1323
+ handler,
1324
+ hooks,
1325
+ );
1326
+ registry.push({
1327
+ contract,
1328
+ compiled,
1329
+ handler: builtHandler,
1330
+ match: (method, pathname) => {
1331
+ if (contract.method.toUpperCase() !== method.toUpperCase()) {
1332
+ return { matched: false as const };
1333
+ }
1334
+ const match = compiled.pattern.exec(pathname);
1335
+ if (!match) return { matched: false as const };
1336
+ return { matched: true as const };
1337
+ },
1338
+ });
1339
+ registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
1340
+ };
1341
+
1342
+ const createBuilder = <C extends HttpContractConfig>(
1343
+ contract: C,
1344
+ shouldRegister: boolean,
1345
+ ): RouteBuilder<Ctx, C> => ({
1346
+ handle: (fn) => {
1347
+ const wrapped = buildHandler(options, finalPorts, contract, fn, hooks);
1348
+ if (shouldRegister) registerRoute(contract, fn);
1349
+ return wrapped;
1350
+ },
1351
+ });
1352
+
1353
+ if (options.routes) {
1354
+ try {
1355
+ for (const route of options.routes) {
1356
+ const contract = resolveContract(route.contract);
1357
+ registerRoute(contract, route.handle);
1358
+ }
1359
+ } catch (error) {
1360
+ try {
1361
+ await stop();
1362
+ } catch (cleanupError) {
1363
+ throw new AggregateError(
1364
+ [error, cleanupError],
1365
+ "Server initialization failed and provider cleanup failed",
1366
+ );
1367
+ }
1368
+ throw error;
1369
+ }
1370
+ }
1371
+
1372
+ try {
1373
+ for (const provider of providers) {
1374
+ const cfg = await loadProviderConfig(provider, env, overrides);
1375
+ const result = await provider.setup({
1376
+ ports: finalPorts,
1377
+ config: cfg,
1378
+ });
1379
+ if (result.ports) {
1380
+ Object.assign(finalPorts, result.ports);
1381
+ }
1382
+ providerResults.push(result);
1383
+ }
1384
+
1385
+ for (const result of providerResults) {
1386
+ if (!result.start) continue;
1387
+ await result.start({
1388
+ ports: finalPorts,
1389
+ });
1390
+ }
1391
+ } catch (error) {
1392
+ try {
1393
+ await stop();
1394
+ } catch (cleanupError) {
1395
+ throw new AggregateError(
1396
+ [error, cleanupError],
1397
+ "Server initialization failed and provider cleanup failed",
1398
+ );
1399
+ }
1400
+ throw error;
1401
+ }
1402
+
1403
+ const api = async (req: HttpRequestLike) => {
1404
+ const url = new URL(req.url);
1405
+
1406
+ for (const entry of registry) {
1407
+ const result = entry.match(req.method, url.pathname);
1408
+ if (result.matched) {
1409
+ return await entry.handler(req);
1410
+ }
1411
+ }
1412
+
1413
+ const notFoundContract: HttpContractConfig = {
1414
+ kind: "http",
1415
+ name: "notFound",
1416
+ method: req.method.toUpperCase() as HttpContractConfig["method"],
1417
+ path: url.pathname || "/",
1418
+ pathParams: null,
1419
+ query: null,
1420
+ body: null,
1421
+ responses: {},
1422
+ metadata: {},
1423
+ };
1424
+
1425
+ const notFoundHandler = buildHandler(
1426
+ options,
1427
+ finalPorts,
1428
+ notFoundContract,
1429
+ async () => errorResponse(404, "NOT_FOUND", "Not found"),
1430
+ hooks,
1431
+ { skipRoutePreparation: true },
1432
+ );
1433
+
1434
+ return await notFoundHandler(req);
1435
+ };
1436
+
1437
+ return {
1438
+ api,
1439
+ route: (contractLike) => {
1440
+ const contract = resolveContract(contractLike);
1441
+ return createBuilder(contract, true);
1442
+ },
1443
+ contracts,
1444
+ stop,
1445
+ ports: finalPorts,
1446
+ };
1447
+ }
1448
+
1449
+ /**
1450
+ * Helper function to define routes with proper type inference.
1451
+ * Eliminates the need for type assertions when passing routes to createServer.
1452
+ *
1453
+ * @example
1454
+ * const routes = defineRoutes<AppContext>([
1455
+ * { contract: myContract, handle: async ({ query }) => { ... } }
1456
+ * ]);
1457
+ */
1458
+ export function defineRoutes<
1459
+ Ctx,
1460
+ const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
1461
+ >(routes: R): FlattenRouteInputs<R> {
1462
+ const flattened: RouteDef<Ctx>[] = [];
1463
+
1464
+ for (const route of routes) {
1465
+ if (isRouteGroup(route)) {
1466
+ flattened.push(...route.routes);
1467
+ } else {
1468
+ flattened.push(route);
1469
+ }
1470
+ }
1471
+
1472
+ return flattened as unknown as FlattenRouteInputs<R>;
1473
+ }
1474
+
1475
+ /**
1476
+ * Extract contract configs from a route list. Use this to drive OpenAPI from
1477
+ * the same route registration list that createServer receives.
1478
+ */
1479
+ export function contractsFromRoutes<
1480
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
1481
+ const R extends readonly RouteDef<any, any>[],
1482
+ >(routes: R): ContractsFromRouteList<R> {
1483
+ return routes.map((route) =>
1484
+ resolveContract(route.contract),
1485
+ ) as unknown as ContractsFromRouteList<R>;
1486
+ }
1487
+
1488
+ /**
1489
+ * Helper function to group related route registrations.
1490
+ *
1491
+ * Route groups are flattened by defineRoutes, so createServer still receives
1492
+ * a regular route list while app code can keep feature route wiring colocated.
1493
+ *
1494
+ * @example
1495
+ * const todoRoutes = defineRouteGroup<AppContext>({
1496
+ * name: "todos",
1497
+ * routes: [
1498
+ * { contract: listTodos, handle: async ({ ctx }) => { ... } }
1499
+ * ]
1500
+ * });
1501
+ */
1502
+ export function defineRouteGroup<
1503
+ Ctx,
1504
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
1505
+ const R extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
1506
+ >(group: { name: string; routes: R }): RouteGroup<Ctx, R> {
1507
+ return {
1508
+ kind: ROUTE_GROUP_KIND,
1509
+ name: group.name,
1510
+ routes: group.routes,
1511
+ };
1512
+ }
1513
+
1514
+ function isRouteGroup<Ctx>(route: RouteInput<Ctx>): route is RouteGroup<Ctx> {
1515
+ return (
1516
+ typeof route === "object" &&
1517
+ route !== null &&
1518
+ "kind" in route &&
1519
+ route.kind === ROUTE_GROUP_KIND
1520
+ );
1521
+ }