@beignet/core 0.0.2 → 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 +173 -0
  2. package/README.md +821 -30
  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 +148 -16
  100. package/dist/jobs/index.d.ts.map +1 -1
  101. package/dist/jobs/index.js +174 -14
  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 +18 -4
  116. package/dist/outbox/index.d.ts.map +1 -1
  117. package/dist/outbox/index.js +104 -4
  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 +46 -5
  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 +89 -65
  184. package/dist/server/hooks/auth.d.ts.map +1 -1
  185. package/dist/server/hooks/auth.js +84 -55
  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 +84 -6
  214. package/dist/server/http.d.ts.map +1 -1
  215. package/dist/server/index.d.ts +36 -12
  216. package/dist/server/index.d.ts.map +1 -1
  217. package/dist/server/index.js +24 -8
  218. package/dist/server/index.js.map +1 -1
  219. package/dist/server/instrumentation.d.ts +108 -0
  220. package/dist/server/instrumentation.d.ts.map +1 -0
  221. package/dist/server/instrumentation.js +297 -0
  222. package/dist/server/instrumentation.js.map +1 -0
  223. package/dist/server/openapi.d.ts +3 -3
  224. package/dist/server/openapi.d.ts.map +1 -1
  225. package/dist/server/openapi.js +1 -1
  226. package/dist/server/openapi.js.map +1 -1
  227. package/dist/server/providers/index.d.ts +3 -3
  228. package/dist/server/providers/index.d.ts.map +1 -1
  229. package/dist/server/providers/index.js +3 -3
  230. package/dist/server/providers/index.js.map +1 -1
  231. package/dist/server/providers/loadProviderConfig.d.ts +2 -2
  232. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  233. package/dist/server/providers/loadProviderConfig.js +2 -2
  234. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  235. package/dist/server/request-context.d.ts +67 -0
  236. package/dist/server/request-context.d.ts.map +1 -0
  237. package/dist/server/request-context.js +79 -0
  238. package/dist/server/request-context.js.map +1 -0
  239. package/dist/server/server-context.d.ts +38 -0
  240. package/dist/server/server-context.d.ts.map +1 -0
  241. package/dist/server/server-context.js +38 -0
  242. package/dist/server/server-context.js.map +1 -0
  243. package/dist/server/server.d.ts +148 -35
  244. package/dist/server/server.d.ts.map +1 -1
  245. package/dist/server/server.js +482 -145
  246. package/dist/server/server.js.map +1 -1
  247. package/dist/server/types.d.ts +2 -2
  248. package/dist/server/types.d.ts.map +1 -1
  249. package/dist/server/types.js +2 -2
  250. package/dist/server/types.js.map +1 -1
  251. package/dist/server/use-case-route.d.ts +263 -0
  252. package/dist/server/use-case-route.d.ts.map +1 -0
  253. package/dist/server/use-case-route.js +77 -0
  254. package/dist/server/use-case-route.js.map +1 -0
  255. package/dist/server-only.d.ts +8 -0
  256. package/dist/server-only.d.ts.map +1 -0
  257. package/dist/server-only.js +8 -0
  258. package/dist/server-only.js.map +1 -0
  259. package/dist/tasks/index.d.ts +139 -0
  260. package/dist/tasks/index.d.ts.map +1 -0
  261. package/dist/tasks/index.js +98 -0
  262. package/dist/tasks/index.js.map +1 -0
  263. package/dist/testing/index.d.ts +611 -5
  264. package/dist/testing/index.d.ts.map +1 -1
  265. package/dist/testing/index.js +434 -4
  266. package/dist/testing/index.js.map +1 -1
  267. package/dist/tracing/index.d.ts +89 -0
  268. package/dist/tracing/index.d.ts.map +1 -0
  269. package/dist/tracing/index.js +101 -0
  270. package/dist/tracing/index.js.map +1 -0
  271. package/dist/uploads/client.d.ts +278 -0
  272. package/dist/uploads/client.d.ts.map +1 -0
  273. package/dist/uploads/client.js +428 -0
  274. package/dist/uploads/client.js.map +1 -0
  275. package/dist/uploads/index.d.ts +361 -0
  276. package/dist/uploads/index.d.ts.map +1 -0
  277. package/dist/uploads/index.js +543 -0
  278. package/dist/uploads/index.js.map +1 -0
  279. package/package.json +34 -3
  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 +340 -29
  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 +151 -6
  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 +93 -8
  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 +175 -158
  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 +15 -12
  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 +112 -6
  337. package/src/server/index.ts +63 -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 +1045 -229
  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 +1153 -6
  350. package/src/tracing/index.ts +176 -0
  351. package/src/uploads/client.ts +861 -0
  352. package/src/uploads/index.ts +1071 -0
  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
