@beignet/core 0.0.3 → 0.0.4

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 (360) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +785 -43
  3. package/dist/application/index.d.ts +28 -2
  4. package/dist/application/index.d.ts.map +1 -1
  5. package/dist/application/index.js +140 -12
  6. package/dist/application/index.js.map +1 -1
  7. package/dist/client/client.d.ts +2 -2
  8. package/dist/client/client.d.ts.map +1 -1
  9. package/dist/client/client.js +136 -48
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/client/error-messages.d.ts +14 -0
  12. package/dist/client/error-messages.d.ts.map +1 -0
  13. package/dist/client/error-messages.js +23 -0
  14. package/dist/client/error-messages.js.map +1 -0
  15. package/dist/client/index.d.ts +8 -4
  16. package/dist/client/index.d.ts.map +1 -1
  17. package/dist/client/index.js +6 -2
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/types.d.ts +35 -5
  20. package/dist/client/types.d.ts.map +1 -1
  21. package/dist/client-only.d.ts +8 -0
  22. package/dist/client-only.d.ts.map +1 -0
  23. package/dist/client-only.js +8 -0
  24. package/dist/client-only.js.map +1 -0
  25. package/dist/config/index.d.ts +5 -5
  26. package/dist/config/index.d.ts.map +1 -1
  27. package/dist/config/index.js +2 -2
  28. package/dist/config/index.js.map +1 -1
  29. package/dist/contracts/catalog-errors.d.ts +27 -0
  30. package/dist/contracts/catalog-errors.d.ts.map +1 -0
  31. package/dist/contracts/catalog-errors.js +69 -0
  32. package/dist/contracts/catalog-errors.js.map +1 -0
  33. package/dist/contracts/contract-builder.d.ts +15 -12
  34. package/dist/contracts/contract-builder.d.ts.map +1 -1
  35. package/dist/contracts/contract-builder.js +15 -41
  36. package/dist/contracts/contract-builder.js.map +1 -1
  37. package/dist/contracts/contract-group.d.ts +11 -8
  38. package/dist/contracts/contract-group.d.ts.map +1 -1
  39. package/dist/contracts/contract-group.js +13 -40
  40. package/dist/contracts/contract-group.js.map +1 -1
  41. package/dist/contracts/contract-like.d.ts +1 -1
  42. package/dist/contracts/contract-like.d.ts.map +1 -1
  43. package/dist/contracts/index.d.ts +13 -9
  44. package/dist/contracts/index.d.ts.map +1 -1
  45. package/dist/contracts/index.js +9 -5
  46. package/dist/contracts/index.js.map +1 -1
  47. package/dist/contracts/openapi-meta.d.ts +48 -0
  48. package/dist/contracts/openapi-meta.d.ts.map +1 -1
  49. package/dist/contracts/openapi-meta.js +3 -0
  50. package/dist/contracts/openapi-meta.js.map +1 -1
  51. package/dist/contracts/path-template.d.ts +1 -1
  52. package/dist/contracts/path-template.js +2 -2
  53. package/dist/contracts/path-template.js.map +1 -1
  54. package/dist/contracts/schema-shape.d.ts +37 -0
  55. package/dist/contracts/schema-shape.d.ts.map +1 -0
  56. package/dist/contracts/schema-shape.js +61 -0
  57. package/dist/contracts/schema-shape.js.map +1 -0
  58. package/dist/contracts/success-status.d.ts +32 -0
  59. package/dist/contracts/success-status.d.ts.map +1 -0
  60. package/dist/contracts/success-status.js +18 -0
  61. package/dist/contracts/success-status.js.map +1 -0
  62. package/dist/contracts/types.d.ts +25 -5
  63. package/dist/contracts/types.d.ts.map +1 -1
  64. package/dist/contracts/types.js.map +1 -1
  65. package/dist/contracts/utils.d.ts +1 -1
  66. package/dist/contracts/utils.d.ts.map +1 -1
  67. package/dist/contracts/utils.js +1 -1
  68. package/dist/contracts/utils.js.map +1 -1
  69. package/dist/domain/events.d.ts +1 -1
  70. package/dist/domain/events.d.ts.map +1 -1
  71. package/dist/domain/events.js +1 -1
  72. package/dist/domain/events.js.map +1 -1
  73. package/dist/domain/index.d.ts +3 -3
  74. package/dist/domain/index.d.ts.map +1 -1
  75. package/dist/domain/index.js +3 -3
  76. package/dist/domain/index.js.map +1 -1
  77. package/dist/errors/catalog.d.ts +9 -1
  78. package/dist/errors/catalog.d.ts.map +1 -1
  79. package/dist/errors/catalog.js +7 -1
  80. package/dist/errors/catalog.js.map +1 -1
  81. package/dist/errors/http.d.ts +10 -0
  82. package/dist/errors/http.d.ts.map +1 -1
  83. package/dist/errors/http.js +11 -1
  84. package/dist/errors/http.js.map +1 -1
  85. package/dist/errors/index.d.ts +4 -4
  86. package/dist/errors/index.d.ts.map +1 -1
  87. package/dist/errors/index.js +4 -4
  88. package/dist/errors/index.js.map +1 -1
  89. package/dist/errors/response.d.ts +4 -1
  90. package/dist/errors/response.d.ts.map +1 -1
  91. package/dist/errors/response.js.map +1 -1
  92. package/dist/events/index.d.ts +10 -12
  93. package/dist/events/index.d.ts.map +1 -1
  94. package/dist/events/index.js +10 -10
  95. package/dist/events/index.js.map +1 -1
  96. package/dist/idempotency/index.d.ts +5 -3
  97. package/dist/idempotency/index.d.ts.map +1 -1
  98. package/dist/idempotency/index.js.map +1 -1
  99. package/dist/jobs/index.d.ts +12 -14
  100. package/dist/jobs/index.d.ts.map +1 -1
  101. package/dist/jobs/index.js +13 -13
  102. package/dist/jobs/index.js.map +1 -1
  103. package/dist/notifications/index.d.ts +14 -16
  104. package/dist/notifications/index.d.ts.map +1 -1
  105. package/dist/notifications/index.js +14 -14
  106. package/dist/notifications/index.js.map +1 -1
  107. package/dist/openapi/index.d.ts +8 -3
  108. package/dist/openapi/index.d.ts.map +1 -1
  109. package/dist/openapi/index.js +41 -29
  110. package/dist/openapi/index.js.map +1 -1
  111. package/dist/openapi/schema-introspector.d.ts +37 -0
  112. package/dist/openapi/schema-introspector.d.ts.map +1 -1
  113. package/dist/openapi/schema-introspector.js +23 -17
  114. package/dist/openapi/schema-introspector.js.map +1 -1
  115. package/dist/outbox/index.d.ts +15 -6
  116. package/dist/outbox/index.d.ts.map +1 -1
  117. package/dist/outbox/index.js +60 -16
  118. package/dist/outbox/index.js.map +1 -1
  119. package/dist/ports/audit.d.ts +56 -10
  120. package/dist/ports/audit.d.ts.map +1 -1
  121. package/dist/ports/audit.js +71 -3
  122. package/dist/ports/audit.js.map +1 -1
  123. package/dist/ports/auth.d.ts +92 -0
  124. package/dist/ports/auth.d.ts.map +1 -1
  125. package/dist/ports/auth.js +92 -0
  126. package/dist/ports/auth.js.map +1 -1
  127. package/dist/ports/events.d.ts +2 -2
  128. package/dist/ports/events.d.ts.map +1 -1
  129. package/dist/ports/index.d.ts +62 -33
  130. package/dist/ports/index.d.ts.map +1 -1
  131. package/dist/ports/index.js +28 -34
  132. package/dist/ports/index.js.map +1 -1
  133. package/dist/ports/policy.d.ts +32 -3
  134. package/dist/ports/policy.d.ts.map +1 -1
  135. package/dist/ports/policy.js +13 -2
  136. package/dist/ports/policy.js.map +1 -1
  137. package/dist/ports/testing.d.ts +1030 -2
  138. package/dist/ports/testing.d.ts.map +1 -1
  139. package/dist/ports/testing.js +1031 -1
  140. package/dist/ports/testing.js.map +1 -1
  141. package/dist/ports/unbound.d.ts +21 -0
  142. package/dist/ports/unbound.d.ts.map +1 -0
  143. package/dist/ports/unbound.js +57 -0
  144. package/dist/ports/unbound.js.map +1 -0
  145. package/dist/ports/unit-of-work.d.ts +1 -1
  146. package/dist/ports/unit-of-work.d.ts.map +1 -1
  147. package/dist/ports/unit-of-work.js +1 -1
  148. package/dist/ports/unit-of-work.js.map +1 -1
  149. package/dist/providers/index.d.ts +3 -2
  150. package/dist/providers/index.d.ts.map +1 -1
  151. package/dist/providers/index.js +3 -2
  152. package/dist/providers/index.js.map +1 -1
  153. package/dist/providers/instrumentation.d.ts +45 -4
  154. package/dist/providers/instrumentation.d.ts.map +1 -1
  155. package/dist/providers/instrumentation.js +25 -6
  156. package/dist/providers/instrumentation.js.map +1 -1
  157. package/dist/providers/metadata.d.ts +39 -0
  158. package/dist/providers/metadata.d.ts.map +1 -0
  159. package/dist/providers/metadata.js +169 -0
  160. package/dist/providers/metadata.js.map +1 -0
  161. package/dist/providers/provider.d.ts +114 -9
  162. package/dist/providers/provider.d.ts.map +1 -1
  163. package/dist/providers/provider.js +3 -20
  164. package/dist/providers/provider.js.map +1 -1
  165. package/dist/schedules/index.d.ts +94 -13
  166. package/dist/schedules/index.d.ts.map +1 -1
  167. package/dist/schedules/index.js +66 -12
  168. package/dist/schedules/index.js.map +1 -1
  169. package/dist/server/audit-context.d.ts +29 -0
  170. package/dist/server/audit-context.d.ts.map +1 -0
  171. package/dist/server/audit-context.js +44 -0
  172. package/dist/server/audit-context.js.map +1 -0
  173. package/dist/server/context.d.ts +141 -0
  174. package/dist/server/context.d.ts.map +1 -0
  175. package/dist/server/context.js +39 -0
  176. package/dist/server/context.js.map +1 -0
  177. package/dist/server/contract-like.d.ts +1 -1
  178. package/dist/server/contract-like.d.ts.map +1 -1
  179. package/dist/server/contract-like.js +1 -1
  180. package/dist/server/contract-like.js.map +1 -1
  181. package/dist/server/health.d.ts +2 -2
  182. package/dist/server/health.d.ts.map +1 -1
  183. package/dist/server/hooks/auth.d.ts +49 -10
  184. package/dist/server/hooks/auth.d.ts.map +1 -1
  185. package/dist/server/hooks/auth.js +77 -37
  186. package/dist/server/hooks/auth.js.map +1 -1
  187. package/dist/server/hooks/cors.d.ts +1 -1
  188. package/dist/server/hooks/cors.d.ts.map +1 -1
  189. package/dist/server/hooks/errors.d.ts +2 -2
  190. package/dist/server/hooks/errors.d.ts.map +1 -1
  191. package/dist/server/hooks/errors.js +2 -2
  192. package/dist/server/hooks/errors.js.map +1 -1
  193. package/dist/server/hooks/idempotency.d.ts +78 -0
  194. package/dist/server/hooks/idempotency.d.ts.map +1 -0
  195. package/dist/server/hooks/idempotency.js +154 -0
  196. package/dist/server/hooks/idempotency.js.map +1 -0
  197. package/dist/server/hooks/index.d.ts +8 -7
  198. package/dist/server/hooks/index.d.ts.map +1 -1
  199. package/dist/server/hooks/index.js +6 -5
  200. package/dist/server/hooks/index.js.map +1 -1
  201. package/dist/server/hooks/logging.d.ts +2 -2
  202. package/dist/server/hooks/logging.d.ts.map +1 -1
  203. package/dist/server/hooks/logging.js +1 -1
  204. package/dist/server/hooks/logging.js.map +1 -1
  205. package/dist/server/hooks/rate-limit.d.ts +25 -7
  206. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  207. package/dist/server/hooks/rate-limit.js +47 -12
  208. package/dist/server/hooks/rate-limit.js.map +1 -1
  209. package/dist/server/hooks.d.ts +1 -1
  210. package/dist/server/hooks.d.ts.map +1 -1
  211. package/dist/server/hooks.js +1 -1
  212. package/dist/server/hooks.js.map +1 -1
  213. package/dist/server/http.d.ts +61 -35
  214. package/dist/server/http.d.ts.map +1 -1
  215. package/dist/server/http.js +1 -20
  216. package/dist/server/http.js.map +1 -1
  217. package/dist/server/index.d.ts +36 -12
  218. package/dist/server/index.d.ts.map +1 -1
  219. package/dist/server/index.js +24 -8
  220. package/dist/server/index.js.map +1 -1
  221. package/dist/server/instrumentation.d.ts +108 -0
  222. package/dist/server/instrumentation.d.ts.map +1 -0
  223. package/dist/server/instrumentation.js +297 -0
  224. package/dist/server/instrumentation.js.map +1 -0
  225. package/dist/server/openapi.d.ts +3 -3
  226. package/dist/server/openapi.d.ts.map +1 -1
  227. package/dist/server/openapi.js +1 -1
  228. package/dist/server/openapi.js.map +1 -1
  229. package/dist/server/providers/index.d.ts +3 -3
  230. package/dist/server/providers/index.d.ts.map +1 -1
  231. package/dist/server/providers/index.js +3 -3
  232. package/dist/server/providers/index.js.map +1 -1
  233. package/dist/server/providers/loadProviderConfig.d.ts +2 -2
  234. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  235. package/dist/server/providers/loadProviderConfig.js +2 -2
  236. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  237. package/dist/server/request-context.d.ts +67 -0
  238. package/dist/server/request-context.d.ts.map +1 -0
  239. package/dist/server/request-context.js +79 -0
  240. package/dist/server/request-context.js.map +1 -0
  241. package/dist/server/server-context.d.ts +38 -0
  242. package/dist/server/server-context.d.ts.map +1 -0
  243. package/dist/server/server-context.js +38 -0
  244. package/dist/server/server-context.js.map +1 -0
  245. package/dist/server/server.d.ts +105 -33
  246. package/dist/server/server.d.ts.map +1 -1
  247. package/dist/server/server.js +434 -118
  248. package/dist/server/server.js.map +1 -1
  249. package/dist/server/types.d.ts +2 -2
  250. package/dist/server/types.d.ts.map +1 -1
  251. package/dist/server/types.js +2 -2
  252. package/dist/server/types.js.map +1 -1
  253. package/dist/server/use-case-route.d.ts +263 -0
  254. package/dist/server/use-case-route.d.ts.map +1 -0
  255. package/dist/server/use-case-route.js +77 -0
  256. package/dist/server/use-case-route.js.map +1 -0
  257. package/dist/server-only.d.ts +8 -0
  258. package/dist/server-only.d.ts.map +1 -0
  259. package/dist/server-only.js +8 -0
  260. package/dist/server-only.js.map +1 -0
  261. package/dist/tasks/index.d.ts +139 -0
  262. package/dist/tasks/index.d.ts.map +1 -0
  263. package/dist/tasks/index.js +98 -0
  264. package/dist/tasks/index.js.map +1 -0
  265. package/dist/testing/index.d.ts +607 -5
  266. package/dist/testing/index.d.ts.map +1 -1
  267. package/dist/testing/index.js +426 -4
  268. package/dist/testing/index.js.map +1 -1
  269. package/dist/tracing/index.d.ts +89 -0
  270. package/dist/tracing/index.d.ts.map +1 -0
  271. package/dist/tracing/index.js +101 -0
  272. package/dist/tracing/index.js.map +1 -0
  273. package/dist/uploads/client.d.ts +1 -1
  274. package/dist/uploads/client.d.ts.map +1 -1
  275. package/dist/uploads/index.d.ts +2 -2
  276. package/dist/uploads/index.d.ts.map +1 -1
  277. package/dist/uploads/index.js +1 -1
  278. package/dist/uploads/index.js.map +1 -1
  279. package/package.json +24 -2
  280. package/src/application/index.ts +193 -10
  281. package/src/client/client.ts +148 -150
  282. package/src/client/error-messages.ts +35 -0
  283. package/src/client/index.ts +12 -4
  284. package/src/client/types.ts +44 -5
  285. package/src/client-only.ts +7 -0
  286. package/src/config/index.ts +6 -6
  287. package/src/contracts/catalog-errors.ts +115 -0
  288. package/src/contracts/contract-builder.ts +39 -76
  289. package/src/contracts/contract-group.ts +33 -68
  290. package/src/contracts/contract-like.ts +1 -1
  291. package/src/contracts/index.ts +24 -11
  292. package/src/contracts/openapi-meta.ts +55 -0
  293. package/src/contracts/path-template.ts +2 -2
  294. package/src/contracts/schema-shape.ts +75 -0
  295. package/src/contracts/success-status.ts +68 -0
  296. package/src/contracts/types.ts +32 -5
  297. package/src/contracts/utils.ts +5 -2
  298. package/src/domain/events.ts +6 -2
  299. package/src/domain/index.ts +3 -3
  300. package/src/errors/catalog.ts +9 -1
  301. package/src/errors/http.ts +11 -1
  302. package/src/errors/index.ts +4 -4
  303. package/src/errors/response.ts +4 -1
  304. package/src/events/index.ts +12 -26
  305. package/src/idempotency/index.ts +5 -3
  306. package/src/jobs/index.ts +14 -24
  307. package/src/notifications/index.ts +17 -27
  308. package/src/openapi/index.ts +73 -38
  309. package/src/openapi/schema-introspector.ts +68 -17
  310. package/src/outbox/index.ts +84 -19
  311. package/src/ports/audit.ts +120 -11
  312. package/src/ports/auth.ts +132 -0
  313. package/src/ports/events.ts +2 -2
  314. package/src/ports/index.ts +104 -35
  315. package/src/ports/policy.ts +50 -3
  316. package/src/ports/testing.ts +2220 -33
  317. package/src/ports/unbound.ts +64 -0
  318. package/src/ports/unit-of-work.ts +6 -2
  319. package/src/providers/index.ts +16 -3
  320. package/src/providers/instrumentation.ts +86 -7
  321. package/src/providers/metadata.ts +234 -0
  322. package/src/providers/provider.ts +168 -9
  323. package/src/schedules/index.ts +173 -23
  324. package/src/server/audit-context.ts +45 -0
  325. package/src/server/context.ts +224 -0
  326. package/src/server/contract-like.ts +1 -1
  327. package/src/server/health.ts +2 -2
  328. package/src/server/hooks/auth.ts +141 -51
  329. package/src/server/hooks/cors.ts +1 -1
  330. package/src/server/hooks/errors.ts +7 -4
  331. package/src/server/hooks/idempotency.ts +263 -0
  332. package/src/server/hooks/index.ts +14 -7
  333. package/src/server/hooks/logging.ts +3 -3
  334. package/src/server/hooks/rate-limit.ts +85 -17
  335. package/src/server/hooks.ts +1 -1
  336. package/src/server/http.ts +78 -51
  337. package/src/server/index.ts +62 -12
  338. package/src/server/instrumentation.ts +470 -0
  339. package/src/server/openapi.ts +4 -4
  340. package/src/server/providers/index.ts +6 -3
  341. package/src/server/providers/loadProviderConfig.ts +4 -4
  342. package/src/server/request-context.ts +116 -0
  343. package/src/server/server-context.ts +44 -0
  344. package/src/server/server.ts +886 -238
  345. package/src/server/types.ts +2 -2
  346. package/src/server/use-case-route.ts +430 -0
  347. package/src/server-only.ts +7 -0
  348. package/src/tasks/index.ts +275 -0
  349. package/src/testing/index.ts +1142 -6
  350. package/src/tracing/index.ts +176 -0
  351. package/src/uploads/client.ts +1 -1
  352. package/src/uploads/index.ts +7 -3
  353. package/dist/ports/mailer.d.ts +0 -6
  354. package/dist/ports/mailer.d.ts.map +0 -1
  355. package/dist/ports/mailer.js +0 -2
  356. package/dist/ports/mailer.js.map +0 -1
  357. package/dist/ports/schedules.d.ts +0 -9
  358. package/dist/ports/schedules.d.ts.map +0 -1
  359. package/dist/ports/schedules.js +0 -2
  360. package/dist/ports/schedules.js.map +0 -1
