@beignet/core 0.0.33 → 0.0.35

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 (142) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +207 -30
  3. package/dist/agent-capabilities/index.d.ts +167 -0
  4. package/dist/agent-capabilities/index.d.ts.map +1 -0
  5. package/dist/agent-capabilities/index.js +275 -0
  6. package/dist/agent-capabilities/index.js.map +1 -0
  7. package/dist/application/index.d.ts +2 -2
  8. package/dist/application/index.d.ts.map +1 -1
  9. package/dist/application/index.js +13 -3
  10. package/dist/application/index.js.map +1 -1
  11. package/dist/client/client.d.ts +2 -0
  12. package/dist/client/client.d.ts.map +1 -1
  13. package/dist/client/client.js +33 -16
  14. package/dist/client/client.js.map +1 -1
  15. package/dist/contracts/contract-builder.d.ts +5 -7
  16. package/dist/contracts/contract-builder.d.ts.map +1 -1
  17. package/dist/contracts/contract-builder.js +3 -18
  18. package/dist/contracts/contract-builder.js.map +1 -1
  19. package/dist/contracts/contract-group.d.ts +2 -2
  20. package/dist/contracts/contract-group.d.ts.map +1 -1
  21. package/dist/contracts/contract-group.js +2 -1
  22. package/dist/contracts/contract-group.js.map +1 -1
  23. package/dist/contracts/metadata.d.ts +3 -0
  24. package/dist/contracts/metadata.d.ts.map +1 -0
  25. package/dist/contracts/metadata.js +14 -0
  26. package/dist/contracts/metadata.js.map +1 -0
  27. package/dist/contracts/types.d.ts +20 -0
  28. package/dist/contracts/types.d.ts.map +1 -1
  29. package/dist/contracts/types.js.map +1 -1
  30. package/dist/events/index.d.ts +4 -4
  31. package/dist/events/index.d.ts.map +1 -1
  32. package/dist/events/index.js +5 -5
  33. package/dist/events/index.js.map +1 -1
  34. package/dist/flags/index.d.ts +0 -4
  35. package/dist/flags/index.d.ts.map +1 -1
  36. package/dist/flags/index.js +0 -4
  37. package/dist/flags/index.js.map +1 -1
  38. package/dist/idempotency/index.d.ts.map +1 -1
  39. package/dist/idempotency/index.js +14 -9
  40. package/dist/idempotency/index.js.map +1 -1
  41. package/dist/jobs/index.d.ts.map +1 -1
  42. package/dist/jobs/index.js +10 -4
  43. package/dist/jobs/index.js.map +1 -1
  44. package/dist/notifications/index.d.ts +194 -10
  45. package/dist/notifications/index.d.ts.map +1 -1
  46. package/dist/notifications/index.js +397 -59
  47. package/dist/notifications/index.js.map +1 -1
  48. package/dist/openapi/index.js +15 -8
  49. package/dist/openapi/index.js.map +1 -1
  50. package/dist/ports/auth.d.ts +2 -0
  51. package/dist/ports/auth.d.ts.map +1 -1
  52. package/dist/ports/auth.js +2 -0
  53. package/dist/ports/auth.js.map +1 -1
  54. package/dist/ports/events.d.ts +2 -2
  55. package/dist/ports/index.d.ts +1 -1
  56. package/dist/ports/index.d.ts.map +1 -1
  57. package/dist/ports/index.js +1 -1
  58. package/dist/ports/index.js.map +1 -1
  59. package/dist/providers/index.d.ts +1 -1
  60. package/dist/providers/index.d.ts.map +1 -1
  61. package/dist/providers/index.js.map +1 -1
  62. package/dist/providers/provider.d.ts +10 -2
  63. package/dist/providers/provider.d.ts.map +1 -1
  64. package/dist/providers/provider.js.map +1 -1
  65. package/dist/schedules/index.d.ts +6 -13
  66. package/dist/schedules/index.d.ts.map +1 -1
  67. package/dist/schedules/index.js +24 -44
  68. package/dist/schedules/index.js.map +1 -1
  69. package/dist/server/index.d.ts +2 -2
  70. package/dist/server/index.d.ts.map +1 -1
  71. package/dist/server/index.js +1 -1
  72. package/dist/server/index.js.map +1 -1
  73. package/dist/server/request-executor.d.ts +72 -0
  74. package/dist/server/request-executor.d.ts.map +1 -0
  75. package/dist/server/request-executor.js +781 -0
  76. package/dist/server/request-executor.js.map +1 -0
  77. package/dist/server/request-preparation.d.ts +39 -0
  78. package/dist/server/request-preparation.d.ts.map +1 -0
  79. package/dist/server/request-preparation.js +248 -0
  80. package/dist/server/request-preparation.js.map +1 -0
  81. package/dist/server/response-finalization.d.ts +34 -0
  82. package/dist/server/response-finalization.d.ts.map +1 -0
  83. package/dist/server/response-finalization.js +279 -0
  84. package/dist/server/response-finalization.js.map +1 -0
  85. package/dist/server/route-definitions.d.ts +139 -0
  86. package/dist/server/route-definitions.d.ts.map +1 -0
  87. package/dist/server/route-definitions.js +94 -0
  88. package/dist/server/route-definitions.js.map +1 -0
  89. package/dist/server/route-matching.d.ts +15 -0
  90. package/dist/server/route-matching.d.ts.map +1 -0
  91. package/dist/server/route-matching.js +46 -0
  92. package/dist/server/route-matching.js.map +1 -0
  93. package/dist/server/server.d.ts +6 -157
  94. package/dist/server/server.d.ts.map +1 -1
  95. package/dist/server/server.js +29 -1340
  96. package/dist/server/server.js.map +1 -1
  97. package/dist/server/use-case-route.d.ts +1 -1
  98. package/dist/testing/index.d.ts +1 -1
  99. package/dist/testing/index.d.ts.map +1 -1
  100. package/dist/testing/index.js +1 -0
  101. package/dist/testing/index.js.map +1 -1
  102. package/dist/tracing/index.d.ts +1 -1
  103. package/dist/tracing/index.d.ts.map +1 -1
  104. package/dist/tracing/index.js.map +1 -1
  105. package/dist/uploads/client.js +4 -7
  106. package/dist/uploads/client.js.map +1 -1
  107. package/dist/uploads/index.d.ts.map +1 -1
  108. package/dist/uploads/index.js +59 -12
  109. package/dist/uploads/index.js.map +1 -1
  110. package/package.json +5 -5
  111. package/skills/app-architecture/SKILL.md +31 -1
  112. package/src/agent-capabilities/index.ts +578 -0
  113. package/src/application/index.ts +19 -5
  114. package/src/client/client.ts +41 -17
  115. package/src/contracts/contract-builder.ts +8 -23
  116. package/src/contracts/contract-group.ts +4 -6
  117. package/src/contracts/metadata.ts +20 -0
  118. package/src/contracts/types.ts +39 -0
  119. package/src/events/index.ts +16 -20
  120. package/src/flags/index.ts +0 -5
  121. package/src/idempotency/index.ts +17 -9
  122. package/src/jobs/index.ts +8 -3
  123. package/src/notifications/index.ts +678 -70
  124. package/src/openapi/index.ts +24 -12
  125. package/src/ports/auth.ts +2 -0
  126. package/src/ports/events.ts +2 -2
  127. package/src/ports/index.ts +0 -1
  128. package/src/providers/index.ts +1 -0
  129. package/src/providers/provider.ts +18 -3
  130. package/src/schedules/index.ts +57 -63
  131. package/src/server/index.ts +4 -2
  132. package/src/server/request-executor.ts +1170 -0
  133. package/src/server/request-preparation.ts +409 -0
  134. package/src/server/response-finalization.ts +385 -0
  135. package/src/server/route-definitions.ts +338 -0
  136. package/src/server/route-matching.ts +64 -0
  137. package/src/server/server.ts +63 -2171
  138. package/src/server/use-case-route.ts +1 -1
  139. package/src/testing/index.ts +2 -0
  140. package/src/tracing/index.ts +1 -0
  141. package/src/uploads/client.ts +4 -7
  142. package/src/uploads/index.ts +70 -12
@@ -1,72 +1,33 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
2
  import {
3
- BEIGNET_ERROR_OWNER_HEADER,
4
- type ContractErrorDefinition,
5
- getContractHeaderSchemas,
6
3
  type HttpContractConfig,
7
4
  methodSupportsRequestBody,
8
- parsePathTemplate,
9
- type StandardSchema,
10
5
  } from "../contracts/index.js";
11
6
  import {
12
7
  comparePathParamsToTemplate,
13
8
  formatPathParamsMismatch,
14
9
  getObjectSchemaShape,
15
10
  } from "../contracts/schema-shape.js";
16
- import {
17
- createErrorResponseBody,
18
- httpErrors,
19
- isAppError,
20
- isErrorResponseBody,
21
- toErrorResponseBody,
22
- } from "../errors/index.js";
23
- import {
24
- IdempotencyConflictError,
25
- IdempotencyInProgressError,
26
- } from "../idempotency/index.js";
27
- import {
28
- type MemoInstrumentationEvent,
29
- runWithMemoScope,
30
- } from "../memo/index.js";
11
+ import { createErrorResponseBody } from "../errors/index.js";
12
+ import { runWithMemoScope } from "../memo/index.js";
31
13
  import type { AnyPorts } from "../ports/index.js";