@@ -1,10 +1,29 @@
1
- import { BEIGNET_ERROR_OWNER_HEADER, getContractHeaderSchemas, methodSupportsRequestBody, parsePathTemplate, } from "../contracts";
2
- import { createErrorResponseBody, isAppError, isErrorResponseBody, toErrorResponseBody, } from "../errors";
3
- import { AuthUnauthorizedError, GateAuthorizationError } from "../ports";
4
- import { resolveContract } from "./contract-like";
5
- import { getRequestIdFromContext } from "./hooks/utils";
6
- import { loadProviderConfig, parseStandardSchema, SchemaValidationError, } from "./providers";
1
+ import { BEIGNET_ERROR_OWNER_HEADER, getContractHeaderSchemas, methodSupportsRequestBody, parsePathTemplate, } from "../contracts/index.js";
2
+ import { comparePathParamsToTemplate, formatPathParamsMismatch, getObjectSchemaShape, } from "../contracts/schema-shape.js";
3
+ import { createErrorResponseBody, httpErrors, isAppError, isErrorResponseBody, toErrorResponseBody, } from "../errors/index.js";
4
+ import { IdempotencyConflictError, IdempotencyInProgressError, } from "../idempotency/index.js";
5
+ import { AuthUnauthorizedError, GateAuthorizationError, isUnboundPort, TenantRequiredError, } from "../ports/index.js";
6
+ import { createContextFinalizer, resolveServerContext } from "./context.js";
7
+ import { resolveContract } from "./contract-like.js";
8
+ import { getRequestIdFromContext } from "./hooks/utils.js";
9
+ import { createServerInstrumentation } from "./instrumentation.js";
10
+ import { loadProviderConfig, parseStandardSchema, SchemaValidationError, } from "./providers/index.js";
11
+ import { enterActiveRequestContext, readContextActor, readContextTenant, setActiveRequestIdentity, } from "./request-context.js";
12
+ import { createUseCaseRouteHandler, isUseCaseRouteDef, } from "./use-case-route.js";
7
13
  const ROUTE_GROUP_KIND = "beignet.route-group";