@@ -7,24 +7,46 @@ import {
7
7
  methodSupportsRequestBody,
8
8
  parsePathTemplate,
9
9
  type StandardSchema,
10
- } from "../contracts";
10
+ } from "../contracts/index.js";
11
+ import {
12
+ comparePathParamsToTemplate,
13
+ formatPathParamsMismatch,
14
+ getObjectSchemaShape,
15
+ } from "../contracts/schema-shape.js";
11
16
  import {
12
17
  createErrorResponseBody,
18
+ httpErrors,
13
19
  isAppError,
14
20
  isErrorResponseBody,
15
21
  toErrorResponseBody,
16
- } from "../errors";
17
- import type { AnyPorts } from "../ports";
18
- import { AuthUnauthorizedError, GateAuthorizationError } from "../ports";
22
+ } from "../errors/index.js";
23
+ import {
24
+ IdempotencyConflictError,
25
+ IdempotencyInProgressError,
26
+ } from "../idempotency/index.js";
27
+ import type { AnyPorts } from "../ports/index.js";
28
+ import {
29
+ AuthUnauthorizedError,
30
+ GateAuthorizationError,
31
+ isUnboundPort,
32
+ TenantRequiredError,
33
+ } from "../ports/index.js";
19
34
  import type {
20
- ProvidedPortsOfList,
35
+ InferProviderPorts,
21
36
  ProviderSetupResult,
22
37
  ServiceProvider,
23
- } from "../providers";
24
- import type { ContractLike, ResolveContract } from "./contract-like";
25
- import { resolveContract } from "./contract-like";
26
- import { getRequestIdFromContext } from "./hooks/utils";
38
+ } from "../providers/index.js";
27
39
  import type {
40
+ ContextSeed,
41
+ ServerContextConfig,
42
+ ServiceContextInputArgs,
43
+ } from "./context.js";
44
+ import { createContextFinalizer, resolveServerContext } from "./context.js";
45
+ import type { ContractLike, ResolveContract } from "./contract-like.js";
46
+ import { resolveContract } from "./contract-like.js";
47
+ import { getRequestIdFromContext } from "./hooks/utils.js";
48
+ import type {
49
+ AddedCtxFromHooks,
28
50
  Handler,
29
51
  HandlerArgs,
30
52
  HttpRequestLike,
@@ -35,21 +57,39 @@ import type {
35
57
  ServerCaughtErrorHook,
36
58
  ServerHook,
37
59
  ServerUnhandledErrorMapper,
38
- } from "./http";
60
+ } from "./http.js";
61
+ import type { ServerInstrumentationOptions } from "./instrumentation.js";
62
+ import { createServerInstrumentation } from "./instrumentation.js";
39
63
  import {
40
64
  loadProviderConfig,
41
65
  parseStandardSchema,
42
66
  SchemaValidationError,
43
- } from "./providers";
67
+ } from "./providers/index.js";
68
+ import type { ActiveRequestContext } from "./request-context.js";
69
+ import {
70
+ enterActiveRequestContext,
71
+ readContextActor,
72
+ readContextTenant,
73
+ setActiveRequestIdentity,
74
+ } from "./request-context.js";
75
+ import type {
76
+ AnyUseCaseLike,
77
+ AnyUseCaseRouteDef,
78
+ UseCaseRouteDef,
79
+ ValidatedRouteInputs,
80
+ } from "./use-case-route.js";
81
+ import {
82
+ createUseCaseRouteHandler,
83
+ isUseCaseRouteDef,
84
+ } from "./use-case-route.js";
44
85
 
