@beignet/core 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/README.md +792 -50
  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
@@ -1,9 +1,15 @@
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";
8
14
  /**
9
15
  * Define one route registration with hook-aware handler typing.
@@ -13,7 +19,10 @@ const ROUTE_GROUP_KIND = "beignet.route-group";
13
19
  * added fields.
14
20
  */
15
21
  export function defineRoute() {
16
- return (route) => route;
22
+ function define(route) {
23
+ return route;
24
+ }
25
+ return define;
17
26
  }
18
27
  class PathDecodeError extends Error {
19
28
  constructor() {
@@ -65,6 +74,35 @@ function errorResponse(status, code, message, details) {
65
74
  body: createErrorResponseBody({ code, message, details }),
66
75
  };
67
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
+ }
68
106
  function normalizeResponse(res) {
69
107
  return {
70
108
  status: res.status,
@@ -136,6 +174,42 @@ function responseForHooks(res) {
136
174
  headers: headersToRecord(res.headers),
137
175
  };
138
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
+ }
139
213
  function isHttpResponseLike(value) {
140
214
  return (!isWebResponse(value) &&
141
215
  typeof value === "object" &&
@@ -153,6 +227,25 @@ class ResponseContractViolationError extends Error {
153
227
  this.details = args.details;
154
228
  }
155
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
+ }
156
249
  function getDeclaredCatalogErrorsForStatus(contract, status) {
157
250
  const errors = contract.metadata?.errors;
158
251
  if (typeof errors !== "object" || errors === null)
@@ -175,16 +268,15 @@ async function validateCatalogErrorResponse(contract, res) {
175
268
  if (!matchingError) {
176
269
  throw new ResponseContractViolationError({
177
270
  code: "RESPONSE_VALIDATION_ERROR",
178
- message: `Response validation failed for ${contract.method} ${contract.path} ` +
179
- `(status ${res.status}, contract: ${contract.name})`,
180
- details: {
271
+ message: responseContractViolationMessage(contract, res.status),
272
+ details: responseContractViolationDetails(contract, res.status, {
181
273
  issues: [
182
274
  {
183
275
  message: `Error response code "${body.code}" is not declared for status ${res.status}. ` +
184
276
  `Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
185
277
  },
186
278
  ],
187
- },
279
+ }),
188
280
  });
189
281
  }
190
282
  if (matchingError.details && body.details !== undefined) {
@@ -195,16 +287,17 @@ async function validateCatalogErrorResponse(contract, res) {
195
287
  if (error instanceof SchemaValidationError) {
196
288
  throw new ResponseContractViolationError({
197
289
  code: "RESPONSE_VALIDATION_ERROR",
198
- message: `Response validation failed for ${contract.method} ${contract.path} ` +
199
- `(status ${res.status}, contract: ${contract.name})`,
200
- details: { issues: error.issues },
290
+ message: responseContractViolationMessage(contract, res.status),
291
+ details: responseContractViolationDetails(contract, res.status, {
292
+ issues: error.issues,
293
+ }),
201
294
  });
202
295
  }
203
296
  throw error;
204
297
  }
205
298
  }
206
299
  }
207
- async function validateResponseAgainstContract(contract, res) {
300
+ async function validateResponseAgainstContract(contract, res, responseValidationExemptStatus) {
208
301
  const statusKey = String(res.status);
209
302
  const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
210
303
  if (!hasDeclaredStatus) {
@@ -214,10 +307,9 @@ async function validateResponseAgainstContract(contract, res) {
214
307
  code: "UNDECLARED_RESPONSE_STATUS",
215
308
  message: `Handler returned undeclared status ${res.status} for ` +
216
309
  `${contract.method} ${contract.path} (contract: ${contract.name})`,
217
- details: {
218
- status: res.status,
219
- body: res.body,
220
- },
310
+ details: responseContractViolationDetails(contract, res.status, {
311
+ returnedStatus: res.status,
312
+ }),
221
313
  });
222
314
  }
223
315
  const responseSchema = contract.responses[res.status];
@@ -225,21 +317,25 @@ async function validateResponseAgainstContract(contract, res) {
225
317
  if (res.body !== undefined && res.body !== null) {
226
318
  throw new ResponseContractViolationError({
227
319
  code: "RESPONSE_VALIDATION_ERROR",
228
- message: `Response validation failed for ${contract.method} ${contract.path} ` +
229
- `(status ${res.status}, contract: ${contract.name})`,
230
- details: {
320
+ message: responseContractViolationMessage(contract, res.status),
321
+ details: responseContractViolationDetails(contract, res.status, {
231
322
  issues: [
232
323
  {
233
324
  message: "Response body must be empty for a null response schema.",
234
325
  },
235
326
  ],
236
- },
327
+ }),
237
328
  });
238
329
  }
239
330
  return;
240
331
  }
241
332
  if (!responseSchema)
242
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;
243
339
  try {
244
340
  await parseStandardSchema(responseSchema, res.body);
245
341
  await validateCatalogErrorResponse(contract, res);
@@ -248,17 +344,18 @@ async function validateResponseAgainstContract(contract, res) {
248
344
  if (error instanceof SchemaValidationError) {
249
345
  throw new ResponseContractViolationError({
250
346
  code: "RESPONSE_VALIDATION_ERROR",
251
- message: `Response validation failed for ${contract.method} ${contract.path} ` +
252
- `(status ${res.status}, contract: ${contract.name})`,
253
- details: { issues: error.issues },
347
+ message: responseContractViolationMessage(contract, res.status),
348
+ details: responseContractViolationDetails(contract, res.status, {
349
+ issues: error.issues,
350
+ }),
254
351
  });
255
352
  }
256
353
  throw error;
257
354
  }
258
355
  }
259
- async function finalizeResponse(contract, res) {
356
+ async function finalizeResponse(contract, res, responseValidationExemptStatus) {
260
357
  const normalized = normalizeResponse(res);
261
- await validateResponseAgainstContract(contract, normalized);
358
+ await validateResponseAgainstContract(contract, normalized, responseValidationExemptStatus);
262
359
  return normalized;
263
360
  }
264
361
  function toContractViolationResponse(error) {
@@ -311,11 +408,19 @@ async function parseBody(req) {
311
408
  return undefined;
312
409
  }
313
410
  }
314
- 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(
315
419
  // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
316
- options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverrides) {
317
- const compiled = compilePath(contract.path);
318
- 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;
319
424
  let baseCtx;
320
425
  let pathValue;
321
426
  let queryValue;
@@ -382,6 +487,36 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
382
487
  owner: "framework",
383
488
  };
384
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
+ }
385
520
  if (currentError instanceof GateAuthorizationError) {
386
521
  return {
387
522
  ctx,
@@ -460,8 +595,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
460
595
  matchedParams = preMatchedParams;
461
596
  }
462
597
  else {
463
- const match = compiled.pattern.exec(url.pathname);
464
- if (!match ||
598
+ const compiled = target.compiled;
599
+ const match = compiled ? compiled.pattern.exec(url.pathname) : null;
600
+ if (!compiled ||
601
+ !match ||
465
602
  contract.method.toUpperCase() !== req.method.toUpperCase()) {
466
603
  return errorResponse(404, "NOT_FOUND", "Not found");
467
604
  }
@@ -476,11 +613,49 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
476
613
  }
477
614
  }
478
615
  const rawHeaders = requestHeadersToRecord(req.headers);
479
- const applyTransformHooks = async (initialResult, allowRetry) => {
480
- if (isWebResponse(initialResult.response)) {
481
- 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
+ }
482
648
  }
649
+ return mergeNativeResponseHeaders(nativeResponse, originalHeaders, transformed.headers ?? {});
650
+ };
651
+ const applyTransformHooks = async (initialResult, allowRetry) => {
483
652
  try {
653
+ if (isWebResponse(initialResult.response)) {
654
+ return {
655
+ ...initialResult,
656
+ response: await runNativeBeforeSend(initialResult, initialResult.response),
657
+ };
658
+ }
484
659
  let transformed = normalizeResponse(initialResult.response);
485
660
  for (const hook of hooks) {
486
661
  if (!hook.beforeSend)
@@ -542,11 +717,7 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
542
717
  if (optionsOverrides?.skipRoutePreparation) {
543
718
  let createdCtx;
544
719
  try {
545
- createdCtx = await options.createContext({
546
- req,
547
- ports: finalPorts,
548
- contract,
549
- });
720
+ createdCtx = await contextRuntime.createRequestContext(req, contract);
550
721
  baseCtx = createdCtx;
551
722
  }
552
723
  catch (error) {
@@ -586,16 +757,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
586
757
  query = await parseStandardSchema(contract.query, query);
587
758
  }
588
759
  catch (error) {
589
- result =
590
- error instanceof SchemaValidationError
591
- ? {
592
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid query parameters", { issues: error.issues }),
593
- owner: "framework",
594
- }
595
- : {
596
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid query parameters", error.message),
597
- owner: "framework",
598
- };
760
+ result = {
761
+ response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid query parameters", "query", error),
762
+ owner: "framework",
763
+ };
599
764
  }
600
765
  }
601
766
  // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
@@ -605,16 +770,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
605
770
  path = await parseStandardSchema(contract.pathParams, matchedParams);
606
771
  }
607
772
  catch (error) {
608
- result =
609
- error instanceof SchemaValidationError
610
- ? {
611
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid path parameters", { issues: error.issues }),
612
- owner: "framework",
613
- }
614
- : {
615
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid path parameters", error.message),
616
- owner: "framework",
617
- };
773
+ result = {
774
+ response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid path parameters", "path", error),
775
+ owner: "framework",
776
+ };
618
777
  }
619
778
  }
620
779
  // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
@@ -625,16 +784,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
625
784
  headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
626
785
  }
627
786
  catch (error) {
628
- result =
629
- error instanceof SchemaValidationError
630
- ? {
631
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid request headers", { issues: error.issues }),
632
- owner: "framework",
633
- }
634
- : {
635
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid request headers", error.message),
636
- owner: "framework",
637
- };
787
+ result = {
788
+ response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request headers", "headers", error),
789
+ owner: "framework",
790
+ };
638
791
  }
639
792
  }
640
793
  // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
@@ -643,9 +796,9 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
643
796
  try {
644
797
  body = await parseBody(req);
645
798
  }
646
- catch {
799
+ catch (error) {
647
800
  result = {
648
- response: errorResponse(400, "INVALID_BODY", "Malformed JSON"),
801
+ response: requestValidationError(contract, 400, "INVALID_BODY", "Malformed JSON", "body", error),
649
802
  owner: "framework",
650
803
  };
651
804
  }
@@ -658,21 +811,15 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
658
811
  if (body === undefined &&
659
812
  error instanceof SchemaValidationError) {
660
813
  result = {
661
- response: errorResponse(400, "MISSING_BODY", "Request body is required"),
814
+ response: requestValidationError(contract, 400, "MISSING_BODY", "Request body is required", "body", error),
662
815
  owner: "framework",
663
816
  };
664
817
  }
665
818
  else {
666
- result =
667
- error instanceof SchemaValidationError
668
- ? {
669
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid request body", { issues: error.issues }),
670
- owner: "framework",
671
- }
672
- : {
673
- response: errorResponse(422, "VALIDATION_ERROR", "Invalid request body", error.message),
674
- owner: "framework",
675
- };
819
+ result = {
820
+ response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request body", "body", error),
821
+ owner: "framework",
822
+ };
676
823
  }
677
824
  }
678
825
  }
@@ -683,11 +830,7 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
683
830
  bodyValue = body;
684
831
  let createdCtx;
685
832
  try {
686
- createdCtx = await options.createContext({
687
- req,
688
- ports: finalPorts,
689
- contract,
690
- });
833
+ createdCtx = await contextRuntime.createRequestContext(req, contract);
691
834
  baseCtx = createdCtx;
692
835
  }
693
836
  catch (error) {
@@ -734,7 +877,7 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
734
877
  break;
735
878
  }
736
879
  if (hookResult?.ctx !== undefined) {
737
- currentCtx = hookResult.ctx;
880
+ currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
738
881
  }
739
882
  if (hookResult?.response) {
740
883
  const response = normalizeHttpResponse(hookResult.response);
@@ -764,10 +907,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
764
907
  body,
765
908
  });
766
909
  if (additions && typeof additions === "object") {
767
- currentCtx = {
910
+ currentCtx = contextRuntime.finalizeContext({
768
911
  ...currentCtx,
769
912
  ...additions,
770
- };
913
+ });
771
914
  }
772
915
  }
773
916
  catch (error) {
@@ -777,6 +920,14 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
777
920
  }
778
921
  }
779
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
+ });
780
931
  try {
781
932
  result = {
782
933
  ctx: currentCtx,
@@ -795,9 +946,11 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
795
946
  let finalResponse = normalizeHttpResponse(result.response);
796
947
  let finalError = result.error;
797
948
  let finalOwner = responseOwnerFor(finalResponse, result.owner);
798
- if (finalOwner === "route" && !isWebResponse(finalResponse)) {
949
+ if (finalOwner === "route" &&
950
+ !isWebResponse(finalResponse) &&
951
+ (options.validateResponses ?? true)) {
799
952
  try {
800
- finalResponse = await finalizeResponse(contract, finalResponse);
953
+ finalResponse = await finalizeResponse(contract, finalResponse, target.responseValidationExemptStatus);
801
954
  }
802
955
  catch (error) {
803
956
  if (error instanceof ResponseContractViolationError) {
@@ -855,6 +1008,18 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
855
1008
  }
856
1009
  };
857
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
+ }
858
1023
  /**
859
1024
  * Create a Beignet server instance.
860
1025
  *
@@ -863,19 +1028,80 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
863
1028
  * Use adapter packages such as `@beignet/next` to expose `server.api` to a
864
1029
  * specific runtime.
865
1030
  *
866
- * @param options - Ports, providers, routes, hooks, context factory, and error
867
- * mapping hooks for the server.
1031
+ * @param options - Ports, providers, routes, hooks, context blueprint, and
1032
+ * error mapping hooks for the server.
868
1033
  * @returns A started server instance with final ports and a catch-all handler.
869
1034
  */
870
1035
  export async function createServer(options) {
871
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;
872
1041
  const providers = (options.providers ?? []);
873
1042
  const env = options.providerEnv ?? process.env;
874
1043
  const overrides = options.providerConfig ?? {};
875
1044
  const providerResults = [];
876
1045
  const finalPorts = { ...options.ports };
877
- const hooks = [...(options.hooks ?? [])];
1046
+ const instrumentation = createServerInstrumentation(options.instrumentation);
1047
+ const hooks = [
1048
+ ...(instrumentation.hook
1049
+ ? [instrumentation.hook]
1050
+ : []),
1051
+ ...(options.hooks ?? []),
1052
+ ];
878
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
+ };
879
1105
  let stopped = false;
880
1106
  const stop = async () => {
881
1107
  if (stopped)
@@ -887,6 +1113,7 @@ export async function createServer(options) {
887
1113
  try {
888
1114
  await result?.stop?.({
889
1115
  ports: finalPorts,
1116
+ createServiceContext: lifecycleCreateServiceContext,
890
1117
  });
891
1118
  }
892
1119
  catch (err) {
@@ -899,7 +1126,8 @@ export async function createServer(options) {
899
1126
  };
900
1127
  const registeredPaths = new Set();
901
1128
  const registeredShapes = new Map();
902
- const registerRoute = (contract, handler, routeHooks = []) => {
1129
+ const registeredNames = new Map();
1130
+ const registerRoute = (contract, handler, routeHooks = [], responseValidationExemptStatus) => {
903
1131
  if (contract.body && !methodSupportsRequestBody(contract.method)) {
904
1132
  throw new Error(`Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`);
905
1133
  }
@@ -914,12 +1142,31 @@ export async function createServer(options) {
914
1142
  if (conflictingRoute) {
915
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.`);
916
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
+ }
917
1162
  registeredPaths.add(routeKey);
918
1163
  registeredShapes.set(shapeRouteKey, routeKey);
919
- const builtHandler = buildHandler(options, finalPorts, contract, handler, hooks, routeHooks);
1164
+ registeredNames.set(contract.name, routeKey);
1165
+ const builtHandler = buildHandler(options, finalPorts, contextRuntime, contract, handler, hooks, routeHooks, undefined, responseValidationExemptStatus);
920
1166
  registry.push({
921
1167
  contract,
922
1168
  compiled,
1169
+ method: contract.method.toUpperCase(),
923
1170
  handler: builtHandler,
924
1171
  match: (method, pathname) => {
925
1172
  if (contract.method.toUpperCase() !== method.toUpperCase()) {
@@ -931,11 +1178,11 @@ export async function createServer(options) {
931
1178
  return { matched: true };
932
1179
  },
933
1180
  });
934
- registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
1181
+ registryNeedsSort = true;
935
1182
  };
936
1183
  const createBuilder = (contract, shouldRegister) => ({
937
1184
  handle: (fn) => {
938
- const wrapped = buildHandler(options, finalPorts, contract, fn, hooks);
1185
+ const wrapped = buildHandler(options, finalPorts, contextRuntime, contract, fn, hooks);
939
1186
  if (shouldRegister)
940
1187
  registerRoute(contract, fn);
941
1188
  return wrapped;
@@ -945,7 +1192,21 @@ export async function createServer(options) {
945
1192
  try {
946
1193
  for (const route of options.routes) {
947
1194
  const contract = resolveContract(route.contract);
948
- registerRoute(contract, route.handle, route.hooks);
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
+ }
949
1210
  }
950
1211
  }
951
1212
  catch (error) {
@@ -964,6 +1225,7 @@ export async function createServer(options) {
964
1225
  const result = await provider.setup({
965
1226
  ports: finalPorts,
966
1227
  config: cfg,
1228
+ createServiceContext: lifecycleCreateServiceContext,
967
1229
  });
968
1230
  if (result.ports) {
969
1231
  Object.assign(finalPorts, result.ports);
@@ -975,8 +1237,25 @@ export async function createServer(options) {
975
1237
  continue;
976
1238
  await result.start({
977
1239
  ports: finalPorts,
1240
+ createServiceContext: lifecycleCreateServiceContext,
978
1241
  });
979
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;
980
1259
  }
981
1260
  catch (error) {
982
1261
  try {
@@ -987,27 +1266,62 @@ export async function createServer(options) {
987
1266
  }
988
1267
  throw error;
989
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");
990
1287
  const api = async (req) => {
1288
+ if (registryNeedsSort) {
1289
+ registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
1290
+ registryNeedsSort = false;
1291
+ }
991
1292
  const url = new URL(req.url);
1293
+ const method = req.method.toUpperCase();
1294
+ let pathMatchedMethods;
992
1295
  for (const entry of registry) {
993
- const result = entry.match(req.method, url.pathname);
994
- if (result.matched) {
1296
+ if (!entry.compiled.pattern.test(url.pathname))
1297
+ continue;
1298
+ if (entry.method === method) {
995
1299
  return await entry.handler(req);
996
1300
  }
1301
+ if (!pathMatchedMethods) {
1302
+ pathMatchedMethods = new Set();
1303
+ }
1304
+ pathMatchedMethods.add(entry.method);
997
1305
  }
998
- const notFoundContract = {
999
- kind: "http",
1000
- name: "notFound",
1001
- method: req.method.toUpperCase(),
1002
- path: url.pathname || "/",
1003
- pathParams: null,
1004
- query: null,
1005
- body: null,
1006
- responses: {},
1007
- metadata: {},
1008
- };
1009
- const notFoundHandler = buildHandler(options, finalPorts, notFoundContract, async () => errorResponse(404, "NOT_FOUND", "Not found"), hooks, [], { skipRoutePreparation: true });
1010
- 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, {});
1011
1325
  };
1012
1326
  return {
1013
1327
  api,
@@ -1015,6 +1329,8 @@ export async function createServer(options) {
1015
1329
  const contract = resolveContract(contractLike);
1016
1330
  return createBuilder(contract, true);
1017
1331
  },
1332
+ createRequestContext: (req) => createRequestContext(req),
1333
+ createServiceContext,
1018
1334
  contracts,
1019
1335
  stop,
1020
1336
  ports: finalPorts,