14
+ /**
15
+ * Define one route registration with hook-aware handler typing.
16
+ *
17
+ * Direct route objects are still supported. Use this helper when route-scoped
18
+ * hooks enrich `ctx` for a single handler and you want TypeScript to infer the
19
+ * added fields.
20
+ */
21
+ export function defineRoute() {
22
+ function define(route) {
23
+ return route;
24
+ }
25
+ return define;
26
+ }
8
27
  class PathDecodeError extends Error {
9
28
  constructor() {
10
29
  super("Malformed URL path");
@@ -55,6 +74,35 @@ function errorResponse(status, code, message, details) {
55
74
  body: createErrorResponseBody({ code, message, details }),
56
75
  };
57
76
  }
77
+ function contractDiagnostics(contract) {
78
+ return {
79
+ contract: contract.name,
80
+ method: contract.method,
81
+ path: contract.path,
82
+ };
83
+ }
84
+ function requestValidationDetails(contract, location, error) {
85
+ const details = {
86
+ ...contractDiagnostics(contract),
87
+ location,
88
+ };
89
+ if (error instanceof SchemaValidationError) {
90
+ return {
91
+ ...details,
92
+ issues: error.issues,
93
+ };
94
+ }
95
+ if (error instanceof Error) {
96
+ return {
97
+ ...details,
98
+ message: error.message,
99
+ };
100
+ }
101
+ return details;
102
+ }
103
+ function requestValidationError(contract, status, code, message, location, error) {
104
+ return errorResponse(status, code, message, requestValidationDetails(contract, location, error));
105
+ }
58
106
  function normalizeResponse(res) {
59
107
  return {
60
108
  status: res.status,
@@ -126,6 +174,42 @@ function responseForHooks(res) {
126
174
  headers: headersToRecord(res.headers),
127
175
  };
128
176
  }
177
+ /**
178
+ * Merge hook-applied header changes onto a native web Response.
179
+ *
180
+ * Starts from the native response's `Headers` so `set-cookie` multiplicity is
181
+ * preserved, then applies headers the beforeSend chain added or changed
182
+ * relative to the original headers-only view. The body stream passes through
183
+ * untouched; status and statusText are preserved.
184
+ */
185
+ function mergeNativeResponseHeaders(nativeResponse, originalHeaders, finalHeaders) {
186
+ const originalByLowerKey = new Map();
187
+ for (const [key, value] of Object.entries(originalHeaders)) {
188
+ originalByLowerKey.set(key.toLowerCase(), value);
189
+ }
190
+ let changed = false;
191
+ const merged = new Headers(nativeResponse.headers);
192
+ for (const [key, value] of Object.entries(finalHeaders)) {
193
+ const lowerKey = key.toLowerCase();
194
+ if (originalByLowerKey.get(lowerKey) === value)
195
+ continue;
196
+ changed = true;
197
+ if (lowerKey === "set-cookie") {
198
+ merged.append(lowerKey, value);
199
+ }
200
+ else {
201
+ merged.set(key, value);
202
+ }
203
+ }
204
+ if (!changed) {
205
+ return nativeResponse;
206
+ }
207
+ return new Response(nativeResponse.body, {
208
+ status: nativeResponse.status,
209
+ statusText: nativeResponse.statusText,
210
+ headers: merged,
211
+ });
212
+ }
129
213
  function isHttpResponseLike(value) {
130
214
  return (!isWebResponse(value) &&
131
215
  typeof value === "object" &&
@@ -143,6 +227,25 @@ class ResponseContractViolationError extends Error {
143
227
  this.details = args.details;
144
228
  }
145
229
  }
230
+ function responseContractViolationMessage(contract, status) {
231
+ return (`Response validation failed for ${contract.method} ${contract.path} ` +
232
+ `(status ${status}, contract: ${contract.name})`);
233
+ }
234
+ function declaredResponseStatuses(contract) {
235
+ return Object.keys(contract.responses)
236
+ .map((status) => Number(status))
237
+ .filter((status) => Number.isFinite(status))
238
+ .sort((a, b) => a - b);
239
+ }
240
+ function responseContractViolationDetails(contract, status, details) {
241
+ return {
242
+ ...contractDiagnostics(contract),
243
+ location: "response",
244
+ status,
245
+ declaredStatuses: declaredResponseStatuses(contract),
246
+ ...details,
247
+ };
248
+ }
146
249
  function getDeclaredCatalogErrorsForStatus(contract, status) {
147
250
  const errors = contract.metadata?.errors;
148
251
  if (typeof errors !== "object" || errors === null)
@@ -165,16 +268,15 @@ async function validateCatalogErrorResponse(contract, res) {
165
268
  if (!matchingError) {
166
269
  throw new ResponseContractViolationError({
167
270
  code: "RESPONSE_VALIDATION_ERROR",
168
- message: `Response validation failed for ${contract.method} ${contract.path} ` +
169
- `(status ${res.status}, contract: ${contract.name})`,
170
- details: {
271
+ message: responseContractViolationMessage(contract, res.status),
272
+ details: responseContractViolationDetails(contract, res.status, {
171
273
  issues: [
172
274
  {
173
275
  message: `Error response code "${body.code}" is not declared for status ${res.status}. ` +
174
276
  `Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
175
277
  },
176
278
  ],
177
- },
279
+ }),
178
280
  });
179
281
  }
180
282
  if (matchingError.details && body.details !== undefined) {
@@ -185,16 +287,17 @@ async function validateCatalogErrorResponse(contract, res) {
185
287
  if (error instanceof SchemaValidationError) {
186
288
  throw new ResponseContractViolationError({
187
289
  code: "RESPONSE_VALIDATION_ERROR",
188
- message: `Response validation failed for ${contract.method} ${contract.path} ` +
189
- `(status ${res.status}, contract: ${contract.name})`,
190
- details: { issues: error.issues },
290
+ message: responseContractViolationMessage(contract, res.status),
291
+ details: responseContractViolationDetails(contract, res.status, {
292
+ issues: error.issues,
293
+ }),
191
294
  });
192
295
  }
193
296
  throw error;
194
297
  }
195
298
  }
196
299
  }
197
- async function validateResponseAgainstContract(contract, res) {
300
+ async function validateResponseAgainstContract(contract, res, responseValidationExemptStatus) {
198
301
  const statusKey = String(res.status);
199
302
  const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
200
303
  if (!hasDeclaredStatus) {
@@ -204,10 +307,9 @@ async function validateResponseAgainstContract(contract, res) {
204
307
  code: "UNDECLARED_RESPONSE_STATUS",
205
308
  message: `Handler returned undeclared status ${res.status} for ` +
206
309
  `${contract.method} ${contract.path} (contract: ${contract.name})`,
207
- details: {
208
- status: res.status,
209
- body: res.body,
210
- },
310
+ details: responseContractViolationDetails(contract, res.status, {
311
+ returnedStatus: res.status,
312
+ }),
211
313
  });
212
314
  }
213
315
  const responseSchema = contract.responses[res.status];
@@ -215,21 +317,25 @@ async function validateResponseAgainstContract(contract, res) {
215
317
  if (res.body !== undefined && res.body !== null) {
216
318
  throw new ResponseContractViolationError({
217
319
  code: "RESPONSE_VALIDATION_ERROR",
218
- message: `Response validation failed for ${contract.method} ${contract.path} ` +
219
- `(status ${res.status}, contract: ${contract.name})`,
220
- details: {
320
+ message: responseContractViolationMessage(contract, res.status),
321
+ details: responseContractViolationDetails(contract, res.status, {
221
322
  issues: [
222
323
  {
223
324
  message: "Response body must be empty for a null response schema.",
224
325
  },
225
326
  ],
226
- },
327
+ }),
227
328
  });
228
329
  }
229
330
  return;
230
331
  }
231
332
  if (!responseSchema)
232
333
  return;
334
+ // Binder routes whose use case output schema is the same object as the
335
+ // declared success response schema skip the redundant success-status parse.
336
+ // Error statuses and undeclared statuses are validated unchanged.
337
+ if (res.status === responseValidationExemptStatus)
338
+ return;
233
339
  try {
234
340
  await parseStandardSchema(responseSchema, res.body);
235
341
  await validateCatalogErrorResponse(contract, res);
@@ -238,17 +344,18 @@ async function validateResponseAgainstContract(contract, res) {
238
344
  if (error instanceof SchemaValidationError) {
239
345
  throw new ResponseContractViolationError({
240
346
  code: "RESPONSE_VALIDATION_ERROR",
241
- message: `Response validation failed for ${contract.method} ${contract.path} ` +
242
- `(status ${res.status}, contract: ${contract.name})`,
243
- details: { issues: error.issues },
347
+ message: responseContractViolationMessage(contract, res.status),
348
+ details: responseContractViolationDetails(contract, res.status, {
349
+ issues: error.issues,
350
+ }),
244
351
  });
245
352
  }
246
353
  throw error;
247
354
  }
248
355
  }
249
- async function finalizeResponse(contract, res) {
356
+ async function finalizeResponse(contract, res, responseValidationExemptStatus) {
250
357
  const normalized = normalizeResponse(res);
251
- await validateResponseAgainstContract(contract, normalized);
358
+ await validateResponseAgainstContract(contract, normalized, responseValidationExemptStatus);
252
359
  return normalized;
253
360
  }
254
361
  function toContractViolationResponse(error) {
@@ -301,11 +408,19 @@ async function parseBody(req) {
301
408
  return undefined;
302
409
  }
303
410
  }
304
- function buildHandler(
411
+ /**
412
+ * Build the per-request execution pipeline once.
413
+ *
414
+ * The returned executor takes the contract, compiled pattern, and user handler
415
+ * per invocation so fallback responses (404/405) can reuse a single pipeline
416
+ * across requests instead of rebuilding it per unmatched request.
417
+ */
418
+ function createRequestExecutor(
305
419
  // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
306
- options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
307
- const compiled = compilePath(contract.path);
308
- return async (req, preMatchedParams) => {
420
+ options, finalPorts, contextRuntime, hooks, routeHooks = [], optionsOverrides) {
421
+ const warnedNativeReplacementHooks = new WeakSet();
422
+ return async (target, req, preMatchedParams) => {
423
+ const { contract, handler: userHandler } = target;
309
424
  let baseCtx;
310
425
  let pathValue;
311
426
  let queryValue;
@@ -372,6 +487,36 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
372
487
  owner: "framework",
373
488
  };
374
489
  }
490
+ if (currentError instanceof TenantRequiredError) {
491
+ return {
492
+ ctx,
493
+ response: errorResponse(currentError.status, currentError.code, currentError.message),
494
+ error: currentError,
495
+ owner: "framework",
496
+ };
497
+ }
498
+ if (currentError instanceof IdempotencyConflictError) {
499
+ return {
500
+ ctx,
501
+ response: errorResponse(httpErrors.IdempotencyConflict.status, httpErrors.IdempotencyConflict.code, currentError.message, {
502
+ namespace: currentError.namespace,
503
+ key: currentError.key,
504
+ }),
505
+ error: currentError,
506
+ owner: "framework",
507
+ };
508
+ }
509
+ if (currentError instanceof IdempotencyInProgressError) {
510
+ return {
511
+ ctx,
512
+ response: errorResponse(httpErrors.IdempotencyInProgress.status, httpErrors.IdempotencyInProgress.code, currentError.message, {
513
+ namespace: currentError.namespace,
514
+ key: currentError.key,
515
+ }),
516
+ error: currentError,
517
+ owner: "framework",
518
+ };
519
+ }
375
520
  if (currentError instanceof GateAuthorizationError) {
376
521
  return {
377
522
  ctx,
@@ -450,8 +595,10 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
450
595
  matchedParams = preMatchedParams;
451
596
  }
452
597
  else {
453
- const match = compiled.pattern.exec(url.pathname);
454
- if (!match ||
598
+ const compiled = target.compiled;
599
+ const match = compiled ? compiled.pattern.exec(url.pathname) : null;
600
+ if (!compiled ||
601
+ !match ||
455
602
  contract.method.toUpperCase() !== req.method.toUpperCase()) {
456
603
  return errorResponse(404, "NOT_FOUND", "Not found");
457
604
  }
@@ -466,11 +613,49 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
466
613
  }
467
614
  }
468
615
  const rawHeaders = requestHeadersToRecord(req.headers);
469
- const applyTransformHooks = async (initialResult, allowRetry) => {
470
- if (isWebResponse(initialResult.response)) {
471
- return initialResult;
616
+ const runNativeBeforeSend = async (initialResult, nativeResponse) => {
617
+ const originalView = responseForHooks(nativeResponse);
618
+ const originalHeaders = originalView.headers ?? {};
619
+ let transformed = originalView;
620
+ for (const hook of hooks) {
621
+ if (!hook.beforeSend)
622
+ continue;
623
+ const nextResponse = await hook.beforeSend({
624
+ req,
625
+ ctx: initialResult.ctx,
626
+ contract,
627
+ path: pathValue,
628
+ query: queryValue,
629
+ headers: headersValue,
630
+ body: bodyValue,
631
+ response: transformed,
632
+ error: initialResult.error,
633
+ native: true,
634
+ });
635
+ if (nextResponse) {
636
+ if ((nextResponse.status !== nativeResponse.status ||
637
+ nextResponse.body !== undefined) &&
638
+ !warnedNativeReplacementHooks.has(hook) &&
639
+ process.env.NODE_ENV !== "production") {
640
+ warnedNativeReplacementHooks.add(hook);
641
+ console.warn(`[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.`);
642
+ }
643
+ transformed = {
644
+ status: nativeResponse.status,
645
+ headers: nextResponse.headers,
646
+ };
647
+ }
472
648
  }
649
+ return mergeNativeResponseHeaders(nativeResponse, originalHeaders, transformed.headers ?? {});
650
+ };
651
+ const applyTransformHooks = async (initialResult, allowRetry) => {
473
652
  try {
653
+ if (isWebResponse(initialResult.response)) {
654
+ return {
655
+ ...initialResult,
656
+ response: await runNativeBeforeSend(initialResult, initialResult.response),
657
+ };
658
+ }
474
659
  let transformed = normalizeResponse(initialResult.response);
475
660
  for (const hook of hooks) {
476
661
  if (!hook.beforeSend)
@@ -532,11 +717,7 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
532
717
  if (optionsOverrides?.skipRoutePreparation) {
533
718
  let createdCtx;
534
719
  try {
535
- createdCtx = await options.createContext({
536
- req,
537
- ports: finalPorts,
538
- contract,
539
- });
720
+ createdCtx = await contextRuntime.createRequestContext(req, contract);
540
721
  baseCtx = createdCtx;
541
722
  }
542
723
  catch (error) {
@@ -576,16 +757,10 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
576
757
  query = await parseStandardSchema(contract.query, query);
577
758
  }
578
759
  catch (error) {
579
- result =
580
- error instanceof SchemaValidationError
581
- ? {
582
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid query parameters", { issues: error.issues }),
583
- owner: "framework",
584
- }
585
- : {
586
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid query parameters", error.message),
587
- owner: "framework",
588
- };
760
+ result = {
761
+ response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid query parameters", "query", error),
762
+ owner: "framework",
763
+ };
589
764
  }
590
765
  }
591
766
  // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
@@ -595,16 +770,10 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
595
770
  path = await parseStandardSchema(contract.pathParams, matchedParams);
596
771
  }
597
772
  catch (error) {
598
- result =
599
- error instanceof SchemaValidationError
600
- ? {
601
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid path parameters", { issues: error.issues }),
602
- owner: "framework",
603
- }
604
- : {
605
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid path parameters", error.message),
606
- owner: "framework",
607
- };
773
+ result = {
774
+ response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid path parameters", "path", error),
775
+ owner: "framework",
776
+ };
608
777
  }
609
778
  }
610
779
  // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
@@ -615,16 +784,10 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
615
784
  headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
616
785
  }
617
786
  catch (error) {
618
- result =
619
- error instanceof SchemaValidationError
620
- ? {
621
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid request headers", { issues: error.issues }),
622
- owner: "framework",
623
- }
624
- : {
625
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid request headers", error.message),
626
- owner: "framework",
627
- };
787
+ result = {
788
+ response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request headers", "headers", error),
789
+ owner: "framework",
790
+ };
628
791
  }
629
792
  }
630
793
  // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
@@ -633,9 +796,9 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
633
796
  try {
634
797
  body = await parseBody(req);
635
798
  }
636
- catch {
799
+ catch (error) {
637
800
  result = {
638
- response: errorResponse(400, "INVALID_BODY", "Malformed JSON"),
801
+ response: requestValidationError(contract, 400, "INVALID_BODY", "Malformed JSON", "body", error),
639
802
  owner: "framework",
640
803
  };
641
804
  }
@@ -648,21 +811,15 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
648
811
  if (body === undefined &&
649
812
  error instanceof SchemaValidationError) {
650
813
  result = {
651
- response: errorResponse(400, "MISSING_BODY", "Request body is required"),
814
+ response: requestValidationError(contract, 400, "MISSING_BODY", "Request body is required", "body", error),
652
815
  owner: "framework",
653
816
  };
654
817
  }
655
818
  else {
656
- result =
657
- error instanceof SchemaValidationError
658
- ? {
659
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid request body", { issues: error.issues }),
660
- owner: "framework",
661
- }
662
- : {
663
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid request body", error.message),
664
- owner: "framework",
665
- };
819
+ result = {
820
+ response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request body", "body", error),
821
+ owner: "framework",
822
+ };
666
823
  }
667
824
  }
668
825
  }
@@ -673,11 +830,7 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
673
830
  bodyValue = body;
674
831
  let createdCtx;
675
832
  try {
676
- createdCtx = await options.createContext({
677
- req,
678
- ports: finalPorts,
679
- contract,
680
- });
833
+ createdCtx = await contextRuntime.createRequestContext(req, contract);
681
834
  baseCtx = createdCtx;
682
835
  }
683
836
  catch (error) {
@@ -724,7 +877,7 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
724
877
  break;
725
878
  }
726
879
  if (hookResult?.ctx !== undefined) {
727
- currentCtx = hookResult.ctx;
880
+ currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
728
881
  }
729
882
  if (hookResult?.response) {
730
883
  const response = normalizeHttpResponse(hookResult.response);
@@ -742,6 +895,39 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
742
895
  }
743
896
  }
744
897
  if (!result) {
898
+ for (const hook of routeHooks) {
899
+ try {
900
+ const additions = await hook.resolve({
901
+ req,
902
+ ctx: currentCtx,
903
+ contract,
904
+ path,
905
+ query,
906
+ headers,
907
+ body,
908
+ });
909
+ if (additions && typeof additions === "object") {
910
+ currentCtx = contextRuntime.finalizeContext({
911
+ ...currentCtx,
912
+ ...additions,
913
+ });
914
+ }
915
+ }
916
+ catch (error) {
917
+ result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
918
+ break;
919
+ }
920
+ }
921
+ }
922
+ if (!result) {
923
+ // Hooks may have elevated the actor or resolved a tenant.
924
+ // Refresh the ambient request context so record-time
925
+ // consumers such as createAmbientAuditLog see the finalized
926
+ // identity.
927
+ setActiveRequestIdentity({
928
+ actor: readContextActor(currentCtx),
929
+ tenant: readContextTenant(currentCtx),
930
+ });
745
931
  try {
746
932
  result = {
747
933
  ctx: currentCtx,
@@ -760,9 +946,11 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
760
946
  let finalResponse = normalizeHttpResponse(result.response);
761
947
  let finalError = result.error;
762
948
  let finalOwner = responseOwnerFor(finalResponse, result.owner);
763
- if (finalOwner === "route" && !isWebResponse(finalResponse)) {
949
+ if (finalOwner === "route" &&
950
+ !isWebResponse(finalResponse) &&
951
+ (options.validateResponses ?? true)) {
764
952
  try {
765
- finalResponse = await finalizeResponse(contract, finalResponse);
953
+ finalResponse = await finalizeResponse(contract, finalResponse, target.responseValidationExemptStatus);
766
954
  }
767
955
  catch (error) {
768
956
  if (error instanceof ResponseContractViolationError) {
@@ -820,6 +1008,18 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
820
1008
  }
821
1009
  };
822
1010
  }
1011
+ function buildHandler(
1012
+ // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
1013
+ options, finalPorts, contextRuntime, contract, userHandler, hooks, routeHooks = [], optionsOverrides, responseValidationExemptStatus) {
1014
+ const execute = createRequestExecutor(options, finalPorts, contextRuntime, hooks, routeHooks, optionsOverrides);
1015
+ const executionTarget = {
1016
+ contract,
1017
+ compiled: compilePath(contract.path),
1018
+ handler: userHandler,
1019
+ responseValidationExemptStatus,
1020
+ };
1021
+ return (req, preMatchedParams) => execute(executionTarget, req, preMatchedParams);
1022
+ }
823
1023
  /**
824
1024
  * Create a Beignet server instance.
825
1025
  *
@@ -828,19 +1028,80 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
828
1028
  * Use adapter packages such as `@beignet/next` to expose `server.api` to a
829
1029
  * specific runtime.
830
1030
  *
831
- * @param options - Ports, providers, routes, hooks, context factory, and error
832
- * mapping hooks for the server.
1031
+ * @param options - Ports, providers, routes, hooks, context blueprint, and
1032
+ * error mapping hooks for the server.
833
1033
  * @returns A started server instance with final ports and a catch-all handler.
834
1034
  */
835
1035
  export async function createServer(options) {
836
1036
  const registry = [];
1037
+ // Routes can register after startup via server.route(...), so the registry
1038
+ // is re-sorted lazily before the next dispatch instead of on every
1039
+ // registration.
1040
+ let registryNeedsSort = false;
837
1041
  const providers = (options.providers ?? []);
838
1042
  const env = options.providerEnv ?? process.env;
839
1043
  const overrides = options.providerConfig ?? {};
840
1044
  const providerResults = [];
841
1045
  const finalPorts = { ...options.ports };
842
- const hooks = [...(options.hooks ?? [])];
1046
+ const instrumentation = createServerInstrumentation(options.instrumentation);
1047
+ const hooks = [
1048
+ ...(instrumentation.hook
1049
+ ? [instrumentation.hook]
1050
+ : []),
1051
+ ...(options.hooks ?? []),
1052
+ ];
843
1053
  const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
1054
+ const resolvedContext = resolveServerContext(options.context);
1055
+ const finalizeContext = createContextFinalizer(resolvedContext, () => finalPorts);
1056
+ const createRequestContext = async (req, contract) => {
1057
+ const { requestId, trace } = instrumentation.prepareRequest(req);
1058
+ return finalizeContext(await resolvedContext.request({
1059
+ req,
1060
+ ports: finalPorts,
1061
+ contract,
1062
+ requestId,
1063
+ trace,
1064
+ }));
1065
+ };
1066
+ const createServiceContext = async (...args) => {
1067
+ const serviceFactory = resolvedContext.service;
1068
+ if (!serviceFactory) {
1069
+ throw new Error("Define context.service in createServer(...) to create service contexts.");
1070
+ }
1071
+ const { requestId, trace } = instrumentation.createServiceCorrelation();
1072
+ // Enter the ambient context synchronously (before the factory awaits) so
1073
+ // it propagates to the caller's continuation. Identity fields are filled
1074
+ // onto the same object once the context is finalized, so jobs, listeners,
1075
+ // schedules, and tasks observe the service actor/tenant at record time.
1076
+ const ambient = {
1077
+ requestId,
1078
+ traceId: trace.traceId,
1079
+ spanId: trace.spanId,
1080
+ parentSpanId: trace.parentSpanId,
1081
+ traceparent: trace.traceparent,
1082
+ };
1083
+ enterActiveRequestContext(ambient);
1084
+ const ctx = finalizeContext(await serviceFactory({
1085
+ ports: finalPorts,
1086
+ input: args[0],
1087
+ requestId,
1088
+ trace,
1089
+ }));
1090
+ ambient.actor = readContextActor(ctx);
1091
+ ambient.tenant = readContextTenant(ctx);
1092
+ return ctx;
1093
+ };
1094
+ const contextRuntime = {
1095
+ createRequestContext,
1096
+ finalizeContext,
1097
+ };
1098
+ let serviceContextsAvailable = false;
1099
+ const lifecycleCreateServiceContext = async (input) => {
1100
+ if (!serviceContextsAvailable) {
1101
+ throw new Error("Service contexts are unavailable until providers have started.");
1102
+ }
1103
+ return createServiceContext(...[input]);
1104
+ };
844
1105
  let stopped = false;
845
1106
  const stop = async () => {
846
1107
  if (stopped)
@@ -852,6 +1113,7 @@ export async function createServer(options) {
852
1113
  try {
853
1114
  await result?.stop?.({
854
1115
  ports: finalPorts,
1116
+ createServiceContext: lifecycleCreateServiceContext,
855
1117
  });
856
1118
  }
857
1119
  catch (err) {
@@ -864,7 +1126,8 @@ export async function createServer(options) {
864
1126
  };
865
1127
  const registeredPaths = new Set();
866
1128
  const registeredShapes = new Map();
867
- const registerRoute = (contract, handler) => {
1129
+ const registeredNames = new Map();
1130
+ const registerRoute = (contract, handler, routeHooks = [], responseValidationExemptStatus) => {
868
1131
  if (contract.body && !methodSupportsRequestBody(contract.method)) {
869
1132
  throw new Error(`Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`);
870
1133
  }
@@ -879,12 +1142,31 @@ export async function createServer(options) {
879
1142
  if (conflictingRoute) {
880
1143
  throw new Error(`Ambiguous route: ${routeKey} conflicts with ${conflictingRoute}. Dynamic parameter names are ignored during routing, so each method + path shape must be unique.`);
881
1144
  }
1145
+ const conflictingName = registeredNames.get(contract.name);
1146
+ if (conflictingName) {
1147
+ throw new Error(`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.`);
1148
+ }
1149
+ if (contract.pathParams) {
1150
+ const shape = getObjectSchemaShape(contract.pathParams);
1151
+ if (shape) {
1152
+ const { missingKeys, extraKeys } = comparePathParamsToTemplate({
1153
+ pathKeys: compiled.keys,
1154
+ shapeKeys: Object.keys(shape),
1155
+ });
1156
+ if (missingKeys.length > 0 || extraKeys.length > 0) {
1157
+ const details = formatPathParamsMismatch({ missingKeys, extraKeys });
1158
+ throw new Error(`Path parameters for contract "${contract.name}" must match "${contract.path}" (${details}). Path templates and pathParams schemas drive routing, clients, and OpenAPI together.`);
1159
+ }
1160
+ }
1161
+ }
882
1162
  registeredPaths.add(routeKey);
883
1163
  registeredShapes.set(shapeRouteKey, routeKey);
884
- const builtHandler = buildHandler(options, finalPorts, contract, handler, hooks);
1164
+ registeredNames.set(contract.name, routeKey);
1165
+ const builtHandler = buildHandler(options, finalPorts, contextRuntime, contract, handler, hooks, routeHooks, undefined, responseValidationExemptStatus);
885
1166
  registry.push({
886
1167
  contract,
887
1168
  compiled,
1169
+ method: contract.method.toUpperCase(),
888
1170
  handler: builtHandler,
889
1171
  match: (method, pathname) => {
890
1172
  if (contract.method.toUpperCase() !== method.toUpperCase()) {
@@ -896,11 +1178,11 @@ export async function createServer(options) {
896
1178
  return { matched: true };
897
1179
  },
898
1180
  });
899
- registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
1181
+ registryNeedsSort = true;
900
1182
  };
901
1183
  const createBuilder = (contract, shouldRegister) => ({
902
1184
  handle: (fn) => {
903
- const wrapped = buildHandler(options, finalPorts, contract, fn, hooks);
1185
+ const wrapped = buildHandler(options, finalPorts, contextRuntime, contract, fn, hooks);
904
1186
  if (shouldRegister)
905
1187
  registerRoute(contract, fn);
906
1188
  return wrapped;
@@ -910,7 +1192,21 @@ export async function createServer(options) {
910
1192
  try {
911
1193
  for (const route of options.routes) {
912
1194
  const contract = resolveContract(route.contract);
913
- registerRoute(contract, route.handle);
1195
+ const hasHandle = typeof route.handle === "function";
1196
+ const hasUseCase = isUseCaseRouteDef(route);
1197
+ if (hasHandle && hasUseCase) {
1198
+ throw new Error(`Route for contract "${contract.name}" declares both "handle" and "useCase". Bind the contract to exactly one of them.`);
1199
+ }
1200
+ if (!hasHandle && !hasUseCase) {
1201
+ throw new Error(`Route for contract "${contract.name}" declares neither "handle" nor "useCase". Bind the contract to a use case or implement a handler.`);
1202
+ }
1203
+ if (isUseCaseRouteDef(route)) {
1204
+ const { handler, responseValidationExemptStatus } = createUseCaseRouteHandler(contract, route);
1205
+ registerRoute(contract, handler, route.hooks, responseValidationExemptStatus);
1206
+ }
1207
+ else {
1208
+ registerRoute(contract, route.handle, route.hooks);
1209
+ }
914
1210
  }
915
1211
  }
916
1212
  catch (error) {
@@ -929,6 +1225,7 @@ export async function createServer(options) {
929
1225
  const result = await provider.setup({
930
1226
  ports: finalPorts,
931
1227
  config: cfg,
1228
+ createServiceContext: lifecycleCreateServiceContext,
932
1229
  });
933
1230
  if (result.ports) {
934
1231
  Object.assign(finalPorts, result.ports);
@@ -940,8 +1237,25 @@ export async function createServer(options) {
940
1237
  continue;
941
1238
  await result.start({
942
1239
  ports: finalPorts,
1240
+ createServiceContext: lifecycleCreateServiceContext,
943
1241
  });
944
1242
  }
1243
+ instrumentation.attachPorts(finalPorts);
1244
+ const onUnboundPorts = options.onUnboundPorts ?? "error";
1245
+ if (onUnboundPorts !== "ignore") {
1246
+ const unboundKeys = Object.keys(finalPorts).filter((key) => isUnboundPort(finalPorts[key]));
1247
+ if (unboundKeys.length > 0) {
1248
+ const message = `Unbound ports after provider startup: ${unboundKeys.join(", ")}. ` +
1249
+ "Each port declared as deferred in definePorts(...) must be contributed " +
1250
+ "by a provider (server/providers.ts) or bound in infra/app-ports.ts. " +
1251
+ 'Pass onUnboundPorts: "warn" or "ignore" to change this behavior.';
1252
+ if (onUnboundPorts === "error") {
1253
+ throw new Error(message);
1254
+ }
1255
+ console.warn(`[beignet] ${message}`);
1256
+ }
1257
+ }
1258
+ serviceContextsAvailable = true;
945
1259
  }
946
1260
  catch (error) {
947
1261
  try {
@@ -952,27 +1266,62 @@ export async function createServer(options) {
952
1266
  }
953
1267
  throw error;
954
1268
  }
1269
+ // The fallback 404/405 pipeline is built once at server creation. Only the
1270
+ // contract surface that hooks observe (method, path, and the Allow set for
1271
+ // 405s) is assembled per unmatched request.
1272
+ const executeFallback = createRequestExecutor(options, finalPorts, contextRuntime, hooks, [], {
1273
+ skipRoutePreparation: true,
1274
+ });
1275
+ const fallbackContract = (name, method, path) => ({
1276
+ kind: "http",
1277
+ name,
1278
+ method: method,
1279
+ path,
1280
+ pathParams: null,
1281
+ query: null,
1282
+ body: null,
1283
+ responses: {},
1284
+ metadata: {},
1285
+ });
1286
+ const notFoundHandler = async () => errorResponse(404, "NOT_FOUND", "Not found");
955
1287
  const api = async (req) => {
1288
+ if (registryNeedsSort) {
1289
+ registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
1290
+ registryNeedsSort = false;
1291
+ }
956
1292
  const url = new URL(req.url);
1293
+ const method = req.method.toUpperCase();
1294
+ let pathMatchedMethods;
957
1295
  for (const entry of registry) {
958
- const result = entry.match(req.method, url.pathname);
959
- if (result.matched) {
1296
+ if (!entry.compiled.pattern.test(url.pathname))
1297
+ continue;
1298
+ if (entry.method === method) {
960
1299
  return await entry.handler(req);
961
1300
  }
1301
+ if (!pathMatchedMethods) {
1302
+ pathMatchedMethods = new Set();
1303
+ }
1304
+ pathMatchedMethods.add(entry.method);
962
1305
  }
963
- const notFoundContract = {
964
- kind: "http",
965
- name: "notFound",
966
- method: req.method.toUpperCase(),
967
- path: url.pathname || "/",
968
- pathParams: null,
969
- query: null,
970
- body: null,
971
- responses: {},
972
- metadata: {},
973
- };
974
- const notFoundHandler = buildHandler(options, finalPorts, notFoundContract, async () => errorResponse(404, "NOT_FOUND", "Not found"), hooks, { skipRoutePreparation: true });
975
- return await notFoundHandler(req);
1306
+ const pathname = url.pathname || "/";
1307
+ if (pathMatchedMethods) {
1308
+ const allow = [...pathMatchedMethods].sort().join(", ");
1309
+ return await executeFallback({
1310
+ contract: fallbackContract("methodNotAllowed", method, pathname),
1311
+ handler: async () => ({
1312
+ status: 405,
1313
+ headers: { allow },
1314
+ body: createErrorResponseBody({
1315
+ code: "METHOD_NOT_ALLOWED",
1316
+ message: `Method ${method} is not allowed for ${pathname}`,
1317
+ }),
1318
+ }),
1319
+ }, req, {});
1320
+ }
1321
+ return await executeFallback({
1322
+ contract: fallbackContract("notFound", method, pathname),
1323
+ handler: notFoundHandler,
1324
+ }, req, {});
976
1325
  };
977
1326
  return {
978
1327
  api,
@@ -980,29 +1329,23 @@ export async function createServer(options) {
980
1329
  const contract = resolveContract(contractLike);
981
1330
  return createBuilder(contract, true);
982
1331
  },
1332
+ createRequestContext: (req) => createRequestContext(req),
1333
+ createServiceContext,
983
1334
  contracts,
984
1335
  stop,
985
1336
  ports: finalPorts,
986
1337
  };
987
1338
  }
988
- /**
989
- * Define and flatten route registrations with strong type inference.
990
- *
991
- * Pass route definitions and route groups here before `createServer(...)`.
992
- * Group entries are flattened so downstream tooling receives one route list.
993
- *
994
- * @example
995
- * ```ts
996
- * const routes = defineRoutes<AppContext>([
997
- * { contract: listPosts, handle: async ({ ctx }) => ctx.posts.list() },
998
- * ]);
999
- * ```
1000
- */
1001
1339
  export function defineRoutes(routes) {
1002
1340
  const flattened = [];
1003
1341
  for (const route of routes) {
1004
1342
  if (isRouteGroup(route)) {
1005
- flattened.push(...route.routes);
1343
+ for (const groupRoute of route.routes) {
1344
+ flattened.push({
1345
+ ...groupRoute,
1346
+ hooks: [...(route.hooks ?? []), ...(groupRoute.hooks ?? [])],
1347
+ });
1348
+ }
1006
1349
  }
1007
1350
  else {
1008
1351
  flattened.push(route);
@@ -1019,26 +1362,20 @@ export function defineRoutes(routes) {
1019
1362
  export function contractsFromRoutes(routes) {
1020
1363
  return routes.map((route) => resolveContract(route.contract));
1021
1364
  }
1022
- /**
1023
- * Define a named group of related route registrations.
1024
- *
1025
- * Route groups are flattened by defineRoutes, so createServer still receives
1026
- * a regular route list while app code can keep feature route wiring colocated.
1027
- *
1028
- * @example
1029
- * ```ts
1030
- * const todoRoutes = defineRouteGroup<AppContext>({
1031
- * name: "todos",
1032
- * routes: [
1033
- * { contract: listTodos, handle: async ({ ctx }) => ctx.todos.list() },
1034
- * ]
1035
- * });
1036
- * ```
1037
- */
1038
1365
  export function defineRouteGroup(group) {
1366
+ const createGroup = (input) => ({
1367
+ kind: ROUTE_GROUP_KIND,
1368
+ name: input.name,
1369
+ hooks: input.hooks,
1370
+ routes: input.routes,
1371
+ });
1372
+ if (!group) {
1373
+ return createGroup;
1374
+ }
1039
1375
  return {
1040
1376
  kind: ROUTE_GROUP_KIND,
1041
1377
  name: group.name,
1378
+ hooks: group.hooks,
1042
1379
  routes: group.routes,
1043
1380
  };
1044
1381
  }