45
86
  /**
46
- * Route registration for one contract.
87
+ * Route registration that connects a contract to the handler implementing it.
47
88
  *
48
- * Route definitions connect a contract to the handler that implements it. Most
49
- * apps keep these in `features/<feature>/routes.ts` and compose them with
50
- * `defineRoutes(...)`.
89
+ * Most apps keep route definitions in `features/<feature>/routes.ts` and
90
+ * compose them with `defineRoutes(...)`.
51
91
  */
52
- export type RouteDef<
92
+ export type HandlerRouteDef<
53
93
  Ctx,
54
94
  CLike extends ContractLike = ContractLike,
55
95
  Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
@@ -69,23 +109,27 @@ export type RouteDef<
69
109
  * Handler that implements the contract.
70
110
  */
71
111
  handle: Handler<Ctx & AddedCtxFromHooks<Hooks>, ResolveContract<CLike>>;
112
+ useCase?: never;
113
+ input?: never;
114
+ status?: never;
72
115
  };
73
116
 
74
- type AddedCtxFromHook<Hook> =
75
- Hook extends RouteHook<infer _Ctx, infer AddedCtx> ? AddedCtx : unknown;
76
-
77
- type UnionToIntersection<Union> = (
78
- Union extends unknown
79
- ? (value: Union) => void
80
- : never
81
- ) extends (value: infer Intersection) => void
82
- ? Intersection
83
- : never;
84
-
85
- type AddedCtxFromHooks<Hooks extends readonly unknown[]> =
86
- Hooks extends readonly []
87
- ? unknown
88
- : UnionToIntersection<AddedCtxFromHook<Hooks[number]>>;
117
+ /**
118
+ * Route registration for one contract.
119
+ *
120
+ * Routes either bind the contract directly to a use case (`{ contract,
121
+ * useCase }`) or implement a full handler (`{ contract, handle }`). The full
122
+ * handler form is the escape hatch for response headers, streaming, native
123
+ * `Response` values, and multi-status handling.
124
+ */
125
+ export type RouteDef<
126
+ Ctx,
127
+ CLike extends ContractLike = ContractLike,
128
+ Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
129
+ Ctx,
130
+ object
131
+ >[],
132
+ > = HandlerRouteDef<Ctx, CLike, Hooks> | AnyUseCaseRouteDef<Ctx, CLike, Hooks>;
89
133
 
90
134
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
91
135
  type AnyRouteDef = RouteDef<any, any>;
@@ -145,7 +189,7 @@ type RouteGroupBuilder<Ctx> = {
145
189
  >(group: {
146
190
  name: string;
147
191
  hooks?: GroupHooks;
148
- routes: R;
192
+ routes: R & ValidatedRouteInputs<Ctx & AddedCtxFromHooks<GroupHooks>, R>;
149
193
  }): RouteGroup<Ctx, R>;
150
194
  <
151
195
  const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
@@ -153,7 +197,7 @@ type RouteGroupBuilder<Ctx> = {
153
197
  >(group: {
154
198
  name: string;
155
199
  hooks?: GroupHooks;
156
- routes: R;
200
+ routes: R & ValidatedRouteInputs<Ctx & AddedCtxFromHooks<GroupHooks>, R>;
157
201
  }): RouteGroup<Ctx, R>;
158
202
  };
159
203
 
@@ -192,28 +236,54 @@ type ContractsFromRouteList<
192
236
  * added fields.
193
237
  */
194
238
  export function defineRoute<Ctx>() {
195
- return <
239
+ function define<
240
+ CLike extends ContractLike,
241
+ UC extends AnyUseCaseLike,
242
+ const Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
243
+ >(
244
+ route: UseCaseRouteDef<Ctx, CLike, UC, Hooks>,
245
+ ): UseCaseRouteDef<Ctx, CLike, UC, Hooks>;
246
+ function define<
196
247
  CLike extends ContractLike,
197
248
  const Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
198
249
  >(
199
- route: RouteDef<Ctx, CLike, Hooks>,
200
- ): RouteDef<Ctx, CLike, Hooks> => route;
250
+ route: HandlerRouteDef<Ctx, CLike, Hooks>,
251
+ ): HandlerRouteDef<Ctx, CLike, Hooks>;
252
+ function define(route: unknown): unknown {
253
+ return route;
254
+ }
255
+
256
+ return define;
201
257
  }
202
258
 
259
+ /**
260
+ * Loosely typed provider list element.
261
+ *
262
+ * Required-port, app-context, and service-input generics are erased here so
263
+ * providers created with the typed `createProvider<Requires, Context,
264
+ * ServiceInput>()` form stay assignable to server provider lists.
265
+ */
266
+ type AnyServiceProvider = ServiceProvider<
267
+ unknown,
268
+ // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
269
+ StandardSchemaV1<any, any>,
270
+ AnyPorts,
271
+ // biome-ignore lint/suspicious/noExplicitAny: provider context types are erased at this level
272
+ any,
273
+ // biome-ignore lint/suspicious/noExplicitAny: provider service-input types are erased at this level
274
+ any
275
+ >;
276
+
203
277
  /**
204
278
  * Options for creating a Beignet server instance.
205
279
  */
206
280
  export type CreateServerOptions<
207
281
  Ctx,
208
282
  Ports extends AnyPorts,
283
+ ServiceInput = void,
209
284
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
210
285
  Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