32
- import {
33
- AuthUnauthorizedError,
34
- EntitlementRequiredError,
35
- GateAuthorizationError,
36
- isUnboundPort,
37
- TenantRequiredError,
38
- } from "../ports/index.js";
39
- import {
40
- createProviderInstrumentation,
41
- type InferProviderPorts,
42
- type ProviderInstrumentationTarget,
43
- type ProviderSetupResult,
44
- resolveProviderInstrumentationPort,
45
- type ServiceProvider,
14
+ import { isUnboundPort } from "../ports/index.js";
15
+ import type {
16
+ InferProviderPorts,
17
+ ProviderSetupResult,
18
+ ServiceProvider,
46
19
  } from "../providers/index.js";
47
- import {
48
- parseTraceparent,
49
- resolveTracingPort,
50
- runWithTracing,
51
- type TraceContextInput,
52
- } from "../tracing/index.js";
53
20
  import type {
54
- ContextSeed,
55
21
  ServerContextConfig,
56
22
  ServiceContextInputArgs,
57
23
  } from "./context.js";
58
24
  import { createContextFinalizer, resolveServerContext } from "./context.js";
59
25
  import type { ContractLike, ResolveContract } from "./contract-like.js";
60
26
  import { resolveContract } from "./contract-like.js";
61
- import { getRequestIdFromContext } from "./hooks/utils.js";
62
27
  import type {
63
- AddedCtxFromHooks,
64
28
  Handler,
65
- HandlerArgs,
66
29
  HttpRequestLike,
67
30
  HttpResponse,
68
- HttpResponseLike,
69
- RequestStageTimings,
70
31
  ResolvedRoute,
71
32
  RouteHook,
72
33
  ServerCaughtErrorHook,
@@ -75,209 +36,54 @@ import type {
75
36
  } from "./http.js";
76
37
  import type { ServerInstrumentationOptions } from "./instrumentation.js";
77
38
  import { createServerInstrumentation } from "./instrumentation.js";
78
- import {
79
- getResponseFinalizerHook,
80
- type ResponseFinalizerResponseOwner,
81
- type ResponseFinalizerValidationState,
82
- } from "./internal-hooks.js";
83
- import {
84
- loadProviderConfig,
85
- parseStandardSchema,
86
- SchemaValidationError,
87
- } from "./providers/index.js";
39
+ import { loadProviderConfig } from "./providers/index.js";
88
40
  import type { ActiveRequestContext } from "./request-context.js";
89
41
  import {
90
42
  enterActiveRequestContext,
91
43
  readContextActor,
92
44
  readContextTenant,
93
45
  runWithActiveRequestContext,
94
- setActiveRequestIdentity,
95
46
  } from "./request-context.js";
47
+ import {
48
+ buildHandler,
49
+ createMemoScopeRecorder,
50
+ createRequestExecutor,
51
+ } from "./request-executor.js";
52
+ import type { RequestBodyOptions } from "./request-preparation.js";
53
+ import { errorResponse } from "./response-finalization.js";
54
+ import {
55
+ contractsFromRoutes,
56
+ createRoutes,
57
+ defineRoutes,
58
+ type HandlerRouteDef,
59
+ type RouteDef,
60
+ type RouteDefinitionBuilder,
61
+ type RouteGroup,
62
+ type RouteGroupBuilder,
63
+ type Routes,
64
+ } from "./route-definitions.js";
65
+ import {
66
+ type CompiledPath,
67
+ compareRouteSpecificity,
68
+ compilePath,
69
+ } from "./route-matching.js";
96
70
  import type { RuntimeIntegrityCheck } from "./runtime-integrity.js";
97
71
  import { runRuntimeIntegrityCheck } from "./runtime-integrity.js";
98
- import type {
99
- AnyUseCaseLike,
100
- AnyUseCaseRouteDef,
101
- UseCaseRouteDef,
102
- ValidatedRouteInputs,
103
- } from "./use-case-route.js";
104
72
  import {
105
73
  createUseCaseRouteHandler,
106
74
  isUseCaseRouteDef,
107
75
  } from "./use-case-route.js";
108
76
 
109
- /**
110
- * Route registration that connects a contract to the handler implementing it.
111
- *
112
- * Most apps keep route definitions in `features/<feature>/routes.ts` and
113
- * compose them with `defineRoutes(...)`.
114
- */
115
- export type HandlerRouteDef<
116
- Ctx,
117
- CLike extends ContractLike = ContractLike,
118
- Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
119
- Ctx,
120
- object
121
- >[],
122
- > = {
123
- /**
124
- * Contract builder or plain contract config for this route.
125
- */
126
- contract: CLike;
127
- /**
128
- * Route-scoped hooks that run after group hooks and before the handler.
129
- */
130
- hooks?: Hooks;
131
- /**
132
- * Handler that implements the contract.
133
- */
134
- handle: Handler<Ctx & AddedCtxFromHooks<Hooks>, ResolveContract<CLike>>;
135
- useCase?: never;
136
- input?: never;
137
- status?: never;
138
- };
139
-
140
- /**
141
- * Route registration for one contract.
142
- *
143
- * Routes either bind the contract directly to a use case (`{ contract,
144
- * useCase }`) or implement a full handler (`{ contract, handle }`). The full
145
- * handler form is the escape hatch for response headers, streaming, native
146
- * `Response` values, and multi-status handling.
147
- */
148
- export type RouteDef<
149
- Ctx,
150
- CLike extends ContractLike = ContractLike,
151
- Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
152
- Ctx,
153
- object
154
- >[],
155
- > = HandlerRouteDef<Ctx, CLike, Hooks> | AnyUseCaseRouteDef<Ctx, CLike, Hooks>;
156
-
157
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
158
- type AnyRouteDef = RouteDef<any, any>;
159
-
160
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
161
- type PlainRouteDef<Ctx> = RouteDef<Ctx, any, readonly []>;
162
-
163
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
164
- type AnyContractRouteDef<Ctx> = RouteDef<Ctx, any>;
165
-
166
- const ROUTE_GROUP_KIND = "beignet.route-group";
167
-
168
- /**
169
- * Named collection of related route registrations.
170
- *
171
- * Route groups colocate feature routes and can apply scoped route hooks to
172
- * every route in the group. `defineRoutes(...)` flattens them before server
173
- * registration while preserving those hooks.
174
- */
175
- export type RouteGroup<
176
- Ctx,
177
- Routes extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
178
- > = {
179
- /**
180
- * Internal marker used by `defineRoutes(...)`.
181
- */
182
- kind: typeof ROUTE_GROUP_KIND;
183
- /**
184
- * Human-readable group name.
185
- */
186
- name: string;
187
- /**
188
- * Hooks applied to every route in this group.
189
- */
190
- hooks?: readonly RouteHook<Ctx, object>[];
191
- /**
192
- * Route definitions in this group.
193
- */
194
- routes: Routes;
195
- };
196
-
197
- type RouteInput<Ctx> =
198
- | AnyContractRouteDef<Ctx>
199
- | AnyRouteDef
200
- | RouteGroup<Ctx, readonly AnyRouteDef[]>;
201
-
202
- type ContextualRouteInput<Ctx> =
203
- | PlainRouteDef<Ctx>
204
- | RouteGroup<Ctx, readonly AnyRouteDef[]>;
205
-
206
- type RouteGroupBuilder<Ctx> = {
207
- <
208
- const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
209
- const R extends readonly PlainRouteDef<
210
- Ctx & AddedCtxFromHooks<GroupHooks>
211
- >[] = readonly PlainRouteDef<Ctx & AddedCtxFromHooks<GroupHooks>>[],
212
- >(group: {
213
- name: string;
214
- hooks?: GroupHooks;
215
- routes: R & ValidatedRouteInputs<Ctx & AddedCtxFromHooks<GroupHooks>, R>;
216
- }): RouteGroup<Ctx, R>;
217
- <
218
- const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
219
- const R extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
220
- >(group: {
221
- name: string;
222
- hooks?: GroupHooks;
223
- routes: R & ValidatedRouteInputs<Ctx & AddedCtxFromHooks<GroupHooks>, R>;
224
- }): RouteGroup<Ctx, R>;
225
- };
226
-
227
- type RoutesFromInput<Input> =
228
- Input extends RouteGroup<infer _Ctx, infer Routes>
229
- ? Routes
230
- : Input extends AnyRouteDef
231
- ? readonly [Input]
232
- : readonly [];
233
-
234
- type FlattenRouteInputs<Inputs extends readonly unknown[]> =
235
- number extends Inputs["length"]
236
- ? readonly AnyRouteDef[]
237
- : Inputs extends readonly [infer First, ...infer Rest]
238
- ? readonly [...RoutesFromInput<First>, ...FlattenRouteInputs<Rest>]
239
- : readonly [];
240
-
241
- type ContractsFromRouteList<
242
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
243
- Routes extends readonly RouteDef<any, any>[],
244
- > = {
245
- readonly [Index in keyof Routes]: Routes[Index] extends RouteDef<
246
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
247
- any,
248
- infer CLike
249
- >
250
- ? ResolveContract<CLike>
251
- : never;
77
+ export type {
78
+ HandlerRouteDef,
79
+ RequestBodyOptions,
80
+ RouteDef,
81
+ RouteDefinitionBuilder,
82
+ RouteGroup,
83
+ RouteGroupBuilder,
84
+ Routes,
252
85
  };
253
-
254
- /**
255
- * Define one route registration with hook-aware handler typing.
256
- *
257
- * Direct route objects are still supported. Use this helper when route-scoped
258
- * hooks enrich `ctx` for a single handler and you want TypeScript to infer the
259
- * added fields.
260
- */
261
- export function defineRoute<Ctx>() {
262
- function define<
263
- CLike extends ContractLike,
264
- UC extends AnyUseCaseLike,
265
- const Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
266
- >(
267
- route: UseCaseRouteDef<Ctx, CLike, UC, Hooks>,
268
- ): UseCaseRouteDef<Ctx, CLike, UC, Hooks>;
269
- function define<
270
- CLike extends ContractLike,
271
- const Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
272
- >(
273
- route: HandlerRouteDef<Ctx, CLike, Hooks>,
274
- ): HandlerRouteDef<Ctx, CLike, Hooks>;
275
- function define(route: unknown): unknown {
276
- return route;
277
- }
278
-
279
- return define;
280
- }
86
+ export { contractsFromRoutes, createRoutes, defineRoutes };
281
87
 
282
88
  /**
283
89
  * Loosely typed provider list element.
@@ -297,18 +103,6 @@ type AnyServiceProvider = ServiceProvider<
297
103
  any
298
104
  >;
299
105
 
300
- /**
301
- * Request body limits enforced before contract body validation.
302
- */
303
- export interface RequestBodyOptions {
304
- /**
305
- * Maximum request body size in bytes for JSON/text route bodies.
306
- *
307
- * @default 1048576
308
- */
309
- maxBytes?: number;
310
- }
311
-
312
106
  /**
313
107
  * Options for creating a Beignet server instance.
314
108
  */
@@ -537,1779 +331,6 @@ export interface ServerInstance<
537
331
  ports: Ports;
538
332
  }
539
333
 
540
- type ExecutionResult<Ctx> = {
541
- ctx?: Ctx;
542
- response: HttpResponse;
543
- error?: unknown;
544
- owner?: ResponseOwner;
545
- };
546
-
547
- type ResponseOwner = ResponseFinalizerResponseOwner;
548
-
549
- type CompiledPath = {
550
- keys: string[];
551
- pattern: RegExp;
552
- segments: ReturnType<typeof parsePathTemplate>["segments"];
553
- normalizedPath: string;
554
- shapeKey: string;
555
- };
556
-
557
- class PathDecodeError extends Error {
558
- constructor() {
559
- super("Malformed URL path");
560
- this.name = "PathDecodeError";
561
- }
562
- }
563
-
564
- function compilePath(path: string) {
565
- const parsed = parsePathTemplate(path);
566
- const regexParts = parsed.segments.map((segment) =>
567
- segment.kind === "dynamic"
568
- ? "([^/]+)"
569
- : segment.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
570
- );
571
- const pattern = new RegExp(`^/${regexParts.join("/")}$`);
572
- return { ...parsed, pattern };
573
- }
574
-
575
- function decodeMatchedParams(
576
- keys: string[],
577
- match: RegExpExecArray,
578
- ): Record<string, string> {
579
- const params: Record<string, string> = {};
580
- try {
581
- keys.forEach((key, index) => {
582
- params[key] = decodeURIComponent(match[index + 1]);
583
- });
584
- } catch (error) {
585
- if (error instanceof URIError) {
586
- throw new PathDecodeError();
587
- }
588
- throw error;
589
- }
590
- return params;
591
- }
592
-
593
- function compareRouteSpecificity(a: CompiledPath, b: CompiledPath): number {
594
- const maxLength = Math.max(a.segments.length, b.segments.length);
595
- for (let index = 0; index < maxLength; index++) {
596
- const aSegment = a.segments[index];
597
- const bSegment = b.segments[index];
598
-
599
- if (!aSegment) return 1;
600
- if (!bSegment) return -1;
601
-
602
- if (aSegment.kind === bSegment.kind) continue;
603
- return aSegment.kind === "static" ? -1 : 1;
604
- }
605
-
606
- return 0;
607
- }
608
-
609
- function errorResponse(
610
- status: number,
611
- code: string,
612
- message: string,
613
- details?: unknown,
614
- ): HttpResponseLike {
615
- return {
616
- status,
617
- body: createErrorResponseBody({ code, message, details }),
618
- };
619
- }
620
-
621
- type RequestValidationLocation = "query" | "path" | "headers" | "body";
622
-
623
- function contractDiagnostics(contract: HttpContractConfig) {
624
- return {
625
- contract: contract.name,
626
- method: contract.method,
627
- path: contract.path,
628
- };
629
- }
630
-
631
- function requestValidationDetails(
632
- contract: HttpContractConfig,
633
- location: RequestValidationLocation,
634
- error?: unknown,
635
- ) {
636
- const details = {
637
- ...contractDiagnostics(contract),
638
- location,
639
- };
640
-
641
- if (error instanceof SchemaValidationError) {
642
- return {
643
- ...details,
644
- issues: error.issues,
645
- };
646
- }
647
-
648
- if (error instanceof Error) {
649
- return {
650
- ...details,
651
- message: error.message,
652
- };
653
- }
654
-
655
- return details;
656
- }
657
-
658
- function requestValidationError(
659
- contract: HttpContractConfig,
660
- status: number,
661
- code: string,
662
- message: string,
663
- location: RequestValidationLocation,
664
- error?: unknown,
665
- ): HttpResponseLike {
666
- return errorResponse(
667
- status,
668
- code,
669
- message,
670
- requestValidationDetails(contract, location, error),
671
- );
672
- }
673
-
674
- function normalizeResponse(res: HttpResponseLike): HttpResponseLike {
675
- return {
676
- status: res.status,
677
- headers: res.headers,
678
- body: res.body,
679
- };
680
- }
681
-
682
- function isWebResponse(value: unknown): value is Response {
683
- return typeof Response !== "undefined" && value instanceof Response;
684
- }
685
-
686
- function normalizeHttpResponse(res: HttpResponse): HttpResponse {
687
- return isWebResponse(res) ? res : normalizeResponse(res);
688
- }
689
-
690
- function withFrameworkErrorOwnerHeader(
691
- res: HttpResponseLike,
692
- owner: ResponseOwner,
693
- ): HttpResponseLike {
694
- if (
695
- owner !== "framework" ||
696
- res.status < 400 ||
697
- !isErrorResponseBody(res.body)
698
- ) {
699
- return res;
700
- }
701
-
702
- return {
703
- ...res,
704
- headers: {
705
- ...(res.headers ?? {}),
706
- [BEIGNET_ERROR_OWNER_HEADER]: "framework",
707
- },
708
- };
709
- }
710
-
711
- function responseOwnerFor(
712
- res: HttpResponse,
713
- owner?: ResponseOwner,
714
- ): ResponseOwner {
715
- if (isWebResponse(res)) return "transport";
716
- return owner ?? "route";
717
- }
718
-
719
- function headersToRecord(headers: Headers): Record<string, string> {
720
- const record: Record<string, string> = {};
721
- headers.forEach((value, key) => {
722
- record[key] = value;
723
- });
724
- return record;
725
- }
726
-
727
- function requestHeadersToRecord(headers: Headers): Record<string, string> {
728
- const record: Record<string, string> = {};
729
- headers.forEach((value, key) => {
730
- record[key.toLowerCase()] = value;
731
- });
732
- return record;
733
- }
734
-
735
- async function parseHeaderSchemas(
736
- schemas: readonly StandardSchema[],
737
- rawHeaders: Record<string, string>,
738
- ): Promise<Record<string, unknown>> {
739
- let parsedHeaders: Record<string, unknown> = rawHeaders;
740
-
741
- for (const schema of schemas) {
742
- const parsed = await parseStandardSchema(schema, rawHeaders);
743
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
744
- parsedHeaders = {
745
- ...parsedHeaders,
746
- ...(parsed as Record<string, unknown>),
747
- };
748
- } else {
749
- parsedHeaders = parsed as Record<string, unknown>;
750
- }
751
- }
752
-
753
- return parsedHeaders;
754
- }
755
-
756
- function responseForHooks(res: HttpResponse): HttpResponseLike {
757
- if (!isWebResponse(res)) {
758
- return normalizeResponse(res);
759
- }
760
-
761
- return {
762
- status: res.status,
763
- headers: headersToRecord(res.headers),
764
- };
765
- }
766
-
767
- /**
768
- * Merge hook-applied header changes onto a native web Response.
769
- *
770
- * Starts from the native response's `Headers` so `set-cookie` multiplicity is
771
- * preserved, then applies headers the beforeSend chain added or changed
772
- * relative to the original headers-only view. The body stream passes through
773
- * untouched; status and statusText are preserved.
774
- */
775
- function mergeNativeResponseHeaders(
776
- nativeResponse: Response,
777
- originalHeaders: Record<string, string>,
778
- finalHeaders: Record<string, string>,
779
- ): Response {
780
- const originalByLowerKey = new Map<string, string>();
781
- for (const [key, value] of Object.entries(originalHeaders)) {
782
- originalByLowerKey.set(key.toLowerCase(), value);
783
- }
784
-
785
- let changed = false;
786
- const merged = new Headers(nativeResponse.headers);
787
- for (const [key, value] of Object.entries(finalHeaders)) {
788
- const lowerKey = key.toLowerCase();
789
- if (originalByLowerKey.get(lowerKey) === value) continue;
790
- changed = true;
791
- if (lowerKey === "set-cookie") {
792
- merged.append(lowerKey, value);
793
- } else {
794
- merged.set(key, value);
795
- }
796
- }
797
-
798
- if (!changed) {
799
- return nativeResponse;
800
- }
801
-
802
- return new Response(nativeResponse.body, {
803
- status: nativeResponse.status,
804
- statusText: nativeResponse.statusText,
805
- headers: merged,
806
- });
807
- }
808
-
809
- function isHttpResponseLike(value: unknown): value is HttpResponseLike {
810
- return (
811
- !isWebResponse(value) &&
812
- typeof value === "object" &&
813
- value !== null &&
814
- "status" in value &&
815
- typeof (value as { status?: unknown }).status === "number"
816
- );
817
- }
818
-
819
- class ResponseContractViolationError extends Error {
820
- readonly code: "RESPONSE_VALIDATION_ERROR" | "UNDECLARED_RESPONSE_STATUS";
821
- readonly details?: unknown;
822
-
823
- constructor(args: {
824
- code: "RESPONSE_VALIDATION_ERROR" | "UNDECLARED_RESPONSE_STATUS";
825
- message: string;
826
- details?: unknown;
827
- }) {
828
- super(args.message);
829
- this.name = "ResponseContractViolationError";
830
- this.code = args.code;
831
- this.details = args.details;
832
- }
833
- }
834
-
835
- function responseContractViolationMessage(
836
- contract: HttpContractConfig,
837
- status: number,
838
- ): string {
839
- return (
840
- `Response validation failed for ${contract.method} ${contract.path} ` +
841
- `(status ${status}, contract: ${contract.name})`
842
- );
843
- }
844
-
845
- function declaredResponseStatuses(contract: HttpContractConfig): number[] {
846
- return Object.keys(contract.responses)
847
- .map((status) => Number(status))
848
- .filter((status) => Number.isFinite(status))
849
- .sort((a, b) => a - b);
850
- }
851
-
852
- function responseContractViolationDetails(
853
- contract: HttpContractConfig,
854
- status: number,
855
- details?: Record<string, unknown>,
856
- ) {
857
- return {
858
- ...contractDiagnostics(contract),
859
- location: "response",
860
- status,
861
- declaredStatuses: declaredResponseStatuses(contract),
862
- ...details,
863
- };
864
- }
865
-
866
- function getDeclaredCatalogErrorsForStatus(
867
- contract: HttpContractConfig,
868
- status: number,
869
- ): ContractErrorDefinition[] {
870
- const errors = contract.metadata?.errors;
871
- if (typeof errors !== "object" || errors === null) return [];
872
-
873
- return Object.values(errors).filter(
874
- (error): error is ContractErrorDefinition =>
875
- typeof error === "object" &&
876
- error !== null &&
877
- typeof (error as { code?: unknown }).code === "string" &&
878
- typeof (error as { status?: unknown }).status === "number" &&
879
- typeof (error as { message?: unknown }).message === "string" &&
880
- (error as { status: number }).status === status,
881
- );
882
- }
883
-
884
- async function validateCatalogErrorResponse<C extends HttpContractConfig>(
885
- contract: C,
886
- res: HttpResponseLike,
887
- ): Promise<void> {
888
- const body = res.body;
889
- if (res.status < 400 || !isErrorResponseBody(body)) return;
890
-
891
- const declaredErrors = getDeclaredCatalogErrorsForStatus(
892
- contract,
893
- res.status,
894
- );
895
- if (declaredErrors.length === 0) return;
896
-
897
- const matchingError = declaredErrors.find(
898
- (error) => error.code === body.code,
899
- );
900
- if (!matchingError) {
901
- throw new ResponseContractViolationError({
902
- code: "RESPONSE_VALIDATION_ERROR",
903
- message: responseContractViolationMessage(contract, res.status),
904
- details: responseContractViolationDetails(contract, res.status, {
905
- issues: [
906
- {
907
- message:
908
- `Error response code "${body.code}" is not declared for status ${res.status}. ` +
909
- `Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
910
- },
911
- ],
912
- }),
913
- });
914
- }
915
-
916
- if (matchingError.details && body.details !== undefined) {
917
- try {
918
- await parseStandardSchema(matchingError.details, body.details);
919
- } catch (error) {
920
- if (error instanceof SchemaValidationError) {
921
- throw new ResponseContractViolationError({
922
- code: "RESPONSE_VALIDATION_ERROR",
923
- message: responseContractViolationMessage(contract, res.status),
924
- details: responseContractViolationDetails(contract, res.status, {
925
- issues: error.issues,
926
- }),
927
- });
928
- }
929
- throw error;
930
- }
931
- }
932
- }
933
-
934
- async function validateResponseAgainstContract<C extends HttpContractConfig>(
935
- contract: C,
936
- res: HttpResponseLike,
937
- responseValidationExemptStatus?: number,
938
- ): Promise<void> {
939
- const statusKey = String(res.status);
940
- const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
941
-
942
- if (!hasDeclaredStatus) {
943
- if (Object.keys(contract.responses).length === 0) return;
944
-
945
- throw new ResponseContractViolationError({
946
- code: "UNDECLARED_RESPONSE_STATUS",
947
- message:
948
- `Handler returned undeclared status ${res.status} for ` +
949
- `${contract.method} ${contract.path} (contract: ${contract.name})`,
950
- details: responseContractViolationDetails(contract, res.status, {
951
- returnedStatus: res.status,
952
- }),
953
- });
954
- }
955
-
956
- const responseSchema = contract.responses[res.status];
957
- if (responseSchema === null) {
958
- if (res.body !== undefined && res.body !== null) {
959
- throw new ResponseContractViolationError({
960
- code: "RESPONSE_VALIDATION_ERROR",
961
- message: responseContractViolationMessage(contract, res.status),
962
- details: responseContractViolationDetails(contract, res.status, {
963
- issues: [
964
- {
965
- message:
966
- "Response body must be empty for a null response schema.",
967
- },
968
- ],
969
- }),
970
- });
971
- }
972
- return;
973
- }
974
-
975
- if (!responseSchema) return;
976
-
977
- // Binder routes whose use case output schema is the same object as the
978
- // declared success response schema skip the redundant success-status parse.
979
- // Error statuses and undeclared statuses are validated unchanged.
980
- if (res.status === responseValidationExemptStatus) return;
981
-
982
- try {
983
- await parseStandardSchema(responseSchema, res.body);
984
- await validateCatalogErrorResponse(contract, res);
985
- } catch (error) {
986
- if (error instanceof SchemaValidationError) {
987
- throw new ResponseContractViolationError({
988
- code: "RESPONSE_VALIDATION_ERROR",
989
- message: responseContractViolationMessage(contract, res.status),
990
- details: responseContractViolationDetails(contract, res.status, {
991
- issues: error.issues,
992
- }),
993
- });
994
- }
995
- throw error;
996
- }
997
- }
998
-
999
- async function finalizeResponse<C extends HttpContractConfig>(
1000
- contract: C,
1001
- res: HttpResponseLike,
1002
- responseValidationExemptStatus?: number,
1003
- ): Promise<HttpResponseLike> {
1004
- const normalized = normalizeResponse(res);
1005
- await validateResponseAgainstContract(
1006
- contract,
1007
- normalized,
1008
- responseValidationExemptStatus,
1009
- );
1010
- return normalized;
1011
- }
1012
-
1013
- function toContractViolationResponse(
1014
- error: ResponseContractViolationError,
1015
- ): HttpResponseLike {
1016
- return {
1017
- status: 500,
1018
- body: createErrorResponseBody({
1019
- code: error.code,
1020
- message: error.message,
1021
- details: error.details,
1022
- }),
1023
- };
1024
- }
1025
-
1026
- function defaultErrorResponse(err: unknown, ctx?: unknown): HttpResponseLike {
1027
- const requestId = getRequestIdFromContext(ctx);
1028
- return {
1029
- status: 500,
1030
- body: createErrorResponseBody({
1031
- code: "INTERNAL_SERVER_ERROR",
1032
- message: "Internal server error",
1033
- requestId,
1034
- details:
1035
- process.env.NODE_ENV !== "production" && err instanceof Error
1036
- ? {
1037
- error: {
1038
- message: err.message,
1039
- stack: err.stack,
1040
- },
1041
- }
1042
- : undefined,
1043
- }),
1044
- };
1045
- }
1046
-
1047
- const DEFAULT_REQUEST_BODY_MAX_BYTES = 1024 * 1024;
1048
-
1049
- class RequestBodyTooLargeError extends Error {
1050
- readonly maxBytes: number;
1051
- readonly actualBytes?: number;
1052
-
1053
- constructor(maxBytes: number, actualBytes?: number) {
1054
- super("Request body exceeds the configured size limit.");
1055
- this.name = "RequestBodyTooLargeError";
1056
- this.maxBytes = maxBytes;
1057
- this.actualBytes = actualBytes;
1058
- }
1059
- }
1060
-
1061
- function requestBodyLimit(options: RequestBodyOptions | undefined): number {
1062
- const maxBytes = options?.maxBytes ?? DEFAULT_REQUEST_BODY_MAX_BYTES;
1063
-
1064
- if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
1065
- throw new Error(
1066
- "createServer requestBody.maxBytes must be a positive number.",
1067
- );
1068
- }
1069
-
1070
- return maxBytes;
1071
- }
1072
-
1073
- function assertContentLengthWithinLimit(
1074
- headers: Headers,
1075
- maxBytes: number,
1076
- ): void {
1077
- const contentLength = headers.get("content-length");
1078
- if (contentLength === null) return;
1079
-
1080
- const actualBytes = Number(contentLength);
1081
- if (!Number.isFinite(actualBytes) || actualBytes < 0) return;
1082
- if (actualBytes > maxBytes) {
1083
- throw new RequestBodyTooLargeError(maxBytes, actualBytes);
1084
- }
1085
- }
1086
-
1087
- async function readLimitedRequestText(
1088
- req: HttpRequestLike,
1089
- maxBytes: number,
1090
- ): Promise<string> {
1091
- assertContentLengthWithinLimit(req.headers, maxBytes);
1092
-
1093
- const body = req.raw?.body;
1094
- if (!body) {
1095
- const text = await req.text();
1096
- const actualBytes = new TextEncoder().encode(text).byteLength;
1097
- if (actualBytes > maxBytes) {
1098
- throw new RequestBodyTooLargeError(maxBytes, actualBytes);
1099
- }
1100
- return text;
1101
- }
1102
-
1103
- const reader = body.getReader();
1104
- const decoder = new TextDecoder();
1105
- let received = 0;
1106
- let text = "";
1107
-
1108
- try {
1109
- while (true) {
1110
- const result = await reader.read();
1111
- if (result.done) break;
1112
- received += result.value.byteLength;
1113
- if (received > maxBytes) {
1114
- throw new RequestBodyTooLargeError(maxBytes, received);
1115
- }
1116
- text += decoder.decode(result.value, { stream: true });
1117
- }
1118
- text += decoder.decode();
1119
- } finally {
1120
- reader.releaseLock();
1121
- }
1122
-
1123
- return text;
1124
- }
1125
-
1126
- async function parseBody(
1127
- req: HttpRequestLike,
1128
- maxBytes: number,
1129
- ): Promise<unknown> {
1130
- const method = req.method.toUpperCase() as HttpContractConfig["method"];
1131
- if (!methodSupportsRequestBody(method)) {
1132
- return undefined;
1133
- }
1134
-
1135
- const bodyReq = req.clone?.() ?? req;
1136
- const contentType = req.headers.get("content-type") || "";
1137
- if (contentType.includes("application/json")) {
1138
- const text = await readLimitedRequestText(bodyReq, maxBytes);
1139
- if (text === "") return undefined;
1140
- return JSON.parse(text);
1141
- }
1142
-
1143
- try {
1144
- const text = await readLimitedRequestText(bodyReq, maxBytes);
1145
- return text === "" ? undefined : text;
1146
- } catch (error) {
1147
- if (error instanceof RequestBodyTooLargeError) throw error;
1148
- return undefined;
1149
- }
1150
- }
1151
-
1152
- type ExecutionTarget<Ctx, C extends HttpContractConfig> = {
1153
- contract: C;
1154
- /**
1155
- * Compiled route pattern. Only required when the executor is invoked
1156
- * without pre-matched params.
1157
- */
1158
- compiled?: CompiledPath;
1159
- handler: Handler<Ctx, C>;
1160
- /**
1161
- * Success status whose response schema validation is skipped because the
1162
- * use case bound to this route already validated its output against the
1163
- * same schema object.
1164
- */
1165
- responseValidationExemptStatus?: number;
1166
- };
1167
-
1168
- /**
1169
- * Build the per-request execution pipeline once.
1170
- *
1171
- * The returned executor takes the contract, compiled pattern, and user handler
1172
- * per invocation so fallback responses (404/405) can reuse a single pipeline
1173
- * across requests instead of rebuilding it per unmatched request.
1174
- */
1175
- function createRequestExecutor<
1176
- Ctx,
1177
- Ports extends AnyPorts,
1178
- C extends HttpContractConfig,
1179
- Providers extends readonly AnyServiceProvider[] = readonly [],
1180
- FinalPorts extends Ports & InferProviderPorts<Providers> = Ports &
1181
- InferProviderPorts<Providers>,
1182
- >(
1183
- // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
1184
- options: CreateServerOptions<Ctx, Ports, any, any, Providers>,
1185
- finalPorts: FinalPorts,
1186
- contextRuntime: {
1187
- createRequestContext: (
1188
- req: HttpRequestLike,
1189
- contract: HttpContractConfig,
1190
- ) => Promise<Ctx>;
1191
- finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
1192
- },
1193
- hooks: ServerHook<Ctx, FinalPorts>[],
1194
- routeHooks: readonly RouteHook<unknown, object>[] = [],
1195
- optionsOverrides?: {
1196
- skipRoutePreparation?: boolean;
1197
- /**
1198
- * Run the route through the full hook pipeline without contract
1199
- * preparation: no query/path/header/body parsing or validation, and the
1200
- * request body is left unconsumed for the handler.
1201
- */
1202
- rawRoute?: boolean;
1203
- },
1204
- ): (
1205
- target: ExecutionTarget<Ctx, C>,
1206
- req: HttpRequestLike,
1207
- preMatchedParams?: Record<string, string>,
1208
- ) => Promise<HttpResponse> {
1209
- const warnedNativeReplacementHooks = new WeakSet<object>();
1210
- const maxRequestBodyBytes = requestBodyLimit(options.requestBody);
1211
-
1212
- const executeRequest = async (
1213
- target: ExecutionTarget<Ctx, C>,
1214
- req: HttpRequestLike,
1215
- preMatchedParams?: Record<string, string>,
1216
- ) => {
1217
- const { contract, handler: userHandler } = target;
1218
- let baseCtx: Ctx | undefined;
1219
- let pathValue: HandlerArgs<Ctx, C>["path"] | undefined;
1220
- let queryValue: HandlerArgs<Ctx, C>["query"] | undefined;
1221
- let headersValue: HandlerArgs<Ctx, C>["headers"] | undefined;
1222
- let bodyValue: HandlerArgs<Ctx, C>["body"] | undefined;
1223
- const startedAt = Date.now();
1224
- // Stage timings accumulate because retries re-enter the send phase and
1225
- // beforeHandle spans the route-hook and server-hook loops.
1226
- const stages: RequestStageTimings = {
1227
- onRequestMs: 0,
1228
- parseMs: 0,
1229
- contextMs: 0,
1230
- beforeHandleMs: 0,
1231
- handlerMs: 0,
1232
- sendMs: 0,
1233
- };
1234
- const timeStage = async <T>(
1235
- stage: keyof RequestStageTimings,
1236
- run: () => Promise<T> | T,
1237
- ): Promise<T> => {
1238
- const stageStartedAt = performance.now();
1239
- try {
1240
- return await run();
1241
- } finally {
1242
- stages[stage] += performance.now() - stageStartedAt;
1243
- }
1244
- };
1245
-
1246
- const resolveErrorResult = async (
1247
- error: unknown,
1248
- ctx?: Ctx,
1249
- path?: HandlerArgs<Ctx, C>["path"],
1250
- query?: HandlerArgs<Ctx, C>["query"],
1251
- headers?: HandlerArgs<Ctx, C>["headers"],
1252
- body?: HandlerArgs<Ctx, C>["body"],
1253
- resultOptions?: {
1254
- owner?: ResponseOwner;
1255
- },
1256
- ): Promise<ExecutionResult<Ctx>> => {
1257
- let currentError = error;
1258
-
1259
- const notifyCaughtError = async (caught: unknown) => {
1260
- const args = {
1261
- err: caught,
1262
- req,
1263
- ctx,
1264
- contract,
1265
- path,
1266
- query,
1267
- headers,
1268
- body,
1269
- };
1270
- for (const hook of hooks) {
1271
- if (!hook.onCaughtError) continue;
1272
- try {
1273
- await hook.onCaughtError(args);
1274
- } catch {
1275
- // Observers must not change response behavior.
1276
- }
1277
- }
1278
- if (options.onCaughtError) {
1279
- try {
1280
- await options.onCaughtError(args);
1281
- } catch {
1282
- // Observers must not change response behavior.
1283
- }
1284
- }
1285
- };
1286
-
1287
- await notifyCaughtError(currentError);
1288
-
1289
- if (currentError instanceof ResponseContractViolationError) {
1290
- return {
1291
- ctx,
1292
- response: toContractViolationResponse(currentError),
1293
- error: currentError,
1294
- owner: "framework",
1295
- };
1296
- }
1297
-
1298
- if (isAppError(currentError)) {
1299
- return {
1300
- ctx,
1301
- response: {
1302
- status: currentError.status,
1303
- ...(currentError.headers
1304
- ? { headers: { ...currentError.headers } }
1305
- : {}),
1306
- body: toErrorResponseBody(currentError),
1307
- },
1308
- error: currentError,
1309
- owner: resultOptions?.owner ?? "route",
1310
- };
1311
- }
1312
-
1313
- if (currentError instanceof AuthUnauthorizedError) {
1314
- return {
1315
- ctx,
1316
- response: errorResponse(401, currentError.code, currentError.message),
1317
- error: currentError,
1318
- owner: "framework",
1319
- };
1320
- }
1321
-
1322
- if (currentError instanceof TenantRequiredError) {
1323
- return {
1324
- ctx,
1325
- response: errorResponse(
1326
- currentError.status,
1327
- currentError.code,
1328
- currentError.message,
1329
- ),
1330
- error: currentError,
1331
- owner: "framework",
1332
- };
1333
- }
1334
-
1335
- if (currentError instanceof IdempotencyConflictError) {
1336
- return {
1337
- ctx,
1338
- response: errorResponse(
1339
- httpErrors.IdempotencyConflict.status,
1340
- httpErrors.IdempotencyConflict.code,
1341
- currentError.message,
1342
- {
1343
- namespace: currentError.namespace,
1344
- key: currentError.key,
1345
- },
1346
- ),
1347
- error: currentError,
1348
- owner: "framework",
1349
- };
1350
- }
1351
-
1352
- if (currentError instanceof IdempotencyInProgressError) {
1353
- return {
1354
- ctx,
1355
- response: errorResponse(
1356
- httpErrors.IdempotencyInProgress.status,
1357
- httpErrors.IdempotencyInProgress.code,
1358
- currentError.message,
1359
- {
1360
- namespace: currentError.namespace,
1361
- key: currentError.key,
1362
- },
1363
- ),
1364
- error: currentError,
1365
- owner: "framework",
1366
- };
1367
- }
1368
-
1369
- if (currentError instanceof GateAuthorizationError) {
1370
- return {
1371
- ctx,
1372
- response: errorResponse(
1373
- currentError.status,
1374
- currentError.code,
1375
- currentError.message,
1376
- currentError.details,
1377
- ),
1378
- error: currentError,
1379
- owner: "framework",
1380
- };
1381
- }
1382
-
1383
- if (currentError instanceof EntitlementRequiredError) {
1384
- return {
1385
- ctx,
1386
- response: errorResponse(
1387
- currentError.status,
1388
- currentError.code,
1389
- currentError.message,
1390
- currentError.details,
1391
- ),
1392
- error: currentError,
1393
- owner: "framework",
1394
- };
1395
- }
1396
-
1397
- for (const hook of hooks) {
1398
- if (!hook.mapUnhandledError) continue;
1399
- try {
1400
- const handled = await hook.mapUnhandledError({
1401
- err: currentError,
1402
- req,
1403
- ctx,
1404
- contract,
1405
- path,
1406
- query,
1407
- headers,
1408
- body,
1409
- });
1410
- if (handled) {
1411
- const response = normalizeHttpResponse(handled);
1412
- return {
1413
- ctx,
1414
- response,
1415
- error: currentError,
1416
- owner: responseOwnerFor(response, "framework"),
1417
- };
1418
- }
1419
- } catch (hookError) {
1420
- currentError = hookError;
1421
- await notifyCaughtError(currentError);
1422
- }
1423
- }
1424
-
1425
- if (options.mapUnhandledError) {
1426
- try {
1427
- const handled = await options.mapUnhandledError({
1428
- err: currentError,
1429
- req,
1430
- ctx,
1431
- contract,
1432
- path,
1433
- query,
1434
- headers,
1435
- body,
1436
- });
1437
- if (handled) {
1438
- const response = normalizeHttpResponse(handled);
1439
- return {
1440
- ctx,
1441
- response,
1442
- error: currentError,
1443
- owner: responseOwnerFor(response, "framework"),
1444
- };
1445
- }
1446
- } catch (hookError) {
1447
- currentError = hookError;
1448
- await notifyCaughtError(currentError);
1449
- }
1450
- }
1451
-
1452
- return {
1453
- ctx,
1454
- response: defaultErrorResponse(currentError, ctx),
1455
- error: currentError,
1456
- owner: "framework",
1457
- };
1458
- };
1459
-
1460
- try {
1461
- const url = new URL(req.url);
1462
-
1463
- let matchedParams: Record<string, string>;
1464
- if (preMatchedParams) {
1465
- matchedParams = preMatchedParams;
1466
- } else {
1467
- const compiled = target.compiled;
1468
- const match = compiled ? compiled.pattern.exec(url.pathname) : null;
1469
- if (
1470
- !compiled ||
1471
- !match ||
1472
- contract.method.toUpperCase() !== req.method.toUpperCase()
1473
- ) {
1474
- return errorResponse(404, "NOT_FOUND", "Not found");
1475
- }
1476
- try {
1477
- matchedParams = decodeMatchedParams(compiled.keys, match);
1478
- } catch (error) {
1479
- if (error instanceof PathDecodeError) {
1480
- return errorResponse(400, "INVALID_PATH", "Malformed URL path");
1481
- }
1482
- throw error;
1483
- }
1484
- }
1485
- const rawHeaders = requestHeadersToRecord(req.headers);
1486
-
1487
- const runNativeBeforeSend = async (
1488
- initialResult: ExecutionResult<Ctx>,
1489
- nativeResponse: Response,
1490
- ): Promise<Response> => {
1491
- const originalView = responseForHooks(nativeResponse);
1492
- const originalHeaders = originalView.headers ?? {};
1493
- let transformed = originalView;
1494
- for (const hook of hooks) {
1495
- if (!hook.beforeSend) continue;
1496
- const nextResponse = await hook.beforeSend({
1497
- req,
1498
- ctx: initialResult.ctx,
1499
- contract,
1500
- path: pathValue,
1501
- query: queryValue,
1502
- headers: headersValue,
1503
- body: bodyValue,
1504
- response: transformed,
1505
- error: initialResult.error,
1506
- native: true,
1507
- });
1508
- if (nextResponse) {
1509
- if (
1510
- (nextResponse.status !== nativeResponse.status ||
1511
- nextResponse.body !== undefined) &&
1512
- !warnedNativeReplacementHooks.has(hook) &&
1513
- process.env.NODE_ENV !== "production"
1514
- ) {
1515
- warnedNativeReplacementHooks.add(hook);
1516
- console.warn(
1517
- `[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.`,
1518
- );
1519
- }
1520
- transformed = {
1521
- status: nativeResponse.status,
1522
- headers: nextResponse.headers,
1523
- };
1524
- }
1525
- }
1526
- return mergeNativeResponseHeaders(
1527
- nativeResponse,
1528
- originalHeaders,
1529
- transformed.headers ?? {},
1530
- );
1531
- };
1532
-
1533
- const applyTransformHooks = async (
1534
- initialResult: ExecutionResult<Ctx>,
1535
- allowRetry: boolean,
1536
- ): Promise<ExecutionResult<Ctx>> => {
1537
- try {
1538
- if (isWebResponse(initialResult.response)) {
1539
- return {
1540
- ...initialResult,
1541
- response: await runNativeBeforeSend(
1542
- initialResult,
1543
- initialResult.response,
1544
- ),
1545
- };
1546
- }
1547
-
1548
- let transformed = normalizeResponse(initialResult.response);
1549
- for (const hook of hooks) {
1550
- if (!hook.beforeSend) continue;
1551
- const nextResponse = await hook.beforeSend({
1552
- req,
1553
- ctx: initialResult.ctx,
1554
- contract,
1555
- path: pathValue,
1556
- query: queryValue,
1557
- headers: headersValue,
1558
- body: bodyValue,
1559
- response: transformed,
1560
- error: initialResult.error,
1561
- });
1562
- if (nextResponse) {
1563
- transformed = normalizeResponse(nextResponse);
1564
- }
1565
- }
1566
- return {
1567
- ...initialResult,
1568
- response: transformed,
1569
- };
1570
- } catch (error) {
1571
- const mapped = await resolveErrorResult(
1572
- error,
1573
- initialResult.ctx,
1574
- pathValue,
1575
- queryValue,
1576
- headersValue,
1577
- bodyValue,
1578
- { owner: "framework" },
1579
- );
1580
- if (!allowRetry) {
1581
- return mapped;
1582
- }
1583
- return applyTransformHooks(mapped, false);
1584
- }
1585
- };
1586
-
1587
- const applyResponseFinalizerHooks = async (
1588
- initialResult: ExecutionResult<Ctx>,
1589
- allowRetry: boolean,
1590
- responseValidation: ResponseFinalizerValidationState,
1591
- ): Promise<ExecutionResult<Ctx>> => {
1592
- const response = normalizeHttpResponse(initialResult.response);
1593
- const native = isWebResponse(response);
1594
- const owner = responseOwnerFor(response, initialResult.owner);
1595
-
1596
- try {
1597
- for (const hook of hooks) {
1598
- const finalizer = getResponseFinalizerHook<Ctx>(hook);
1599
- if (!finalizer) continue;
1600
- await finalizer({
1601
- req,
1602
- ctx: initialResult.ctx,
1603
- contract,
1604
- path: pathValue,
1605
- query: queryValue,
1606
- headers: headersValue,
1607
- body: bodyValue,
1608
- response: responseForHooks(response),
1609
- error: initialResult.error,
1610
- native: native ? true : undefined,
1611
- owner,
1612
- responseValidation,
1613
- });
1614
- }
1615
-
1616
- return {
1617
- ...initialResult,
1618
- response,
1619
- };
1620
- } catch (error) {
1621
- const mapped = await resolveErrorResult(
1622
- error,
1623
- initialResult.ctx,
1624
- pathValue,
1625
- queryValue,
1626
- headersValue,
1627
- bodyValue,
1628
- { owner: "framework" },
1629
- );
1630
- if (!allowRetry) {
1631
- return mapped;
1632
- }
1633
- const transformed = await applyTransformHooks(mapped, true);
1634
- return applyResponseFinalizerHooks(
1635
- transformed,
1636
- false,
1637
- "not-applicable",
1638
- );
1639
- }
1640
- };
1641
-
1642
- let result: ExecutionResult<Ctx> | undefined;
1643
-
1644
- const onRequestStartedAt = performance.now();
1645
- for (const hook of hooks) {
1646
- if (!hook.onRequest) continue;
1647
- try {
1648
- const hookResult = await hook.onRequest({
1649
- req,
1650
- ports: finalPorts,
1651
- contract,
1652
- params: matchedParams,
1653
- });
1654
- if (hookResult) {
1655
- const response = normalizeHttpResponse(hookResult);
1656
- result = {
1657
- response,
1658
- owner: responseOwnerFor(response, "framework"),
1659
- };
1660
- break;
1661
- }
1662
- } catch (error) {
1663
- result = await resolveErrorResult(
1664
- error,
1665
- undefined,
1666
- undefined,
1667
- undefined,
1668
- undefined,
1669
- undefined,
1670
- { owner: "framework" },
1671
- );
1672
- break;
1673
- }
1674
- }
1675
- stages.onRequestMs = performance.now() - onRequestStartedAt;
1676
-
1677
- if (!result) {
1678
- if (optionsOverrides?.skipRoutePreparation) {
1679
- let createdCtx!: Ctx;
1680
- try {
1681
- createdCtx = await timeStage("contextMs", () =>
1682
- contextRuntime.createRequestContext(req, contract),
1683
- );
1684
- baseCtx = createdCtx;
1685
- } catch (error) {
1686
- result = await resolveErrorResult(
1687
- error,
1688
- undefined,
1689
- undefined,
1690
- undefined,
1691
- undefined,
1692
- undefined,
1693
- { owner: "framework" },
1694
- );
1695
- }
1696
-
1697
- if (!result) {
1698
- try {
1699
- result = {
1700
- ctx: createdCtx,
1701
- response: normalizeHttpResponse(
1702
- await timeStage("handlerMs", () =>
1703
- userHandler({
1704
- req,
1705
- ctx: createdCtx,
1706
- contract,
1707
- path: {} as HandlerArgs<Ctx, C>["path"],
1708
- query: {} as HandlerArgs<Ctx, C>["query"],
1709
- headers: rawHeaders as HandlerArgs<Ctx, C>["headers"],
1710
- body: undefined as HandlerArgs<Ctx, C>["body"],
1711
- }),
1712
- ),
1713
- ),
1714
- owner: "framework",
1715
- };
1716
- } catch (error) {
1717
- result = await resolveErrorResult(
1718
- error,
1719
- createdCtx,
1720
- undefined,
1721
- undefined,
1722
- undefined,
1723
- undefined,
1724
- { owner: "framework" },
1725
- );
1726
- }
1727
- }
1728
- } else {
1729
- const parseStartedAt = performance.now();
1730
- const rawQuery: Record<string, string | string[]> = {};
1731
- for (const key of new Set(url.searchParams.keys())) {
1732
- const values = url.searchParams.getAll(key);
1733
- rawQuery[key] = values.length === 1 ? values[0] : values;
1734
- }
1735
-
1736
- // biome-ignore lint/suspicious/noExplicitAny: type is narrowed by schema validation below
1737
- let query: any = rawQuery;
1738
- if (contract.query) {
1739
- try {
1740
- query = await parseStandardSchema(contract.query, query);
1741
- } catch (error) {
1742
- result = {
1743
- response: requestValidationError(
1744
- contract,
1745
- 422,
1746
- "VALIDATION_ERROR",
1747
- "Invalid query parameters",
1748
- "query",
1749
- error,
1750
- ),
1751
- owner: "framework",
1752
- };
1753
- }
1754
- }
1755
-
1756
- // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
1757
- let path: any = matchedParams;
1758
- if (!result && contract.pathParams) {
1759
- try {
1760
- path = await parseStandardSchema(
1761
- contract.pathParams,
1762
- matchedParams,
1763
- );
1764
- } catch (error) {
1765
- result = {
1766
- response: requestValidationError(
1767
- contract,
1768
- 422,
1769
- "VALIDATION_ERROR",
1770
- "Invalid path parameters",
1771
- "path",
1772
- error,
1773
- ),
1774
- owner: "framework",
1775
- };
1776
- }
1777
- }
1778
-
1779
- // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
1780
- let headers: any = rawHeaders;
1781
- const headerSchemas = getContractHeaderSchemas(contract.headers);
1782
- if (!result && headerSchemas.length > 0) {
1783
- try {
1784
- headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
1785
- } catch (error) {
1786
- result = {
1787
- response: requestValidationError(
1788
- contract,
1789
- 422,
1790
- "VALIDATION_ERROR",
1791
- "Invalid request headers",
1792
- "headers",
1793
- error,
1794
- ),
1795
- owner: "framework",
1796
- };
1797
- }
1798
- }
1799
-
1800
- // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
1801
- let body: any;
1802
- // Raw routes own body consumption: the handler reads `req` itself,
1803
- // for example to verify a webhook signature over the exact bytes.
1804
- if (!result && !optionsOverrides?.rawRoute) {
1805
- try {
1806
- body = await parseBody(req, maxRequestBodyBytes);
1807
- } catch (error) {
1808
- if (error instanceof RequestBodyTooLargeError) {
1809
- result = {
1810
- response: requestValidationError(
1811
- contract,
1812
- 413,
1813
- "PAYLOAD_TOO_LARGE",
1814
- "Request body is too large",
1815
- "body",
1816
- error,
1817
- ),
1818
- owner: "framework",
1819
- };
1820
- } else {
1821
- result = {
1822
- response: requestValidationError(
1823
- contract,
1824
- 400,
1825
- "INVALID_BODY",
1826
- "Malformed JSON",
1827
- "body",
1828
- error,
1829
- ),
1830
- owner: "framework",
1831
- };
1832
- }
1833
- }
1834
- }
1835
-
1836
- if (!result && contract.body) {
1837
- try {
1838
- body = await parseStandardSchema(contract.body, body);
1839
- } catch (error) {
1840
- if (
1841
- body === undefined &&
1842
- error instanceof SchemaValidationError
1843
- ) {
1844
- result = {
1845
- response: requestValidationError(
1846
- contract,
1847
- 400,
1848
- "MISSING_BODY",
1849
- "Request body is required",
1850
- "body",
1851
- error,
1852
- ),
1853
- owner: "framework",
1854
- };
1855
- } else {
1856
- result = {
1857
- response: requestValidationError(
1858
- contract,
1859
- 422,
1860
- "VALIDATION_ERROR",
1861
- "Invalid request body",
1862
- "body",
1863
- error,
1864
- ),
1865
- owner: "framework",
1866
- };
1867
- }
1868
- }
1869
- }
1870
-
1871
- stages.parseMs = performance.now() - parseStartedAt;
1872
-
1873
- if (!result) {
1874
- pathValue = path;
1875
- queryValue = query;
1876
- headersValue = headers;
1877
- bodyValue = body;
1878
-
1879
- let createdCtx!: Ctx;
1880
- try {
1881
- createdCtx = await timeStage("contextMs", () =>
1882
- contextRuntime.createRequestContext(req, contract),
1883
- );
1884
- baseCtx = createdCtx;
1885
- } catch (error) {
1886
- result = await resolveErrorResult(
1887
- error,
1888
- undefined,
1889
- pathValue,
1890
- queryValue,
1891
- headersValue,
1892
- bodyValue,
1893
- { owner: "framework" },
1894
- );
1895
- }
1896
-
1897
- if (!result) {
1898
- const baseArgs: HandlerArgs<Ctx, C> = {
1899
- req,
1900
- ctx: createdCtx,
1901
- contract,
1902
- path,
1903
- query,
1904
- headers,
1905
- body,
1906
- };
1907
-
1908
- let currentCtx = createdCtx;
1909
- const beforeHandleStartedAt = performance.now();
1910
- for (const hook of routeHooks) {
1911
- try {
1912
- const additions = await hook.resolve({
1913
- req,
1914
- ctx: currentCtx,
1915
- contract,
1916
- path,
1917
- query,
1918
- headers,
1919
- body,
1920
- });
1921
- if (additions && typeof additions === "object") {
1922
- currentCtx = contextRuntime.finalizeContext({
1923
- ...(currentCtx as object),
1924
- ...additions,
1925
- } as Ctx);
1926
- }
1927
- } catch (error) {
1928
- result = await resolveErrorResult(
1929
- error,
1930
- currentCtx,
1931
- pathValue,
1932
- queryValue,
1933
- headersValue,
1934
- bodyValue,
1935
- { owner: "framework" },
1936
- );
1937
- break;
1938
- }
1939
- }
1940
-
1941
- if (!result) {
1942
- for (const hook of hooks) {
1943
- if (!hook.beforeHandle) continue;
1944
- try {
1945
- const hookResult = await hook.beforeHandle({
1946
- req,
1947
- ctx: currentCtx,
1948
- contract,
1949
- path,
1950
- query,
1951
- headers,
1952
- body,
1953
- });
1954
- if (isWebResponse(hookResult)) {
1955
- result = {
1956
- ctx: currentCtx,
1957
- response: hookResult,
1958
- owner: "transport",
1959
- };
1960
- break;
1961
- }
1962
- if (isHttpResponseLike(hookResult)) {
1963
- result = {
1964
- ctx: currentCtx,
1965
- response: normalizeResponse(hookResult),
1966
- owner: "framework",
1967
- };
1968
- break;
1969
- }
1970
- if (hookResult?.ctx !== undefined) {
1971
- currentCtx = contextRuntime.finalizeContext(
1972
- hookResult.ctx,
1973
- );
1974
- }
1975
- if (hookResult?.response) {
1976
- const response = normalizeHttpResponse(
1977
- hookResult.response,
1978
- );
1979
- result = {
1980
- ctx: currentCtx,
1981
- response,
1982
- owner: responseOwnerFor(response, "framework"),
1983
- };
1984
- break;
1985
- }
1986
- } catch (error) {
1987
- result = await resolveErrorResult(
1988
- error,
1989
- currentCtx,
1990
- pathValue,
1991
- queryValue,
1992
- headersValue,
1993
- bodyValue,
1994
- { owner: "framework" },
1995
- );
1996
- break;
1997
- }
1998
- }
1999
- }
2000
- stages.beforeHandleMs = performance.now() - beforeHandleStartedAt;
2001
-
2002
- if (!result) {
2003
- // Hooks may have elevated the actor or resolved a tenant.
2004
- // Refresh the ambient request context so record-time
2005
- // consumers such as createAmbientAuditLog see the finalized
2006
- // identity.
2007
- setActiveRequestIdentity({
2008
- actor: readContextActor(currentCtx),
2009
- tenant: readContextTenant(currentCtx),
2010
- });
2011
- try {
2012
- result = {
2013
- ctx: currentCtx,
2014
- response: normalizeHttpResponse(
2015
- await timeStage("handlerMs", () =>
2016
- userHandler({ ...baseArgs, ctx: currentCtx }),
2017
- ),
2018
- ),
2019
- };
2020
- } catch (error) {
2021
- result = await resolveErrorResult(
2022
- error,
2023
- currentCtx,
2024
- pathValue,
2025
- queryValue,
2026
- headersValue,
2027
- bodyValue,
2028
- );
2029
- }
2030
- }
2031
- }
2032
- }
2033
- }
2034
- }
2035
-
2036
- const sendStartedAt = performance.now();
2037
- result = await applyTransformHooks(result, true);
2038
-
2039
- let finalResponse = normalizeHttpResponse(result.response);
2040
- let finalError = result.error;
2041
- let finalOwner = responseOwnerFor(finalResponse, result.owner);
2042
- let responseValidation: ResponseFinalizerValidationState =
2043
- "not-applicable";
2044
- if (
2045
- finalOwner === "route" &&
2046
- !isWebResponse(finalResponse) &&
2047
- !(options.validateResponses ?? true)
2048
- ) {
2049
- responseValidation = "disabled";
2050
- } else if (
2051
- finalOwner === "route" &&
2052
- !isWebResponse(finalResponse) &&
2053
- (options.validateResponses ?? true)
2054
- ) {
2055
- try {
2056
- finalResponse = await finalizeResponse(
2057
- contract,
2058
- finalResponse,
2059
- target.responseValidationExemptStatus,
2060
- );
2061
- result = {
2062
- ...result,
2063
- response: finalResponse,
2064
- };
2065
- responseValidation = "validated";
2066
- } catch (error) {
2067
- if (error instanceof ResponseContractViolationError) {
2068
- result = {
2069
- ctx: result.ctx,
2070
- response: toContractViolationResponse(error),
2071
- error,
2072
- owner: "framework",
2073
- };
2074
- finalResponse = normalizeHttpResponse(result.response);
2075
- finalError = result.error;
2076
- finalOwner = responseOwnerFor(finalResponse, result.owner);
2077
- result = await applyTransformHooks(result, true);
2078
- finalResponse = normalizeHttpResponse(result.response);
2079
- finalError = result.error;
2080
- finalOwner = responseOwnerFor(finalResponse, result.owner);
2081
- responseValidation = "not-applicable";
2082
- } else {
2083
- throw error;
2084
- }
2085
- }
2086
- }
2087
-
2088
- result = await applyResponseFinalizerHooks(
2089
- result,
2090
- true,
2091
- responseValidation,
2092
- );
2093
- finalResponse = normalizeHttpResponse(result.response);
2094
- finalError = result.error;
2095
- finalOwner = responseOwnerFor(finalResponse, result.owner);
2096
-
2097
- if (!isWebResponse(finalResponse)) {
2098
- finalResponse = withFrameworkErrorOwnerHeader(
2099
- finalResponse,
2100
- finalOwner,
2101
- );
2102
- }
2103
- stages.sendMs = performance.now() - sendStartedAt;
2104
-
2105
- const durationMs = Date.now() - startedAt;
2106
- const stageTimings = roundStageTimings(stages);
2107
- for (const hook of hooks) {
2108
- if (!hook.afterSend) continue;
2109
- try {
2110
- await hook.afterSend({
2111
- req,
2112
- ctx: result.ctx,
2113
- contract,
2114
- path: pathValue,
2115
- query: queryValue,
2116
- headers: headersValue,
2117
- body: bodyValue,
2118
- response: responseForHooks(finalResponse),
2119
- error: finalError,
2120
- durationMs,
2121
- stages: stageTimings,
2122
- });
2123
- } catch {
2124
- // Ignore after-response hook failures; they should never change the response.
2125
- }
2126
- }
2127
-
2128
- return finalResponse;
2129
- } catch (error) {
2130
- const result = await resolveErrorResult(
2131
- error,
2132
- baseCtx,
2133
- pathValue,
2134
- queryValue,
2135
- headersValue,
2136
- bodyValue,
2137
- {
2138
- owner: "framework",
2139
- },
2140
- );
2141
- const response = normalizeHttpResponse(result.response);
2142
- if (isWebResponse(response)) {
2143
- return response;
2144
- }
2145
- return withFrameworkErrorOwnerHeader(
2146
- response,
2147
- responseOwnerFor(response, result.owner),
2148
- );
2149
- }
2150
- };
2151
-
2152
- return async (
2153
- target: ExecutionTarget<Ctx, C>,
2154
- req: HttpRequestLike,
2155
- preMatchedParams?: Record<string, string>,
2156
- ) => {
2157
- const tracing = resolveTracingPort(finalPorts);
2158
- const instrumentationOptions =
2159
- options.instrumentation === false ? undefined : options.instrumentation;
2160
- const pathname = (() => {
2161
- try {
2162
- return new URL(req.url).pathname;
2163
- } catch {
2164
- return req.url;
2165
- }
2166
- })();
2167
- const ignored = (
2168
- instrumentationOptions?.ignorePaths ?? ["/api/devtools"]
2169
- ).some((prefix) => {
2170
- const normalized = prefix.replace(/\/+$/, "");
2171
- return pathname === normalized || pathname.startsWith(`${normalized}/`);
2172
- });
2173
-
2174
- if (!tracing || ignored) {
2175
- return executeRequest(target, req, preMatchedParams);
2176
- }
2177
-
2178
- const traceContextHeader =
2179
- instrumentationOptions?.traceContextHeader ?? "traceparent";
2180
- const active = tracing.current();
2181
- const parsedTraceparent =
2182
- traceContextHeader === false
2183
- ? undefined
2184
- : parseTraceparent(req.headers.get(traceContextHeader));
2185
- const parent: TraceContextInput | undefined = active
2186
- ? undefined
2187
- : !parsedTraceparent
2188
- ? undefined
2189
- : {
2190
- traceparent: parsedTraceparent.traceparent,
2191
- tracestate: req.headers.get("tracestate") ?? undefined,
2192
- };
2193
- const traceAttributes = {
2194
- "beignet.contract.name": target.contract.name,
2195
- "http.request.method": req.method.toUpperCase(),
2196
- "http.route": target.contract.path,
2197
- } as const;
2198
-
2199
- return await runWithTracing(
2200
- tracing,
2201
- {
2202
- name: `beignet.request ${target.contract.name}`,
2203
- type: "request",
2204
- kind: active ? "internal" : "server",
2205
- parent,
2206
- attributes: traceAttributes,
2207
- metricAttributes: traceAttributes,
2208
- },
2209
- async (span) => {
2210
- const response = await executeRequest(target, req, preMatchedParams);
2211
- span?.setAttribute("http.response.status_code", response.status);
2212
- if (response.status >= 500) span?.setStatus("error");
2213
- return response;
2214
- },
2215
- );
2216
- };
2217
- }
2218
-
2219
- /**
2220
- * Build a memo instrumentation recorder from the app ports, or undefined when
2221
- * no instrumentation sink is wired.
2222
- *
2223
- * Resolved per execution rather than at route-build time because providers
2224
- * contribute `ports.instrumentation`/`ports.devtools` during setup, after
2225
- * routes are registered.
2226
- */
2227
- function createMemoScopeRecorder(
2228
- ports: AnyPorts,
2229
- ): ((event: MemoInstrumentationEvent) => void) | undefined {
2230
- const target = ports as ProviderInstrumentationTarget;
2231
- if (!resolveProviderInstrumentationPort(target)) return undefined;
2232
-
2233
- const instrumentation = createProviderInstrumentation(target, {
2234
- providerName: "memo",
2235
- watcher: "memo",
2236
- });
2237
- return (event) => {
2238
- instrumentation.custom({
2239
- name: `memo.${event.kind}`,
2240
- label: event.kind === "hit" ? "Memo hit" : "Memo fill",
2241
- summary: `Memo ${event.kind} for ${event.memo}`,
2242
- details: {
2243
- memo: event.memo,
2244
- key: event.key,
2245
- ...(event.durationMs !== undefined
2246
- ? { durationMs: event.durationMs }
2247
- : {}),
2248
- ...(event.failed ? { failed: true } : {}),
2249
- },
2250
- });
2251
- };
2252
- }
2253
-
2254
- function buildHandler<
2255
- Ctx,
2256
- Ports extends AnyPorts,
2257
- C extends HttpContractConfig,
2258
- Providers extends readonly AnyServiceProvider[] = readonly [],
2259
- FinalPorts extends Ports & InferProviderPorts<Providers> = Ports &
2260
- InferProviderPorts<Providers>,
2261
- >(
2262
- // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
2263
- options: CreateServerOptions<Ctx, Ports, any, any, Providers>,
2264
- finalPorts: FinalPorts,
2265
- contextRuntime: {
2266
- createRequestContext: (
2267
- req: HttpRequestLike,
2268
- contract: HttpContractConfig,
2269
- ) => Promise<Ctx>;
2270
- finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
2271
- },
2272
- contract: C,
2273
- userHandler: Handler<Ctx, C>,
2274
- hooks: ServerHook<Ctx, FinalPorts>[],
2275
- routeHooks: readonly RouteHook<unknown, object>[] = [],
2276
- optionsOverrides?: {
2277
- skipRoutePreparation?: boolean;
2278
- /**
2279
- * Run the route through the full hook pipeline without contract
2280
- * preparation: no query/path/header/body parsing or validation, and the
2281
- * request body is left unconsumed for the handler.
2282
- */
2283
- rawRoute?: boolean;
2284
- },
2285
- responseValidationExemptStatus?: number,
2286
- ): (
2287
- req: HttpRequestLike,
2288
- preMatchedParams?: Record<string, string>,
2289
- ) => Promise<HttpResponse> {
2290
- const execute = createRequestExecutor<Ctx, Ports, C, Providers, FinalPorts>(
2291
- options,
2292
- finalPorts,
2293
- contextRuntime,
2294
- hooks,
2295
- routeHooks,
2296
- optionsOverrides,
2297
- );
2298
- const executionTarget: ExecutionTarget<Ctx, C> = {
2299
- contract,
2300
- compiled: compilePath(contract.path),
2301
- handler: userHandler,
2302
- responseValidationExemptStatus,
2303
- };
2304
-
2305
- // Every HTTP execution runs inside a fresh memo scope so createMemo(...)
2306
- // wrappers dedupe lookups for exactly one request.
2307
- return (req, preMatchedParams) =>
2308
- runWithMemoScope({ record: createMemoScopeRecorder(finalPorts) }, () =>
2309
- execute(executionTarget, req, preMatchedParams),
2310
- );
2311
- }
2312
-
2313
334
  /**
2314
335
  * Create a Beignet server instance.
2315
336
  *
@@ -2726,9 +747,8 @@ export async function createServer<
2726
747
  // 405s) is assembled per unmatched request.
2727
748
  const executeFallback = createRequestExecutor<
2728
749
  Ctx,
2729
- Ports,
2730
- HttpContractConfig,
2731
- Providers
750
+ FinalPorts,
751
+ HttpContractConfig
2732
752
  >(options, finalPorts, contextRuntime, hooks, [], {
2733
753
  skipRoutePreparation: true,
2734
754
  });
@@ -2762,15 +782,35 @@ export async function createServer<
2762
782
  const method = req.method.toUpperCase();
2763
783
 
2764
784
  let pathMatchedMethods: Set<string> | undefined;
785
+ let headCandidate: RegisteredRoute | undefined;
2765
786
  for (const entry of registry) {
2766
787
  if (!entry.compiled.pattern.test(url.pathname)) continue;
2767
- if (entry.method === method) {
788
+ if (method !== "HEAD" && entry.method === method) {
2768
789
  return await entry.handler(req);
2769
790
  }
2770
791
  if (!pathMatchedMethods) {
2771
792
  pathMatchedMethods = new Set();
2772
793
  }
2773
794
  pathMatchedMethods.add(entry.method);
795
+ if (method === "HEAD" && entry.method === "HEAD") {
796
+ if (
797
+ !headCandidate ||
798
+ (headCandidate.method === "GET" &&
799
+ headCandidate.compiled.shapeKey === entry.compiled.shapeKey)
800
+ ) {
801
+ headCandidate = entry;
802
+ }
803
+ }
804
+ if (entry.method === "GET") {
805
+ pathMatchedMethods.add("HEAD");
806
+ if (method === "HEAD" && !headCandidate) {
807
+ headCandidate = entry;
808
+ }
809
+ }
810
+ }
811
+
812
+ if (headCandidate) {
813
+ return await headCandidate.handler(req);
2774
814
  }
2775
815
 
2776
816
  const pathname = url.pathname || "/";
@@ -2838,18 +878,6 @@ export async function createServer<
2838
878
  };
2839
879
  }
2840
880
 
2841
- function roundStageTimings(stages: RequestStageTimings): RequestStageTimings {
2842
- const round = (value: number) => Math.round(value * 100) / 100;
2843
- return {
2844
- onRequestMs: round(stages.onRequestMs),
2845
- parseMs: round(stages.parseMs),
2846
- contextMs: round(stages.contextMs),
2847
- beforeHandleMs: round(stages.beforeHandleMs),
2848
- handlerMs: round(stages.handlerMs),
2849
- sendMs: round(stages.sendMs),
2850
- };
2851
- }
2852
-
2853
881
  function rawRouteContract(init: RawRouteInit): HttpContractConfig {
2854
882
  return {
2855
883
  kind: "http",
@@ -2863,139 +891,3 @@ function rawRouteContract(init: RawRouteInit): HttpContractConfig {
2863
891
  metadata: init.metadata ?? {},
2864
892
  };
2865
893
  }
2866
-
2867
- /**
2868
- * Define and flatten route registrations with strong type inference.
2869
- *
2870
- * Pass route definitions and route groups here before `createServer(...)`.
2871
- * Group entries are flattened so downstream tooling receives one route list.
2872
- *
2873
- * @example
2874
- * ```ts
2875
- * const routes = defineRoutes<AppContext>([
2876
- * { contract: listPosts, useCase: listPostsUseCase },
2877
- * ]);
2878
- * ```
2879
- */
2880
- export function defineRoutes<
2881
- Ctx,
2882
- const R extends
2883
- readonly ContextualRouteInput<Ctx>[] = readonly ContextualRouteInput<Ctx>[],
2884
- >(routes: R & ValidatedRouteInputs<Ctx, R>): FlattenRouteInputs<R>;
2885
- export function defineRoutes<
2886
- Ctx,
2887
- const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
2888
- >(routes: R & ValidatedRouteInputs<Ctx, R>): FlattenRouteInputs<R>;
2889
- export function defineRoutes<
2890
- Ctx,
2891
- const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
2892
- >(routes: R): FlattenRouteInputs<R> {
2893
- const flattened: RouteDef<Ctx>[] = [];
2894
-
2895
- for (const route of routes) {
2896
- if (isRouteGroup(route)) {
2897
- for (const groupRoute of route.routes) {
2898
- flattened.push({
2899
- ...groupRoute,
2900
- hooks: [...(route.hooks ?? []), ...(groupRoute.hooks ?? [])],
2901
- });
2902
- }
2903
- } else {
2904
- flattened.push(route);
2905
- }
2906
- }
2907
-
2908
- return flattened as unknown as FlattenRouteInputs<R>;
2909
- }
2910
-
2911
- /**
2912
- * Extract contract configs from a route list.
2913
- *
2914
- * Use this to drive clients, OpenAPI, and docs from the same route list passed
2915
- * to `createServer(...)`.
2916
- */
2917
- export function contractsFromRoutes<
2918
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
2919
- const R extends readonly RouteDef<any, any>[],
2920
- >(routes: R): ContractsFromRouteList<R> {
2921
- return routes.map((route) =>
2922
- resolveContract(route.contract),
2923
- ) as unknown as ContractsFromRouteList<R>;
2924
- }
2925
-
2926
- /**
2927
- * Define a named group of related route registrations.
2928
- *
2929
- * Route groups are flattened by defineRoutes, so createServer still receives
2930
- * a regular route list while app code can keep feature route wiring and scoped
2931
- * hooks colocated.
2932
- *
2933
- * @example
2934
- * ```ts
2935
- * const todoRoutes = defineRouteGroup<AppContext>()({
2936
- * name: "todos",
2937
- * hooks: [auth.optional()],
2938
- * routes: [
2939
- * { contract: listTodos, useCase: listTodosUseCase },
2940
- * ]
2941
- * });
2942
- * ```
2943
- */
2944
- export function defineRouteGroup<Ctx>(): RouteGroupBuilder<Ctx>;
2945
- /**
2946
- * @deprecated Prefer `defineRouteGroup<Ctx>()({ ... })`. TypeScript cannot
2947
- * infer the route tuple generic when callers provide only `Ctx`, so the direct
2948
- * form preserves context checking but erases per-route contract output checks.
2949
- */
2950
- export function defineRouteGroup<
2951
- Ctx,
2952
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
2953
- const R extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
2954
- >(group: {
2955
- name: string;
2956
- hooks?: readonly RouteHook<Ctx, object>[];
2957
- routes: R & ValidatedRouteInputs<Ctx, R>;
2958
- }): RouteGroup<Ctx, R>;
2959
- export function defineRouteGroup<
2960
- Ctx,
2961
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
2962
- const R extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
2963
- >(group?: {
2964
- name: string;
2965
- hooks?: readonly RouteHook<Ctx, object>[];
2966
- routes: R;
2967
- }): RouteGroup<Ctx, R> | RouteGroupBuilder<Ctx> {
2968
- const createGroup = <
2969
- const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
2970
- const GroupRoutes extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
2971
- >(input: {
2972
- name: string;
2973
- hooks?: GroupHooks;
2974
- routes: GroupRoutes;
2975
- }): RouteGroup<Ctx, GroupRoutes> => ({
2976
- kind: ROUTE_GROUP_KIND,
2977
- name: input.name,
2978
- hooks: input.hooks,
2979
- routes: input.routes,
2980
- });
2981
-
2982
- if (!group) {
2983
- return createGroup;
2984
- }
2985
-
2986
- return {
2987
- kind: ROUTE_GROUP_KIND,
2988
- name: group.name,
2989
- hooks: group.hooks,
2990
- routes: group.routes,
2991
- };
2992
- }
2993
-
2994
- function isRouteGroup<Ctx>(route: RouteInput<Ctx>): route is RouteGroup<Ctx> {
2995
- return (
2996
- typeof route === "object" &&
2997
- route !== null &&
2998
- "kind" in route &&
2999
- route.kind === ROUTE_GROUP_KIND
3000
- );
3001
- }