211
- Providers extends readonly ServiceProvider<
212
- unknown,
213
- // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
214
- StandardSchemaV1<any, any>,
215
- AnyPorts
216
- >[] = readonly [],
286
+ Providers extends readonly AnyServiceProvider[] = readonly [],
217
287
  > = {
218
288
  /**
219
289
  * App-owned ports available to context creation, hooks, and handlers.
@@ -237,24 +307,64 @@ export type CreateServerOptions<
237
307
  providerConfig?: Record<string, unknown>;
238
308
 
239
309
  /**
240
- * Create request context after a route is matched.
310
+ * Context blueprint for request and service contexts.
311
+ *
312
+ * Gate-less contexts may pass a plain request factory. Contexts with a
313
+ * `gate` property must use the blueprint form
314
+ * `{ gate: (ports) => ports.gate, request, service }` so the server owns
315
+ * gate attachment and identity changes can never go stale.
241
316
  *
242
317
  * The `ports` argument includes app ports plus ports provided during server
243
318
  * startup.
244
319
  */
245
- createContext: (args: {
246
- req: HttpRequestLike;
247
- ports: Ports & ProvidedPortsOfList<Providers>;
248
- contract?: HttpContractConfig;
249
- }) => Ctx | Promise<Ctx>;
320
+ context: ServerContextConfig<
321
+ Ctx,
322
+ Ports & InferProviderPorts<Providers>,
323
+ ServiceInput
324
+ >;
250
325
  /**
251
326
  * Server hooks that wrap every registered route.
252
327
  */
253
- hooks?: ServerHook<Ctx, Ports & ProvidedPortsOfList<Providers>>[];
328
+ hooks?: ServerHook<Ctx, Ports & InferProviderPorts<Providers>>[];
329
+ /**
330
+ * Server-owned request instrumentation.
331
+ *
332
+ * The server resolves a request ID and W3C trace context for every request
333
+ * before user hooks and context creation, writes `x-request-id` and
334
+ * `traceparent` response headers, and records request and error events into
335
+ * the resolved provider instrumentation port (`ports.instrumentation`, then
336
+ * `ports.devtools`) when one is installed.
337
+ *
338
+ * Pass `false` to disable headers and event recording. Context factories
339
+ * still receive `requestId` and `trace` arguments.
340
+ */
341
+ instrumentation?: ServerInstrumentationOptions<Ctx> | false;
342
+ /**
343
+ * Whether route-owned responses are validated against the contract's
344
+ * declared statuses and response schemas before they are sent.
345
+ *
346
+ * Disable this to trade response guarantees for throughput, mirroring the
347
+ * client-side `validateResponses` option.
348
+ *
349
+ * @default true
350
+ */
351
+ validateResponses?: boolean;
254
352
  /**
255
353
  * Route list to register up front.
256
354
  */
257
355
  routes?: Routes;
356
+ /**
357
+ * How to handle ports that are still unbound after all providers have
358
+ * started.
359
+ *
360
+ * Ports declared as `deferred` in `definePorts(...)` boot as throwing
361
+ * placeholders until a provider contributes them. The default `"error"`
362
+ * fails startup and lists the unbound port keys. Apps that bind every port
363
+ * directly are unaffected.
364
+ *
365
+ * @default "error"
366
+ */
367
+ onUnboundPorts?: "error" | "warn" | "ignore";
258
368
  /**
259
369
  * Global caught-error observer.
260
370
  */
@@ -274,7 +384,11 @@ interface RouteBuilder<Ctx, C extends HttpContractConfig> {
274
384
  /**
275
385
  * Runtime server object returned by `createServer(...)`.
276
386
  */
277
- export interface ServerInstance<Ctx, Ports extends AnyPorts = AnyPorts> {
387
+ export interface ServerInstance<
388
+ Ctx,
389
+ Ports extends AnyPorts = AnyPorts,
390
+ ServiceInput = void,
391
+ > {
278
392
  /**
279
393
  * Catch-all request handler for platform adapters.
280
394
  */
@@ -285,6 +399,22 @@ export interface ServerInstance<Ctx, Ports extends AnyPorts = AnyPorts> {
285
399
  route: <CLike extends ContractLike>(
286
400
  contractLike: CLike,
287
401
  ) => RouteBuilder<Ctx, ResolveContract<CLike>>;
402
+ /**
403
+ * Build a fully assembled request context from a framework-neutral request.
404
+ *
405
+ * Use this for adapter entry points outside the route pipeline, such as
406
+ * server components or upload routes.
407
+ */
408
+ createRequestContext: (req: HttpRequestLike) => Promise<Ctx>;
409
+ /**
410
+ * Build a fully assembled service context for schedules, outbox drains,
411
+ * tasks, and background work.
412
+ *
413
+ * Requires `context.service` to be declared in `createServer(...)`.
414
+ */
415
+ createServiceContext: (
416
+ ...args: ServiceContextInputArgs<ServiceInput>
417
+ ) => Promise<Ctx>;
288
418
  /**
289
419
  * Contract configs registered through the `routes` option.
290
420
  */
@@ -380,6 +510,59 @@ function errorResponse(
380
510
  };
381
511
  }
382
512
 
513
+ type RequestValidationLocation = "query" | "path" | "headers" | "body";
514
+
515
+ function contractDiagnostics(contract: HttpContractConfig) {
516
+ return {
517
+ contract: contract.name,
518
+ method: contract.method,
519
+ path: contract.path,
520
+ };
521
+ }
522
+
523
+ function requestValidationDetails(
524
+ contract: HttpContractConfig,
525
+ location: RequestValidationLocation,
526
+ error?: unknown,
527
+ ) {
528
+ const details = {
529
+ ...contractDiagnostics(contract),
530
+ location,
531
+ };
532
+
533
+ if (error instanceof SchemaValidationError) {
534
+ return {
535
+ ...details,
536
+ issues: error.issues,
537
+ };
538
+ }
539
+
540
+ if (error instanceof Error) {
541
+ return {
542
+ ...details,
543
+ message: error.message,
544
+ };
545
+ }
546
+
547
+ return details;
548
+ }
549
+
550
+ function requestValidationError(
551
+ contract: HttpContractConfig,
552
+ status: number,
553
+ code: string,
554
+ message: string,
555
+ location: RequestValidationLocation,
556
+ error?: unknown,
557
+ ): HttpResponseLike {
558
+ return errorResponse(
559
+ status,
560
+ code,
561
+ message,
562
+ requestValidationDetails(contract, location, error),
563
+ );
564
+ }
565
+
383
566
  function normalizeResponse(res: HttpResponseLike): HttpResponseLike {
384
567
  return {
385
568
  status: res.status,
@@ -473,6 +656,48 @@ function responseForHooks(res: HttpResponse): HttpResponseLike {
473
656
  };
474
657
  }
475
658
 
659
+ /**
660
+ * Merge hook-applied header changes onto a native web Response.
661
+ *
662
+ * Starts from the native response's `Headers` so `set-cookie` multiplicity is
663
+ * preserved, then applies headers the beforeSend chain added or changed
664
+ * relative to the original headers-only view. The body stream passes through
665
+ * untouched; status and statusText are preserved.
666
+ */
667
+ function mergeNativeResponseHeaders(
668
+ nativeResponse: Response,
669
+ originalHeaders: Record<string, string>,
670
+ finalHeaders: Record<string, string>,
671
+ ): Response {
672
+ const originalByLowerKey = new Map<string, string>();
673
+ for (const [key, value] of Object.entries(originalHeaders)) {
674
+ originalByLowerKey.set(key.toLowerCase(), value);
675
+ }
676
+
677
+ let changed = false;
678
+ const merged = new Headers(nativeResponse.headers);
679
+ for (const [key, value] of Object.entries(finalHeaders)) {
680
+ const lowerKey = key.toLowerCase();
681
+ if (originalByLowerKey.get(lowerKey) === value) continue;
682
+ changed = true;
683
+ if (lowerKey === "set-cookie") {
684
+ merged.append(lowerKey, value);
685
+ } else {
686
+ merged.set(key, value);
687
+ }
688
+ }
689
+
690
+ if (!changed) {
691
+ return nativeResponse;
692
+ }
693
+
694
+ return new Response(nativeResponse.body, {
695
+ status: nativeResponse.status,
696
+ statusText: nativeResponse.statusText,
697
+ headers: merged,
698
+ });
699
+ }
700
+
476
701
  function isHttpResponseLike(value: unknown): value is HttpResponseLike {
477
702
  return (
478
703
  !isWebResponse(value) &&
@@ -499,6 +724,37 @@ class ResponseContractViolationError extends Error {
499
724
  }
500
725
  }
501
726
 
727
+ function responseContractViolationMessage(
728
+ contract: HttpContractConfig,
729
+ status: number,
730
+ ): string {
731
+ return (
732
+ `Response validation failed for ${contract.method} ${contract.path} ` +
733
+ `(status ${status}, contract: ${contract.name})`
734
+ );
735
+ }
736
+
737
+ function declaredResponseStatuses(contract: HttpContractConfig): number[] {
738
+ return Object.keys(contract.responses)
739
+ .map((status) => Number(status))
740
+ .filter((status) => Number.isFinite(status))
741
+ .sort((a, b) => a - b);
742
+ }
743
+
744
+ function responseContractViolationDetails(
745
+ contract: HttpContractConfig,
746
+ status: number,
747
+ details?: Record<string, unknown>,
748
+ ) {
749
+ return {
750
+ ...contractDiagnostics(contract),
751
+ location: "response",
752
+ status,
753
+ declaredStatuses: declaredResponseStatuses(contract),
754
+ ...details,
755
+ };
756
+ }
757
+
502
758
  function getDeclaredCatalogErrorsForStatus(
503
759
  contract: HttpContractConfig,
504
760
  status: number,
@@ -536,10 +792,8 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
536
792
  if (!matchingError) {
537
793
  throw new ResponseContractViolationError({
538
794
  code: "RESPONSE_VALIDATION_ERROR",
539
- message:
540
- `Response validation failed for ${contract.method} ${contract.path} ` +
541
- `(status ${res.status}, contract: ${contract.name})`,
542
- details: {
795
+ message: responseContractViolationMessage(contract, res.status),
796
+ details: responseContractViolationDetails(contract, res.status, {
543
797
  issues: [
544
798
  {
545
799
  message:
@@ -547,7 +801,7 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
547
801
  `Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
548
802
  },
549
803
  ],
550
- },
804
+ }),
551
805
  });
552
806
  }
553
807
 
@@ -558,10 +812,10 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
558
812
  if (error instanceof SchemaValidationError) {
559
813
  throw new ResponseContractViolationError({
560
814
  code: "RESPONSE_VALIDATION_ERROR",
561
- message:
562
- `Response validation failed for ${contract.method} ${contract.path} ` +
563
- `(status ${res.status}, contract: ${contract.name})`,
564
- details: { issues: error.issues },
815
+ message: responseContractViolationMessage(contract, res.status),
816
+ details: responseContractViolationDetails(contract, res.status, {
817
+ issues: error.issues,
818
+ }),
565
819
  });
566
820
  }
567
821
  throw error;
@@ -572,6 +826,7 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
572
826
  async function validateResponseAgainstContract<C extends HttpContractConfig>(
573
827
  contract: C,
574
828
  res: HttpResponseLike,
829
+ responseValidationExemptStatus?: number,
575
830
  ): Promise<void> {
576
831
  const statusKey = String(res.status);
577
832
  const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
@@ -584,10 +839,9 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
584
839
  message:
585
840
  `Handler returned undeclared status ${res.status} for ` +
586
841
  `${contract.method} ${contract.path} (contract: ${contract.name})`,
587
- details: {
588
- status: res.status,
589
- body: res.body,
590
- },
842
+ details: responseContractViolationDetails(contract, res.status, {
843
+ returnedStatus: res.status,
844
+ }),
591
845
  });
592
846
  }
593
847
 
@@ -596,17 +850,15 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
596
850
  if (res.body !== undefined && res.body !== null) {
597
851
  throw new ResponseContractViolationError({
598
852
  code: "RESPONSE_VALIDATION_ERROR",
599
- message:
600
- `Response validation failed for ${contract.method} ${contract.path} ` +
601
- `(status ${res.status}, contract: ${contract.name})`,
602
- details: {
853
+ message: responseContractViolationMessage(contract, res.status),
854
+ details: responseContractViolationDetails(contract, res.status, {
603
855
  issues: [
604
856
  {
605
857
  message:
606
858
  "Response body must be empty for a null response schema.",
607
859
  },
608
860
  ],
609
- },
861
+ }),
610
862
  });
611
863
  }
612
864
  return;
@@ -614,6 +866,11 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
614
866
 
615
867
  if (!responseSchema) return;
616
868
 
869
+ // Binder routes whose use case output schema is the same object as the
870
+ // declared success response schema skip the redundant success-status parse.
871
+ // Error statuses and undeclared statuses are validated unchanged.
872
+ if (res.status === responseValidationExemptStatus) return;
873
+
617
874
  try {
618
875
  await parseStandardSchema(responseSchema, res.body);
619
876
  await validateCatalogErrorResponse(contract, res);
@@ -621,10 +878,10 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
621
878
  if (error instanceof SchemaValidationError) {
622
879
  throw new ResponseContractViolationError({
623
880
  code: "RESPONSE_VALIDATION_ERROR",
624
- message:
625
- `Response validation failed for ${contract.method} ${contract.path} ` +
626
- `(status ${res.status}, contract: ${contract.name})`,
627
- details: { issues: error.issues },
881
+ message: responseContractViolationMessage(contract, res.status),
882
+ details: responseContractViolationDetails(contract, res.status, {
883
+ issues: error.issues,
884
+ }),
628
885
  });
629
886
  }
630
887
  throw error;
@@ -634,9 +891,14 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
634
891
  async function finalizeResponse<C extends HttpContractConfig>(
635
892
  contract: C,
636
893
  res: HttpResponseLike,
894
+ responseValidationExemptStatus?: number,
637
895
  ): Promise<HttpResponseLike> {
638
896
  const normalized = normalizeResponse(res);
639
- await validateResponseAgainstContract(contract, normalized);
897
+ await validateResponseAgainstContract(
898
+ contract,
899
+ normalized,
900
+ responseValidationExemptStatus,
901
+ );
640
902
  return normalized;
641
903
  }
642
904
 
@@ -696,39 +958,65 @@ async function parseBody(req: HttpRequestLike): Promise<unknown> {
696
958
  }
697
959
  }
698
960
 
699
- function buildHandler<
961
+ type ExecutionTarget<Ctx, C extends HttpContractConfig> = {
962
+ contract: C;
963
+ /**
964
+ * Compiled route pattern. Only required when the executor is invoked
965
+ * without pre-matched params.
966
+ */
967
+ compiled?: CompiledPath;
968
+ handler: Handler<Ctx, C>;
969
+ /**
970
+ * Success status whose response schema validation is skipped because the
971
+ * use case bound to this route already validated its output against the
972
+ * same schema object.
973
+ */
974
+ responseValidationExemptStatus?: number;
975
+ };
976
+
977
+ /**
978
+ * Build the per-request execution pipeline once.
979
+ *
980
+ * The returned executor takes the contract, compiled pattern, and user handler
981
+ * per invocation so fallback responses (404/405) can reuse a single pipeline
982
+ * across requests instead of rebuilding it per unmatched request.
983
+ */
984
+ function createRequestExecutor<
700
985
  Ctx,
701
986
  Ports extends AnyPorts,
702
987
  C extends HttpContractConfig,
703
- Providers extends readonly ServiceProvider<
704
- unknown,
705
- // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
706
- StandardSchemaV1<any, any>,
707
- AnyPorts
708
- >[] = readonly [],
709
- FinalPorts extends Ports & ProvidedPortsOfList<Providers> = Ports &
710
- ProvidedPortsOfList<Providers>,
988
+ Providers extends readonly AnyServiceProvider[] = readonly [],
989
+ FinalPorts extends Ports & InferProviderPorts<Providers> = Ports &
990
+ InferProviderPorts<Providers>,
711
991
  >(
712
992
  // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
713
- options: CreateServerOptions<Ctx, Ports, any, Providers>,
993
+ options: CreateServerOptions<Ctx, Ports, any, any, Providers>,
714
994
  finalPorts: FinalPorts,
715
- contract: C,
716
- userHandler: Handler<Ctx, C>,
995
+ contextRuntime: {
996
+ createRequestContext: (
997
+ req: HttpRequestLike,
998
+ contract: HttpContractConfig,
999
+ ) => Promise<Ctx>;
1000
+ finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
1001
+ },
717
1002
  hooks: ServerHook<Ctx, FinalPorts>[],
718
1003
  routeHooks: readonly RouteHook<unknown, object>[] = [],
719
1004
  optionsOverrides?: {
720
1005
  skipRoutePreparation?: boolean;
721
1006
  },
722
1007
  ): (
1008
+ target: ExecutionTarget<Ctx, C>,
723
1009
  req: HttpRequestLike,
724
1010
  preMatchedParams?: Record<string, string>,
725
1011
  ) => Promise<HttpResponse> {
726
- const compiled = compilePath(contract.path);
1012
+ const warnedNativeReplacementHooks = new WeakSet<object>();
727
1013
 
728
1014
  return async (
1015
+ target: ExecutionTarget<Ctx, C>,
729
1016
  req: HttpRequestLike,
730
1017
  preMatchedParams?: Record<string, string>,
731
1018
  ) => {
1019
+ const { contract, handler: userHandler } = target;
732
1020
  let baseCtx: Ctx | undefined;
733
1021
  let pathValue: HandlerArgs<Ctx, C>["path"] | undefined;
734
1022
  let queryValue: HandlerArgs<Ctx, C>["query"] | undefined;
@@ -809,6 +1097,53 @@ function buildHandler<
809
1097
  };
810
1098
  }
811
1099
 
1100
+ if (currentError instanceof TenantRequiredError) {
1101
+ return {
1102
+ ctx,
1103
+ response: errorResponse(
1104
+ currentError.status,
1105
+ currentError.code,
1106
+ currentError.message,
1107
+ ),
1108
+ error: currentError,
1109
+ owner: "framework",
1110
+ };
1111
+ }
1112
+
1113
+ if (currentError instanceof IdempotencyConflictError) {
1114
+ return {
1115
+ ctx,
1116
+ response: errorResponse(
1117
+ httpErrors.IdempotencyConflict.status,
1118
+ httpErrors.IdempotencyConflict.code,
1119
+ currentError.message,
1120
+ {
1121
+ namespace: currentError.namespace,
1122
+ key: currentError.key,
1123
+ },
1124
+ ),
1125
+ error: currentError,
1126
+ owner: "framework",
1127
+ };
1128
+ }
1129
+
1130
+ if (currentError instanceof IdempotencyInProgressError) {
1131
+ return {
1132
+ ctx,
1133
+ response: errorResponse(
1134
+ httpErrors.IdempotencyInProgress.status,
1135
+ httpErrors.IdempotencyInProgress.code,
1136
+ currentError.message,
1137
+ {
1138
+ namespace: currentError.namespace,
1139
+ key: currentError.key,
1140
+ },
1141
+ ),
1142
+ error: currentError,
1143
+ owner: "framework",
1144
+ };
1145
+ }
1146
+
812
1147
  if (currentError instanceof GateAuthorizationError) {
813
1148
  return {
814
1149
  ctx,
@@ -893,8 +1228,10 @@ function buildHandler<
893
1228
  if (preMatchedParams) {
894
1229
  matchedParams = preMatchedParams;
895
1230
  } else {
896
- const match = compiled.pattern.exec(url.pathname);
1231
+ const compiled = target.compiled;
1232
+ const match = compiled ? compiled.pattern.exec(url.pathname) : null;
897
1233
  if (
1234
+ !compiled ||
898
1235
  !match ||
899
1236
  contract.method.toUpperCase() !== req.method.toUpperCase()
900
1237
  ) {
@@ -911,15 +1248,67 @@ function buildHandler<
911
1248
  }
912
1249
  const rawHeaders = requestHeadersToRecord(req.headers);
913
1250
 
1251
+ const runNativeBeforeSend = async (
1252
+ initialResult: ExecutionResult<Ctx>,
1253
+ nativeResponse: Response,
1254
+ ): Promise<Response> => {
1255
+ const originalView = responseForHooks(nativeResponse);
1256
+ const originalHeaders = originalView.headers ?? {};
1257
+ let transformed = originalView;
1258
+ for (const hook of hooks) {
1259
+ if (!hook.beforeSend) continue;
1260
+ const nextResponse = await hook.beforeSend({
1261
+ req,
1262
+ ctx: initialResult.ctx,
1263
+ contract,
1264
+ path: pathValue,
1265
+ query: queryValue,
1266
+ headers: headersValue,
1267
+ body: bodyValue,
1268
+ response: transformed,
1269
+ error: initialResult.error,
1270
+ native: true,
1271
+ });
1272
+ if (nextResponse) {
1273
+ if (
1274
+ (nextResponse.status !== nativeResponse.status ||
1275
+ nextResponse.body !== undefined) &&
1276
+ !warnedNativeReplacementHooks.has(hook) &&
1277
+ process.env.NODE_ENV !== "production"
1278
+ ) {
1279
+ warnedNativeReplacementHooks.add(hook);
1280
+ console.warn(
1281
+ `[beignet] beforeSend hook "${hook.name ?? "(anonymous)"}" returned a replacement status or body for a native Response on ${contract.method} ${contract.path}. Native responses are headers-only in beforeSend; status and body changes are ignored.`,
1282
+ );
1283
+ }
1284
+ transformed = {
1285
+ status: nativeResponse.status,
1286
+ headers: nextResponse.headers,
1287
+ };
1288
+ }
1289
+ }
1290
+ return mergeNativeResponseHeaders(
1291
+ nativeResponse,
1292
+ originalHeaders,
1293
+ transformed.headers ?? {},
1294
+ );
1295
+ };
1296
+
914
1297
  const applyTransformHooks = async (
915
1298
  initialResult: ExecutionResult<Ctx>,
916
1299
  allowRetry: boolean,
917
1300
  ): Promise<ExecutionResult<Ctx>> => {
918
- if (isWebResponse(initialResult.response)) {
919
- return initialResult;
920
- }
921
-
922
1301
  try {
1302
+ if (isWebResponse(initialResult.response)) {
1303
+ return {
1304
+ ...initialResult,
1305
+ response: await runNativeBeforeSend(
1306
+ initialResult,
1307
+ initialResult.response,
1308
+ ),
1309
+ };
1310
+ }
1311
+
923
1312
  let transformed = normalizeResponse(initialResult.response);
924
1313
  for (const hook of hooks) {
925
1314
  if (!hook.beforeSend) continue;
@@ -996,11 +1385,10 @@ function buildHandler<
996
1385
  if (optionsOverrides?.skipRoutePreparation) {
997
1386
  let createdCtx!: Ctx;
998
1387
  try {
999
- createdCtx = await options.createContext({
1388
+ createdCtx = await contextRuntime.createRequestContext(
1000
1389
  req,
1001
- ports: finalPorts,
1002
1390
  contract,
1003
- });
1391
+ );
1004
1392
  baseCtx = createdCtx;
1005
1393
  } catch (error) {
1006
1394
  result = await resolveErrorResult(
@@ -1056,26 +1444,17 @@ function buildHandler<
1056
1444
  try {
1057
1445
  query = await parseStandardSchema(contract.query, query);
1058
1446
  } catch (error) {
1059
- result =
1060
- error instanceof SchemaValidationError
1061
- ? {
1062
- response: errorResponse(
1063
- 422,
1064
- "VALIDATION_ERROR",
1065
- "Invalid query parameters",
1066
- { issues: error.issues },
1067
- ),
1068
- owner: "framework",
1069
- }
1070
- : {
1071
- response: errorResponse(
1072
- 422,
1073
- "VALIDATION_ERROR",
1074
- "Invalid query parameters",
1075
- (error as Error).message,
1076
- ),
1077
- owner: "framework",
1078
- };
1447
+ result = {
1448
+ response: requestValidationError(
1449
+ contract,
1450
+ 422,
1451
+ "VALIDATION_ERROR",
1452
+ "Invalid query parameters",
1453
+ "query",
1454
+ error,
1455
+ ),
1456
+ owner: "framework",
1457
+ };
1079
1458
  }
1080
1459
  }
1081
1460
 
@@ -1088,26 +1467,17 @@ function buildHandler<
1088
1467
  matchedParams,
1089
1468
  );
1090
1469
  } catch (error) {
1091
- result =
1092
- error instanceof SchemaValidationError
1093
- ? {
1094
- response: errorResponse(
1095
- 422,
1096
- "VALIDATION_ERROR",
1097
- "Invalid path parameters",
1098
- { issues: error.issues },
1099
- ),
1100
- owner: "framework",
1101
- }
1102
- : {
1103
- response: errorResponse(
1104
- 422,
1105
- "VALIDATION_ERROR",
1106
- "Invalid path parameters",
1107
- (error as Error).message,
1108
- ),
1109
- owner: "framework",
1110
- };
1470
+ result = {
1471
+ response: requestValidationError(
1472
+ contract,
1473
+ 422,
1474
+ "VALIDATION_ERROR",
1475
+ "Invalid path parameters",
1476
+ "path",
1477
+ error,
1478
+ ),
1479
+ owner: "framework",
1480
+ };
1111
1481
  }
1112
1482
  }
1113
1483
 
@@ -1118,26 +1488,17 @@ function buildHandler<
1118
1488
  try {
1119
1489
  headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
1120
1490
  } catch (error) {
1121
- result =
1122
- error instanceof SchemaValidationError
1123
- ? {
1124
- response: errorResponse(
1125
- 422,
1126
- "VALIDATION_ERROR",
1127
- "Invalid request headers",
1128
- { issues: error.issues },
1129
- ),
1130
- owner: "framework",
1131
- }
1132
- : {
1133
- response: errorResponse(
1134
- 422,
1135
- "VALIDATION_ERROR",
1136
- "Invalid request headers",
1137
- (error as Error).message,
1138
- ),
1139
- owner: "framework",
1140
- };
1491
+ result = {
1492
+ response: requestValidationError(
1493
+ contract,
1494
+ 422,
1495
+ "VALIDATION_ERROR",
1496
+ "Invalid request headers",
1497
+ "headers",
1498
+ error,
1499
+ ),
1500
+ owner: "framework",
1501
+ };
1141
1502
  }
1142
1503
  }
1143
1504
 
@@ -1146,9 +1507,16 @@ function buildHandler<
1146
1507
  if (!result) {
1147
1508
  try {
1148
1509
  body = await parseBody(req);
1149
- } catch {
1510
+ } catch (error) {
1150
1511
  result = {
1151
- response: errorResponse(400, "INVALID_BODY", "Malformed JSON"),
1512
+ response: requestValidationError(
1513
+ contract,
1514
+ 400,
1515
+ "INVALID_BODY",
1516
+ "Malformed JSON",
1517
+ "body",
1518
+ error,
1519
+ ),
1152
1520
  owner: "framework",
1153
1521
  };
1154
1522
  }
@@ -1163,34 +1531,28 @@ function buildHandler<
1163
1531
  error instanceof SchemaValidationError
1164
1532
  ) {
1165
1533
  result = {
1166
- response: errorResponse(
1534
+ response: requestValidationError(
1535
+ contract,
1167
1536
  400,
1168
1537
  "MISSING_BODY",
1169
1538
  "Request body is required",
1539
+ "body",
1540
+ error,
1170
1541
  ),
1171
1542
  owner: "framework",
1172
1543
  };
1173
1544
  } else {
1174
- result =
1175
- error instanceof SchemaValidationError
1176
- ? {
1177
- response: errorResponse(
1178
- 422,
1179
- "VALIDATION_ERROR",
1180
- "Invalid request body",
1181
- { issues: error.issues },
1182
- ),
1183
- owner: "framework",
1184
- }
1185
- : {
1186
- response: errorResponse(
1187
- 422,
1188
- "VALIDATION_ERROR",
1189
- "Invalid request body",
1190
- (error as Error).message,
1191
- ),
1192
- owner: "framework",
1193
- };
1545
+ result = {
1546
+ response: requestValidationError(
1547
+ contract,
1548
+ 422,
1549
+ "VALIDATION_ERROR",
1550
+ "Invalid request body",
1551
+ "body",
1552
+ error,
1553
+ ),
1554
+ owner: "framework",
1555
+ };
1194
1556
  }
1195
1557
  }
1196
1558
  }
@@ -1203,11 +1565,10 @@ function buildHandler<
1203
1565
 
1204
1566
  let createdCtx!: Ctx;
1205
1567
  try {
1206
- createdCtx = await options.createContext({
1568
+ createdCtx = await contextRuntime.createRequestContext(
1207
1569
  req,
1208
- ports: finalPorts,
1209
1570
  contract,
1210
- });
1571
+ );
1211
1572
  baseCtx = createdCtx;
1212
1573
  } catch (error) {
1213
1574
  result = await resolveErrorResult(
@@ -1262,7 +1623,7 @@ function buildHandler<
1262
1623
  break;
1263
1624
  }
1264
1625
  if (hookResult?.ctx !== undefined) {
1265
- currentCtx = hookResult.ctx;
1626
+ currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
1266
1627
  }
1267
1628
  if (hookResult?.response) {
1268
1629
  const response = normalizeHttpResponse(hookResult.response);
@@ -1300,10 +1661,10 @@ function buildHandler<
1300
1661
  body,
1301
1662
  });
1302
1663
  if (additions && typeof additions === "object") {
1303
- currentCtx = {
1664
+ currentCtx = contextRuntime.finalizeContext({
1304
1665
  ...(currentCtx as object),
1305
1666
  ...additions,
1306
- } as Ctx;
1667
+ } as Ctx);
1307
1668
  }
1308
1669
  } catch (error) {
1309
1670
  result = await resolveErrorResult(
@@ -1321,6 +1682,14 @@ function buildHandler<
1321
1682
  }
1322
1683
 
1323
1684
  if (!result) {
1685
+ // Hooks may have elevated the actor or resolved a tenant.
1686
+ // Refresh the ambient request context so record-time
1687
+ // consumers such as createAmbientAuditLog see the finalized
1688
+ // identity.
1689
+ setActiveRequestIdentity({
1690
+ actor: readContextActor(currentCtx),
1691
+ tenant: readContextTenant(currentCtx),
1692
+ });
1324
1693
  try {
1325
1694
  result = {
1326
1695
  ctx: currentCtx,
@@ -1349,9 +1718,17 @@ function buildHandler<
1349
1718
  let finalResponse = normalizeHttpResponse(result.response);
1350
1719
  let finalError = result.error;
1351
1720
  let finalOwner = responseOwnerFor(finalResponse, result.owner);
1352
- if (finalOwner === "route" && !isWebResponse(finalResponse)) {
1721
+ if (
1722
+ finalOwner === "route" &&
1723
+ !isWebResponse(finalResponse) &&
1724
+ (options.validateResponses ?? true)
1725
+ ) {
1353
1726
  try {
1354
- finalResponse = await finalizeResponse(contract, finalResponse);
1727
+ finalResponse = await finalizeResponse(
1728
+ contract,
1729
+ finalResponse,
1730
+ target.responseValidationExemptStatus,
1731
+ );
1355
1732
  } catch (error) {
1356
1733
  if (error instanceof ResponseContractViolationError) {
1357
1734
  result = await applyTransformHooks(
@@ -1424,6 +1801,55 @@ function buildHandler<
1424
1801
  };
1425
1802
  }
1426
1803
 
1804
+ function buildHandler<
1805
+ Ctx,
1806
+ Ports extends AnyPorts,
1807
+ C extends HttpContractConfig,
1808
+ Providers extends readonly AnyServiceProvider[] = readonly [],
1809
+ FinalPorts extends Ports & InferProviderPorts<Providers> = Ports &
1810
+ InferProviderPorts<Providers>,
1811
+ >(
1812
+ // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
1813
+ options: CreateServerOptions<Ctx, Ports, any, any, Providers>,
1814
+ finalPorts: FinalPorts,
1815
+ contextRuntime: {
1816
+ createRequestContext: (
1817
+ req: HttpRequestLike,
1818
+ contract: HttpContractConfig,
1819
+ ) => Promise<Ctx>;
1820
+ finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
1821
+ },
1822
+ contract: C,
1823
+ userHandler: Handler<Ctx, C>,
1824
+ hooks: ServerHook<Ctx, FinalPorts>[],
1825
+ routeHooks: readonly RouteHook<unknown, object>[] = [],
1826
+ optionsOverrides?: {
1827
+ skipRoutePreparation?: boolean;
1828
+ },
1829
+ responseValidationExemptStatus?: number,
1830
+ ): (
1831
+ req: HttpRequestLike,
1832
+ preMatchedParams?: Record<string, string>,
1833
+ ) => Promise<HttpResponse> {
1834
+ const execute = createRequestExecutor<Ctx, Ports, C, Providers, FinalPorts>(
1835
+ options,
1836
+ finalPorts,
1837
+ contextRuntime,
1838
+ hooks,
1839
+ routeHooks,
1840
+ optionsOverrides,
1841
+ );
1842
+ const executionTarget: ExecutionTarget<Ctx, C> = {
1843
+ contract,
1844
+ compiled: compilePath(contract.path),
1845
+ handler: userHandler,
1846
+ responseValidationExemptStatus,
1847
+ };
1848
+
1849
+ return (req, preMatchedParams) =>
1850
+ execute(executionTarget, req, preMatchedParams);
1851
+ }
1852
+
1427
1853
  /**
1428
1854
  * Create a Beignet server instance.
1429
1855
  *
@@ -1432,42 +1858,128 @@ function buildHandler<
1432
1858
  * Use adapter packages such as `@beignet/next` to expose `server.api` to a
1433
1859
  * specific runtime.
1434
1860
  *
1435
- * @param options - Ports, providers, routes, hooks, context factory, and error
1436
- * mapping hooks for the server.
1861
+ * @param options - Ports, providers, routes, hooks, context blueprint, and
1862
+ * error mapping hooks for the server.
1437
1863
  * @returns A started server instance with final ports and a catch-all handler.
1438
1864
  */
1439
1865
  export async function createServer<
1440
1866
  Ctx,
1441
1867
  Ports extends AnyPorts,
1868
+ ServiceInput = void,
1442
1869
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
1443
1870
  Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
1444
- Providers extends readonly ServiceProvider<
1445
- unknown,
1446
- // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
1447
- StandardSchemaV1<any, any>,
1448
- AnyPorts
1449
- >[] = readonly [],
1871
+ Providers extends readonly AnyServiceProvider[] = readonly [],
1450
1872
  >(
1451
- options: CreateServerOptions<Ctx, Ports, Routes, Providers>,
1452
- ): Promise<ServerInstance<Ctx, Ports & ProvidedPortsOfList<Providers>>> {
1873
+ options: CreateServerOptions<Ctx, Ports, ServiceInput, Routes, Providers>,
1874
+ ): Promise<
1875
+ ServerInstance<Ctx, Ports & InferProviderPorts<Providers>, ServiceInput>
1876
+ > {
1453
1877
  type RegisteredRoute = ResolvedRoute<Ctx, HttpContractConfig> & {
1454
1878
  compiled: CompiledPath;
1879
+ /**
1880
+ * Uppercased contract method, cached for dispatch.
1881
+ */
1882
+ method: string;
1455
1883
  };
1456
1884
  const registry: RegisteredRoute[] = [];
1457
- type FinalPorts = Ports & ProvidedPortsOfList<Providers>;
1458
- const providers = (options.providers ?? []) as readonly ServiceProvider<
1459
- unknown,
1460
- // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
1461
- StandardSchemaV1<any, any>,
1462
- AnyPorts
1463
- >[];
1885
+ // Routes can register after startup via server.route(...), so the registry
1886
+ // is re-sorted lazily before the next dispatch instead of on every
1887
+ // registration.
1888
+ let registryNeedsSort = false;
1889
+ type FinalPorts = Ports & InferProviderPorts<Providers>;
1890
+ const providers = (options.providers ?? []) as readonly AnyServiceProvider[];
1464
1891
  const env = options.providerEnv ?? process.env;
1465
1892
  const overrides = options.providerConfig ?? {};
1466
1893
  const providerResults: ProviderSetupResult<AnyPorts>[] = [];
1467
1894
  const finalPorts = { ...options.ports } as FinalPorts;
1468
- const hooks = [...((options.hooks ?? []) as ServerHook<Ctx, FinalPorts>[])];
1895
+ const instrumentation = createServerInstrumentation<Ctx>(
1896
+ options.instrumentation,
1897
+ );
1898
+ const hooks = [
1899
+ ...(instrumentation.hook
1900
+ ? [instrumentation.hook as ServerHook<Ctx, FinalPorts>]
1901
+ : []),
1902
+ ...((options.hooks ?? []) as ServerHook<Ctx, FinalPorts>[]),
1903
+ ];
1469
1904
  const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
1470
1905
 
1906
+ const resolvedContext = resolveServerContext<Ctx, FinalPorts, ServiceInput>(
1907
+ options.context,
1908
+ );
1909
+ const finalizeContext = createContextFinalizer(
1910
+ resolvedContext,
1911
+ () => finalPorts,
1912
+ );
1913
+ const createRequestContext = async (
1914
+ req: HttpRequestLike,
1915
+ contract?: HttpContractConfig,
1916
+ ): Promise<Ctx> => {
1917
+ const { requestId, trace } = instrumentation.prepareRequest(req);
1918
+ return finalizeContext(
1919
+ await resolvedContext.request({
1920
+ req,
1921
+ ports: finalPorts,
1922
+ contract,
1923
+ requestId,
1924
+ trace,
1925
+ }),
1926
+ );
1927
+ };
1928
+ const createServiceContext = async (
1929
+ ...args: ServiceContextInputArgs<ServiceInput>
1930
+ ): Promise<Ctx> => {
1931
+ const serviceFactory = resolvedContext.service;
1932
+ if (!serviceFactory) {
1933
+ throw new Error(
1934
+ "Define context.service in createServer(...) to create service contexts.",
1935
+ );
1936
+ }
1937
+
1938
+ const { requestId, trace } = instrumentation.createServiceCorrelation();
1939
+ // Enter the ambient context synchronously (before the factory awaits) so
1940
+ // it propagates to the caller's continuation. Identity fields are filled
1941
+ // onto the same object once the context is finalized, so jobs, listeners,
1942
+ // schedules, and tasks observe the service actor/tenant at record time.
1943
+ const ambient: ActiveRequestContext = {
1944
+ requestId,
1945
+ traceId: trace.traceId,
1946
+ spanId: trace.spanId,
1947
+ parentSpanId: trace.parentSpanId,
1948
+ traceparent: trace.traceparent,
1949
+ };
1950
+ enterActiveRequestContext(ambient);
1951
+ const ctx = finalizeContext(
1952
+ await serviceFactory({
1953
+ ports: finalPorts,
1954
+ input: args[0] as ServiceInput,
1955
+ requestId,
1956
+ trace,
1957
+ }),
1958
+ );
1959
+ ambient.actor = readContextActor(ctx);
1960
+ ambient.tenant = readContextTenant(ctx);
1961
+ return ctx;
1962
+ };
1963
+ const contextRuntime = {
1964
+ createRequestContext,
1965
+ finalizeContext,
1966
+ };
1967
+
1968
+ let serviceContextsAvailable = false;
1969
+ const lifecycleCreateServiceContext = async (
1970
+ input?: unknown,
1971
+ ): Promise<unknown> => {
1972
+ if (!serviceContextsAvailable) {
1973
+ throw new Error(
1974
+ "Service contexts are unavailable until providers have started.",
1975
+ );
1976
+ }
1977
+
1978
+ return createServiceContext(
1979
+ ...([input] as ServiceContextInputArgs<ServiceInput>),
1980
+ );
1981
+ };
1982
+
1471
1983
  let stopped = false;
1472
1984
  const stop = async () => {
1473
1985
  if (stopped) return;
@@ -1478,6 +1990,7 @@ export async function createServer<
1478
1990
  try {
1479
1991
  await result?.stop?.({
1480
1992
  ports: finalPorts,
1993
+ createServiceContext: lifecycleCreateServiceContext,
1481
1994
  });
1482
1995
  } catch (err) {
1483
1996
  errors.push(err);
@@ -1490,11 +2003,13 @@ export async function createServer<
1490
2003
 
1491
2004
  const registeredPaths = new Set<string>();
1492
2005
  const registeredShapes = new Map<string, string>();
2006
+ const registeredNames = new Map<string, string>();
1493
2007
 
1494
2008
  const registerRoute = <C extends HttpContractConfig>(
1495
2009
  contract: C,
1496
2010
  handler: Handler<Ctx, C>,
1497
2011
  routeHooks: readonly RouteHook<unknown, object>[] = [],
2012
+ responseValidationExemptStatus?: number,
1498
2013
  ): void => {
1499
2014
  if (contract.body && !methodSupportsRequestBody(contract.method)) {
1500
2015
  throw new Error(
@@ -1516,20 +2031,46 @@ export async function createServer<
1516
2031
  `Ambiguous route: ${routeKey} conflicts with ${conflictingRoute}. Dynamic parameter names are ignored during routing, so each method + path shape must be unique.`,
1517
2032
  );
1518
2033
  }
2034
+ const conflictingName = registeredNames.get(contract.name);
2035
+ if (conflictingName) {
2036
+ throw new Error(
2037
+ `Duplicate contract name: "${contract.name}" is registered for both ${conflictingName} and ${routeKey}. Contract names must be unique because typed clients, OpenAPI operations, and devtools key on them.`,
2038
+ );
2039
+ }
2040
+ if (contract.pathParams) {
2041
+ const shape = getObjectSchemaShape(contract.pathParams);
2042
+ if (shape) {
2043
+ const { missingKeys, extraKeys } = comparePathParamsToTemplate({
2044
+ pathKeys: compiled.keys,
2045
+ shapeKeys: Object.keys(shape),
2046
+ });
2047
+ if (missingKeys.length > 0 || extraKeys.length > 0) {
2048
+ const details = formatPathParamsMismatch({ missingKeys, extraKeys });
2049
+ throw new Error(
2050
+ `Path parameters for contract "${contract.name}" must match "${contract.path}" (${details}). Path templates and pathParams schemas drive routing, clients, and OpenAPI together.`,
2051
+ );
2052
+ }
2053
+ }
2054
+ }
1519
2055
  registeredPaths.add(routeKey);
1520
2056
  registeredShapes.set(shapeRouteKey, routeKey);
2057
+ registeredNames.set(contract.name, routeKey);
1521
2058
 
1522
2059
  const builtHandler = buildHandler(
1523
2060
  options,
1524
2061
  finalPorts,
2062
+ contextRuntime,
1525
2063
  contract,
1526
2064
  handler,
1527
2065
  hooks,
1528
2066
  routeHooks,
2067
+ undefined,
2068
+ responseValidationExemptStatus,
1529
2069
  );
1530
2070
  registry.push({
1531
2071
  contract,
1532
2072
  compiled,
2073
+ method: contract.method.toUpperCase(),
1533
2074
  handler: builtHandler,
1534
2075
  match: (method, pathname) => {
1535
2076
  if (contract.method.toUpperCase() !== method.toUpperCase()) {
@@ -1540,7 +2081,7 @@ export async function createServer<
1540
2081
  return { matched: true as const };
1541
2082
  },
1542
2083
  });
1543
- registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
2084
+ registryNeedsSort = true;
1544
2085
  };
1545
2086
 
1546
2087
  const createBuilder = <C extends HttpContractConfig>(
@@ -1548,7 +2089,14 @@ export async function createServer<
1548
2089
  shouldRegister: boolean,
1549
2090
  ): RouteBuilder<Ctx, C> => ({
1550
2091
  handle: (fn) => {
1551
- const wrapped = buildHandler(options, finalPorts, contract, fn, hooks);
2092
+ const wrapped = buildHandler(
2093
+ options,
2094
+ finalPorts,
2095
+ contextRuntime,
2096
+ contract,
2097
+ fn,
2098
+ hooks,
2099
+ );
1552
2100
  if (shouldRegister) registerRoute(contract, fn);
1553
2101
  return wrapped;
1554
2102
  },
@@ -1558,11 +2106,36 @@ export async function createServer<
1558
2106
  try {
1559
2107
  for (const route of options.routes) {
1560
2108
  const contract = resolveContract(route.contract);
1561
- registerRoute(
1562
- contract,
1563
- route.handle as Handler<Ctx, typeof contract>,
1564
- route.hooks as readonly RouteHook<unknown, object>[] | undefined,
1565
- );
2109
+ const hasHandle = typeof route.handle === "function";
2110
+ const hasUseCase = isUseCaseRouteDef(route);
2111
+
2112
+ if (hasHandle && hasUseCase) {
2113
+ throw new Error(
2114
+ `Route for contract "${contract.name}" declares both "handle" and "useCase". Bind the contract to exactly one of them.`,
2115
+ );
2116
+ }
2117
+ if (!hasHandle && !hasUseCase) {
2118
+ throw new Error(
2119
+ `Route for contract "${contract.name}" declares neither "handle" nor "useCase". Bind the contract to a use case or implement a handler.`,
2120
+ );
2121
+ }
2122
+
2123
+ if (isUseCaseRouteDef(route)) {
2124
+ const { handler, responseValidationExemptStatus } =
2125
+ createUseCaseRouteHandler<Ctx, typeof contract>(contract, route);
2126
+ registerRoute(
2127
+ contract,
2128
+ handler,
2129
+ route.hooks as readonly RouteHook<unknown, object>[] | undefined,
2130
+ responseValidationExemptStatus,
2131
+ );
2132
+ } else {
2133
+ registerRoute(
2134
+ contract,
2135
+ route.handle as Handler<Ctx, typeof contract>,
2136
+ route.hooks as readonly RouteHook<unknown, object>[] | undefined,
2137
+ );
2138
+ }
1566
2139
  }
1567
2140
  } catch (error) {
1568
2141
  try {
@@ -1583,6 +2156,7 @@ export async function createServer<
1583
2156
  const result = await provider.setup({
1584
2157
  ports: finalPorts,
1585
2158
  config: cfg,
2159
+ createServiceContext: lifecycleCreateServiceContext,
1586
2160
  });
1587
2161
  if (result.ports) {
1588
2162
  Object.assign(finalPorts, result.ports);
@@ -1594,8 +2168,31 @@ export async function createServer<
1594
2168
  if (!result.start) continue;
1595
2169
  await result.start({
1596
2170
  ports: finalPorts,
2171
+ createServiceContext: lifecycleCreateServiceContext,
1597
2172
  });
1598
2173
  }
2174
+
2175
+ instrumentation.attachPorts(finalPorts);
2176
+
2177
+ const onUnboundPorts = options.onUnboundPorts ?? "error";
2178
+ if (onUnboundPorts !== "ignore") {
2179
+ const unboundKeys = Object.keys(finalPorts).filter((key) =>
2180
+ isUnboundPort((finalPorts as AnyPorts)[key]),
2181
+ );
2182
+ if (unboundKeys.length > 0) {
2183
+ const message =
2184
+ `Unbound ports after provider startup: ${unboundKeys.join(", ")}. ` +
2185
+ "Each port declared as deferred in definePorts(...) must be contributed " +
2186
+ "by a provider (server/providers.ts) or bound in infra/app-ports.ts. " +
2187
+ 'Pass onUnboundPorts: "warn" or "ignore" to change this behavior.';
2188
+ if (onUnboundPorts === "error") {
2189
+ throw new Error(message);
2190
+ }
2191
+ console.warn(`[beignet] ${message}`);
2192
+ }
2193
+ }
2194
+
2195
+ serviceContextsAvailable = true;
1599
2196
  } catch (error) {
1600
2197
  try {
1601
2198
  await stop();
@@ -1608,39 +2205,88 @@ export async function createServer<
1608
2205
  throw error;
1609
2206
  }
1610
2207
 
2208
+ // The fallback 404/405 pipeline is built once at server creation. Only the
2209
+ // contract surface that hooks observe (method, path, and the Allow set for
2210
+ // 405s) is assembled per unmatched request.
2211
+ const executeFallback = createRequestExecutor<
2212
+ Ctx,
2213
+ Ports,
2214
+ HttpContractConfig,
2215
+ Providers
2216
+ >(options, finalPorts, contextRuntime, hooks, [], {
2217
+ skipRoutePreparation: true,
2218
+ });
2219
+
2220
+ const fallbackContract = (
2221
+ name: string,
2222
+ method: string,
2223
+ path: string,
2224
+ ): HttpContractConfig => ({
2225
+ kind: "http",
2226
+ name,
2227
+ method: method as HttpContractConfig["method"],
2228
+ path,
2229
+ pathParams: null,
2230
+ query: null,
2231
+ body: null,
2232
+ responses: {},
2233
+ metadata: {},
2234
+ });
2235
+
2236
+ const notFoundHandler: Handler<Ctx, HttpContractConfig> = async () =>
2237
+ errorResponse(404, "NOT_FOUND", "Not found");
2238
+
1611
2239
  const api = async (req: HttpRequestLike) => {
2240
+ if (registryNeedsSort) {
2241
+ registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
2242
+ registryNeedsSort = false;
2243
+ }
2244
+
1612
2245
  const url = new URL(req.url);
2246
+ const method = req.method.toUpperCase();
1613
2247
 
2248
+ let pathMatchedMethods: Set<string> | undefined;
1614
2249
  for (const entry of registry) {
1615
- const result = entry.match(req.method, url.pathname);
1616
- if (result.matched) {
2250
+ if (!entry.compiled.pattern.test(url.pathname)) continue;
2251
+ if (entry.method === method) {
1617
2252
  return await entry.handler(req);
1618
2253
  }
2254
+ if (!pathMatchedMethods) {
2255
+ pathMatchedMethods = new Set();
2256
+ }
2257
+ pathMatchedMethods.add(entry.method);
1619
2258
  }
1620
2259
 
1621
- const notFoundContract: HttpContractConfig = {
1622
- kind: "http",
1623
- name: "notFound",
1624
- method: req.method.toUpperCase() as HttpContractConfig["method"],
1625
- path: url.pathname || "/",
1626
- pathParams: null,
1627
- query: null,
1628
- body: null,
1629
- responses: {},
1630
- metadata: {},
1631
- };
2260
+ const pathname = url.pathname || "/";
1632
2261
 
1633
- const notFoundHandler = buildHandler(
1634
- options,
1635
- finalPorts,
1636
- notFoundContract,
1637
- async () => errorResponse(404, "NOT_FOUND", "Not found"),
1638
- hooks,
1639
- [],
1640
- { skipRoutePreparation: true },
1641
- );
2262
+ if (pathMatchedMethods) {
2263
+ const allow = [...pathMatchedMethods].sort().join(", ");
2264
+
2265
+ return await executeFallback(
2266
+ {
2267
+ contract: fallbackContract("methodNotAllowed", method, pathname),
2268
+ handler: async () => ({
2269
+ status: 405,
2270
+ headers: { allow },
2271
+ body: createErrorResponseBody({
2272
+ code: "METHOD_NOT_ALLOWED",
2273
+ message: `Method ${method} is not allowed for ${pathname}`,
2274
+ }),
2275
+ }),
2276
+ },
2277
+ req,
2278
+ {},
2279
+ );
2280
+ }
1642
2281
 
1643
- return await notFoundHandler(req);
2282
+ return await executeFallback(
2283
+ {
2284
+ contract: fallbackContract("notFound", method, pathname),
2285
+ handler: notFoundHandler,
2286
+ },
2287
+ req,
2288
+ {},
2289
+ );
1644
2290
  };
1645
2291
 
1646
2292
  return {
@@ -1649,6 +2295,8 @@ export async function createServer<
1649
2295
  const contract = resolveContract(contractLike);
1650
2296
  return createBuilder(contract, true);
1651
2297
  },
2298
+ createRequestContext: (req) => createRequestContext(req),
2299
+ createServiceContext,
1652
2300
  contracts,
1653
2301
  stop,
1654
2302
  ports: finalPorts,
@@ -1664,7 +2312,7 @@ export async function createServer<
1664
2312
  * @example
1665
2313
  * ```ts
1666
2314
  * const routes = defineRoutes<AppContext>([
1667
- * { contract: listPosts, handle: async ({ ctx }) => ctx.posts.list() },
2315
+ * { contract: listPosts, useCase: listPostsUseCase },
1668
2316
  * ]);
1669
2317
  * ```
1670
2318
  */
@@ -1672,11 +2320,11 @@ export function defineRoutes<
1672
2320
  Ctx,
1673
2321
  const R extends
1674
2322
  readonly ContextualRouteInput<Ctx>[] = readonly ContextualRouteInput<Ctx>[],
1675
- >(routes: R): FlattenRouteInputs<R>;
2323
+ >(routes: R & ValidatedRouteInputs<Ctx, R>): FlattenRouteInputs<R>;
1676
2324
  export function defineRoutes<
1677
2325
  Ctx,
1678
2326
  const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
1679
- >(routes: R): FlattenRouteInputs<R>;
2327
+ >(routes: R & ValidatedRouteInputs<Ctx, R>): FlattenRouteInputs<R>;
1680
2328
  export function defineRoutes<
1681
2329
  Ctx,
1682
2330
  const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
@@ -1727,7 +2375,7 @@ export function contractsFromRoutes<
1727
2375
  * name: "todos",
1728
2376
  * hooks: [auth.optional()],
1729
2377
  * routes: [
1730
- * { contract: listTodos, handle: async ({ ctx }) => ctx.todos.list() },
2378
+ * { contract: listTodos, useCase: listTodosUseCase },
1731
2379
  * ]
1732
2380
  * });
1733
2381
  * ```
@@ -1740,7 +2388,7 @@ export function defineRouteGroup<
1740
2388
  >(group: {
1741
2389
  name: string;
1742
2390
  hooks?: readonly RouteHook<Ctx, object>[];
1743
- routes: R;
2391
+ routes: R & ValidatedRouteInputs<Ctx, R>;
1744
2392
  }): RouteGroup<Ctx, R>;
1745
2393
  export function defineRouteGroup<
1746
2394
  Ctx,