@beignet/core 0.0.34 → 0.0.36

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 (120) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +145 -15
  3. package/dist/agent-capabilities/index.d.ts +197 -0
  4. package/dist/agent-capabilities/index.d.ts.map +1 -0
  5. package/dist/agent-capabilities/index.js +282 -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 -3
  16. package/dist/contracts/contract-builder.d.ts.map +1 -1
  17. package/dist/contracts/contract-builder.js +3 -12
  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/idempotency/index.d.ts.map +1 -1
  35. package/dist/idempotency/index.js +14 -9
  36. package/dist/idempotency/index.js.map +1 -1
  37. package/dist/jobs/index.d.ts.map +1 -1
  38. package/dist/jobs/index.js +10 -4
  39. package/dist/jobs/index.js.map +1 -1
  40. package/dist/openapi/index.js +15 -8
  41. package/dist/openapi/index.js.map +1 -1
  42. package/dist/ports/auth.d.ts +2 -0
  43. package/dist/ports/auth.d.ts.map +1 -1
  44. package/dist/ports/auth.js +2 -0
  45. package/dist/ports/auth.js.map +1 -1
  46. package/dist/providers/index.d.ts +1 -1
  47. package/dist/providers/index.d.ts.map +1 -1
  48. package/dist/providers/index.js.map +1 -1
  49. package/dist/providers/provider.d.ts +9 -1
  50. package/dist/providers/provider.d.ts.map +1 -1
  51. package/dist/providers/provider.js.map +1 -1
  52. package/dist/schedules/index.d.ts +6 -13
  53. package/dist/schedules/index.d.ts.map +1 -1
  54. package/dist/schedules/index.js +24 -44
  55. package/dist/schedules/index.js.map +1 -1
  56. package/dist/server/index.d.ts +2 -2
  57. package/dist/server/index.d.ts.map +1 -1
  58. package/dist/server/index.js +1 -1
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/request-executor.d.ts +72 -0
  61. package/dist/server/request-executor.d.ts.map +1 -0
  62. package/dist/server/request-executor.js +781 -0
  63. package/dist/server/request-executor.js.map +1 -0
  64. package/dist/server/request-preparation.d.ts +39 -0
  65. package/dist/server/request-preparation.d.ts.map +1 -0
  66. package/dist/server/request-preparation.js +248 -0
  67. package/dist/server/request-preparation.js.map +1 -0
  68. package/dist/server/response-finalization.d.ts +34 -0
  69. package/dist/server/response-finalization.d.ts.map +1 -0
  70. package/dist/server/response-finalization.js +279 -0
  71. package/dist/server/response-finalization.js.map +1 -0
  72. package/dist/server/route-definitions.d.ts +139 -0
  73. package/dist/server/route-definitions.d.ts.map +1 -0
  74. package/dist/server/route-definitions.js +94 -0
  75. package/dist/server/route-definitions.js.map +1 -0
  76. package/dist/server/route-matching.d.ts +15 -0
  77. package/dist/server/route-matching.d.ts.map +1 -0
  78. package/dist/server/route-matching.js +46 -0
  79. package/dist/server/route-matching.js.map +1 -0
  80. package/dist/server/server.d.ts +6 -147
  81. package/dist/server/server.d.ts.map +1 -1
  82. package/dist/server/server.js +29 -1350
  83. package/dist/server/server.js.map +1 -1
  84. package/dist/server/use-case-route.d.ts +1 -1
  85. package/dist/tracing/index.d.ts +1 -1
  86. package/dist/tracing/index.d.ts.map +1 -1
  87. package/dist/tracing/index.js.map +1 -1
  88. package/dist/uploads/client.js +4 -7
  89. package/dist/uploads/client.js.map +1 -1
  90. package/dist/uploads/index.d.ts.map +1 -1
  91. package/dist/uploads/index.js +59 -12
  92. package/dist/uploads/index.js.map +1 -1
  93. package/package.json +5 -1
  94. package/skills/app-architecture/SKILL.md +27 -1
  95. package/src/agent-capabilities/index.ts +643 -0
  96. package/src/application/index.ts +19 -5
  97. package/src/client/client.ts +41 -17
  98. package/src/contracts/contract-builder.ts +8 -16
  99. package/src/contracts/contract-group.ts +4 -6
  100. package/src/contracts/metadata.ts +20 -0
  101. package/src/contracts/types.ts +39 -0
  102. package/src/events/index.ts +16 -20
  103. package/src/idempotency/index.ts +17 -9
  104. package/src/jobs/index.ts +8 -3
  105. package/src/openapi/index.ts +24 -12
  106. package/src/ports/auth.ts +2 -0
  107. package/src/providers/index.ts +1 -0
  108. package/src/providers/provider.ts +17 -2
  109. package/src/schedules/index.ts +57 -63
  110. package/src/server/index.ts +4 -2
  111. package/src/server/request-executor.ts +1170 -0
  112. package/src/server/request-preparation.ts +409 -0
  113. package/src/server/response-finalization.ts +385 -0
  114. package/src/server/route-definitions.ts +338 -0
  115. package/src/server/route-matching.ts +64 -0
  116. package/src/server/server.ts +63 -2139
  117. package/src/server/use-case-route.ts +1 -1
  118. package/src/tracing/index.ts +1 -0
  119. package/src/uploads/client.ts +4 -7
  120. package/src/uploads/index.ts +70 -12
@@ -1,1288 +1,20 @@
1
- import { BEIGNET_ERROR_OWNER_HEADER, getContractHeaderSchemas, methodSupportsRequestBody, parsePathTemplate, } from "../contracts/index.js";
1
+ import { methodSupportsRequestBody, } from "../contracts/index.js";
2
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 { runWithMemoScope, } from "../memo/index.js";
6
- import { AuthUnauthorizedError, EntitlementRequiredError, GateAuthorizationError, isUnboundPort, TenantRequiredError, } from "../ports/index.js";
7
- import { createProviderInstrumentation, resolveProviderInstrumentationPort, } from "../providers/index.js";
8
- import { parseTraceparent, resolveTracingPort, runWithTracing, } from "../tracing/index.js";
3
+ import { createErrorResponseBody } from "../errors/index.js";
4
+ import { runWithMemoScope } from "../memo/index.js";
5
+ import { isUnboundPort } from "../ports/index.js";
9
6
  import { createContextFinalizer, resolveServerContext } from "./context.js";
10
7
  import { resolveContract } from "./contract-like.js";
11
- import { getRequestIdFromContext } from "./hooks/utils.js";
12
8
  import { createServerInstrumentation } from "./instrumentation.js";
13
- import { getResponseFinalizerHook, } from "./internal-hooks.js";
14
- import { loadProviderConfig, parseStandardSchema, SchemaValidationError, } from "./providers/index.js";
15
- import { enterActiveRequestContext, readContextActor, readContextTenant, runWithActiveRequestContext, setActiveRequestIdentity, } from "./request-context.js";
9
+ import { loadProviderConfig } from "./providers/index.js";
10
+ import { enterActiveRequestContext, readContextActor, readContextTenant, runWithActiveRequestContext, } from "./request-context.js";
11
+ import { buildHandler, createMemoScopeRecorder, createRequestExecutor, } from "./request-executor.js";
12
+ import { errorResponse } from "./response-finalization.js";
13
+ import { contractsFromRoutes, createRoutes, defineRoutes, } from "./route-definitions.js";
14
+ import { compareRouteSpecificity, compilePath, } from "./route-matching.js";
16
15
  import { runRuntimeIntegrityCheck } from "./runtime-integrity.js";
17
16
  import { createUseCaseRouteHandler, isUseCaseRouteDef, } from "./use-case-route.js";
18
- const ROUTE_GROUP_KIND = "beignet.route-group";
19
- /**
20
- * Define one route registration with hook-aware handler typing.
21
- *
22
- * Direct route objects are still supported. Use this helper when route-scoped
23
- * hooks enrich `ctx` for a single handler and you want TypeScript to infer the
24
- * added fields.
25
- */
26
- export function defineRoute() {
27
- function define(route) {
28
- return route;
29
- }
30
- return define;
31
- }
32
- class PathDecodeError extends Error {
33
- constructor() {
34
- super("Malformed URL path");
35
- this.name = "PathDecodeError";
36
- }
37
- }
38
- function compilePath(path) {
39
- const parsed = parsePathTemplate(path);
40
- const regexParts = parsed.segments.map((segment) => segment.kind === "dynamic"
41
- ? "([^/]+)"
42
- : segment.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
43
- const pattern = new RegExp(`^/${regexParts.join("/")}$`);
44
- return { ...parsed, pattern };
45
- }
46
- function decodeMatchedParams(keys, match) {
47
- const params = {};
48
- try {
49
- keys.forEach((key, index) => {
50
- params[key] = decodeURIComponent(match[index + 1]);
51
- });
52
- }
53
- catch (error) {
54
- if (error instanceof URIError) {
55
- throw new PathDecodeError();
56
- }
57
- throw error;
58
- }
59
- return params;
60
- }
61
- function compareRouteSpecificity(a, b) {
62
- const maxLength = Math.max(a.segments.length, b.segments.length);
63
- for (let index = 0; index < maxLength; index++) {
64
- const aSegment = a.segments[index];
65
- const bSegment = b.segments[index];
66
- if (!aSegment)
67
- return 1;
68
- if (!bSegment)
69
- return -1;
70
- if (aSegment.kind === bSegment.kind)
71
- continue;
72
- return aSegment.kind === "static" ? -1 : 1;
73
- }
74
- return 0;
75
- }
76
- function errorResponse(status, code, message, details) {
77
- return {
78
- status,
79
- body: createErrorResponseBody({ code, message, details }),
80
- };
81
- }
82
- function contractDiagnostics(contract) {
83
- return {
84
- contract: contract.name,
85
- method: contract.method,
86
- path: contract.path,
87
- };
88
- }
89
- function requestValidationDetails(contract, location, error) {
90
- const details = {
91
- ...contractDiagnostics(contract),
92
- location,
93
- };
94
- if (error instanceof SchemaValidationError) {
95
- return {
96
- ...details,
97
- issues: error.issues,
98
- };
99
- }
100
- if (error instanceof Error) {
101
- return {
102
- ...details,
103
- message: error.message,
104
- };
105
- }
106
- return details;
107
- }
108
- function requestValidationError(contract, status, code, message, location, error) {
109
- return errorResponse(status, code, message, requestValidationDetails(contract, location, error));
110
- }
111
- function normalizeResponse(res) {
112
- return {
113
- status: res.status,
114
- headers: res.headers,
115
- body: res.body,
116
- };
117
- }
118
- function isWebResponse(value) {
119
- return typeof Response !== "undefined" && value instanceof Response;
120
- }
121
- function normalizeHttpResponse(res) {
122
- return isWebResponse(res) ? res : normalizeResponse(res);
123
- }
124
- function withFrameworkErrorOwnerHeader(res, owner) {
125
- if (owner !== "framework" ||
126
- res.status < 400 ||
127
- !isErrorResponseBody(res.body)) {
128
- return res;
129
- }
130
- return {
131
- ...res,
132
- headers: {
133
- ...(res.headers ?? {}),
134
- [BEIGNET_ERROR_OWNER_HEADER]: "framework",
135
- },
136
- };
137
- }
138
- function responseOwnerFor(res, owner) {
139
- if (isWebResponse(res))
140
- return "transport";
141
- return owner ?? "route";
142
- }
143
- function headersToRecord(headers) {
144
- const record = {};
145
- headers.forEach((value, key) => {
146
- record[key] = value;
147
- });
148
- return record;
149
- }
150
- function requestHeadersToRecord(headers) {
151
- const record = {};
152
- headers.forEach((value, key) => {
153
- record[key.toLowerCase()] = value;
154
- });
155
- return record;
156
- }
157
- async function parseHeaderSchemas(schemas, rawHeaders) {
158
- let parsedHeaders = rawHeaders;
159
- for (const schema of schemas) {
160
- const parsed = await parseStandardSchema(schema, rawHeaders);
161
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
162
- parsedHeaders = {
163
- ...parsedHeaders,
164
- ...parsed,
165
- };
166
- }
167
- else {
168
- parsedHeaders = parsed;
169
- }
170
- }
171
- return parsedHeaders;
172
- }
173
- function responseForHooks(res) {
174
- if (!isWebResponse(res)) {
175
- return normalizeResponse(res);
176
- }
177
- return {
178
- status: res.status,
179
- headers: headersToRecord(res.headers),
180
- };
181
- }
182
- /**
183
- * Merge hook-applied header changes onto a native web Response.
184
- *
185
- * Starts from the native response's `Headers` so `set-cookie` multiplicity is
186
- * preserved, then applies headers the beforeSend chain added or changed
187
- * relative to the original headers-only view. The body stream passes through
188
- * untouched; status and statusText are preserved.
189
- */
190
- function mergeNativeResponseHeaders(nativeResponse, originalHeaders, finalHeaders) {
191
- const originalByLowerKey = new Map();
192
- for (const [key, value] of Object.entries(originalHeaders)) {
193
- originalByLowerKey.set(key.toLowerCase(), value);
194
- }
195
- let changed = false;
196
- const merged = new Headers(nativeResponse.headers);
197
- for (const [key, value] of Object.entries(finalHeaders)) {
198
- const lowerKey = key.toLowerCase();
199
- if (originalByLowerKey.get(lowerKey) === value)
200
- continue;
201
- changed = true;
202
- if (lowerKey === "set-cookie") {
203
- merged.append(lowerKey, value);
204
- }
205
- else {
206
- merged.set(key, value);
207
- }
208
- }
209
- if (!changed) {
210
- return nativeResponse;
211
- }
212
- return new Response(nativeResponse.body, {
213
- status: nativeResponse.status,
214
- statusText: nativeResponse.statusText,
215
- headers: merged,
216
- });
217
- }
218
- function isHttpResponseLike(value) {
219
- return (!isWebResponse(value) &&
220
- typeof value === "object" &&
221
- value !== null &&
222
- "status" in value &&
223
- typeof value.status === "number");
224
- }
225
- class ResponseContractViolationError extends Error {
226
- code;
227
- details;
228
- constructor(args) {
229
- super(args.message);
230
- this.name = "ResponseContractViolationError";
231
- this.code = args.code;
232
- this.details = args.details;
233
- }
234
- }
235
- function responseContractViolationMessage(contract, status) {
236
- return (`Response validation failed for ${contract.method} ${contract.path} ` +
237
- `(status ${status}, contract: ${contract.name})`);
238
- }
239
- function declaredResponseStatuses(contract) {
240
- return Object.keys(contract.responses)
241
- .map((status) => Number(status))
242
- .filter((status) => Number.isFinite(status))
243
- .sort((a, b) => a - b);
244
- }
245
- function responseContractViolationDetails(contract, status, details) {
246
- return {
247
- ...contractDiagnostics(contract),
248
- location: "response",
249
- status,
250
- declaredStatuses: declaredResponseStatuses(contract),
251
- ...details,
252
- };
253
- }
254
- function getDeclaredCatalogErrorsForStatus(contract, status) {
255
- const errors = contract.metadata?.errors;
256
- if (typeof errors !== "object" || errors === null)
257
- return [];
258
- return Object.values(errors).filter((error) => typeof error === "object" &&
259
- error !== null &&
260
- typeof error.code === "string" &&
261
- typeof error.status === "number" &&
262
- typeof error.message === "string" &&
263
- error.status === status);
264
- }
265
- async function validateCatalogErrorResponse(contract, res) {
266
- const body = res.body;
267
- if (res.status < 400 || !isErrorResponseBody(body))
268
- return;
269
- const declaredErrors = getDeclaredCatalogErrorsForStatus(contract, res.status);
270
- if (declaredErrors.length === 0)
271
- return;
272
- const matchingError = declaredErrors.find((error) => error.code === body.code);
273
- if (!matchingError) {
274
- throw new ResponseContractViolationError({
275
- code: "RESPONSE_VALIDATION_ERROR",
276
- message: responseContractViolationMessage(contract, res.status),
277
- details: responseContractViolationDetails(contract, res.status, {
278
- issues: [
279
- {
280
- message: `Error response code "${body.code}" is not declared for status ${res.status}. ` +
281
- `Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
282
- },
283
- ],
284
- }),
285
- });
286
- }
287
- if (matchingError.details && body.details !== undefined) {
288
- try {
289
- await parseStandardSchema(matchingError.details, body.details);
290
- }
291
- catch (error) {
292
- if (error instanceof SchemaValidationError) {
293
- throw new ResponseContractViolationError({
294
- code: "RESPONSE_VALIDATION_ERROR",
295
- message: responseContractViolationMessage(contract, res.status),
296
- details: responseContractViolationDetails(contract, res.status, {
297
- issues: error.issues,
298
- }),
299
- });
300
- }
301
- throw error;
302
- }
303
- }
304
- }
305
- async function validateResponseAgainstContract(contract, res, responseValidationExemptStatus) {
306
- const statusKey = String(res.status);
307
- const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
308
- if (!hasDeclaredStatus) {
309
- if (Object.keys(contract.responses).length === 0)
310
- return;
311
- throw new ResponseContractViolationError({
312
- code: "UNDECLARED_RESPONSE_STATUS",
313
- message: `Handler returned undeclared status ${res.status} for ` +
314
- `${contract.method} ${contract.path} (contract: ${contract.name})`,
315
- details: responseContractViolationDetails(contract, res.status, {
316
- returnedStatus: res.status,
317
- }),
318
- });
319
- }
320
- const responseSchema = contract.responses[res.status];
321
- if (responseSchema === null) {
322
- if (res.body !== undefined && res.body !== null) {
323
- throw new ResponseContractViolationError({
324
- code: "RESPONSE_VALIDATION_ERROR",
325
- message: responseContractViolationMessage(contract, res.status),
326
- details: responseContractViolationDetails(contract, res.status, {
327
- issues: [
328
- {
329
- message: "Response body must be empty for a null response schema.",
330
- },
331
- ],
332
- }),
333
- });
334
- }
335
- return;
336
- }
337
- if (!responseSchema)
338
- return;
339
- // Binder routes whose use case output schema is the same object as the
340
- // declared success response schema skip the redundant success-status parse.
341
- // Error statuses and undeclared statuses are validated unchanged.
342
- if (res.status === responseValidationExemptStatus)
343
- return;
344
- try {
345
- await parseStandardSchema(responseSchema, res.body);
346
- await validateCatalogErrorResponse(contract, res);
347
- }
348
- catch (error) {
349
- if (error instanceof SchemaValidationError) {
350
- throw new ResponseContractViolationError({
351
- code: "RESPONSE_VALIDATION_ERROR",
352
- message: responseContractViolationMessage(contract, res.status),
353
- details: responseContractViolationDetails(contract, res.status, {
354
- issues: error.issues,
355
- }),
356
- });
357
- }
358
- throw error;
359
- }
360
- }
361
- async function finalizeResponse(contract, res, responseValidationExemptStatus) {
362
- const normalized = normalizeResponse(res);
363
- await validateResponseAgainstContract(contract, normalized, responseValidationExemptStatus);
364
- return normalized;
365
- }
366
- function toContractViolationResponse(error) {
367
- return {
368
- status: 500,
369
- body: createErrorResponseBody({
370
- code: error.code,
371
- message: error.message,
372
- details: error.details,
373
- }),
374
- };
375
- }
376
- function defaultErrorResponse(err, ctx) {
377
- const requestId = getRequestIdFromContext(ctx);
378
- return {
379
- status: 500,
380
- body: createErrorResponseBody({
381
- code: "INTERNAL_SERVER_ERROR",
382
- message: "Internal server error",
383
- requestId,
384
- details: process.env.NODE_ENV !== "production" && err instanceof Error
385
- ? {
386
- error: {
387
- message: err.message,
388
- stack: err.stack,
389
- },
390
- }
391
- : undefined,
392
- }),
393
- };
394
- }
395
- const DEFAULT_REQUEST_BODY_MAX_BYTES = 1024 * 1024;
396
- class RequestBodyTooLargeError extends Error {
397
- maxBytes;
398
- actualBytes;
399
- constructor(maxBytes, actualBytes) {
400
- super("Request body exceeds the configured size limit.");
401
- this.name = "RequestBodyTooLargeError";
402
- this.maxBytes = maxBytes;
403
- this.actualBytes = actualBytes;
404
- }
405
- }
406
- function requestBodyLimit(options) {
407
- const maxBytes = options?.maxBytes ?? DEFAULT_REQUEST_BODY_MAX_BYTES;
408
- if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
409
- throw new Error("createServer requestBody.maxBytes must be a positive number.");
410
- }
411
- return maxBytes;
412
- }
413
- function assertContentLengthWithinLimit(headers, maxBytes) {
414
- const contentLength = headers.get("content-length");
415
- if (contentLength === null)
416
- return;
417
- const actualBytes = Number(contentLength);
418
- if (!Number.isFinite(actualBytes) || actualBytes < 0)
419
- return;
420
- if (actualBytes > maxBytes) {
421
- throw new RequestBodyTooLargeError(maxBytes, actualBytes);
422
- }
423
- }
424
- async function readLimitedRequestText(req, maxBytes) {
425
- assertContentLengthWithinLimit(req.headers, maxBytes);
426
- const body = req.raw?.body;
427
- if (!body) {
428
- const text = await req.text();
429
- const actualBytes = new TextEncoder().encode(text).byteLength;
430
- if (actualBytes > maxBytes) {
431
- throw new RequestBodyTooLargeError(maxBytes, actualBytes);
432
- }
433
- return text;
434
- }
435
- const reader = body.getReader();
436
- const decoder = new TextDecoder();
437
- let received = 0;
438
- let text = "";
439
- try {
440
- while (true) {
441
- const result = await reader.read();
442
- if (result.done)
443
- break;
444
- received += result.value.byteLength;
445
- if (received > maxBytes) {
446
- throw new RequestBodyTooLargeError(maxBytes, received);
447
- }
448
- text += decoder.decode(result.value, { stream: true });
449
- }
450
- text += decoder.decode();
451
- }
452
- finally {
453
- reader.releaseLock();
454
- }
455
- return text;
456
- }
457
- async function parseBody(req, maxBytes) {
458
- const method = req.method.toUpperCase();
459
- if (!methodSupportsRequestBody(method)) {
460
- return undefined;
461
- }
462
- const bodyReq = req.clone?.() ?? req;
463
- const contentType = req.headers.get("content-type") || "";
464
- if (contentType.includes("application/json")) {
465
- const text = await readLimitedRequestText(bodyReq, maxBytes);
466
- if (text === "")
467
- return undefined;
468
- return JSON.parse(text);
469
- }
470
- try {
471
- const text = await readLimitedRequestText(bodyReq, maxBytes);
472
- return text === "" ? undefined : text;
473
- }
474
- catch (error) {
475
- if (error instanceof RequestBodyTooLargeError)
476
- throw error;
477
- return undefined;
478
- }
479
- }
480
- /**
481
- * Build the per-request execution pipeline once.
482
- *
483
- * The returned executor takes the contract, compiled pattern, and user handler
484
- * per invocation so fallback responses (404/405) can reuse a single pipeline
485
- * across requests instead of rebuilding it per unmatched request.
486
- */
487
- function createRequestExecutor(
488
- // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
489
- options, finalPorts, contextRuntime, hooks, routeHooks = [], optionsOverrides) {
490
- const warnedNativeReplacementHooks = new WeakSet();
491
- const maxRequestBodyBytes = requestBodyLimit(options.requestBody);
492
- const executeRequest = async (target, req, preMatchedParams) => {
493
- const { contract, handler: userHandler } = target;
494
- let baseCtx;
495
- let pathValue;
496
- let queryValue;
497
- let headersValue;
498
- let bodyValue;
499
- const startedAt = Date.now();
500
- // Stage timings accumulate because retries re-enter the send phase and
501
- // beforeHandle spans the route-hook and server-hook loops.
502
- const stages = {
503
- onRequestMs: 0,
504
- parseMs: 0,
505
- contextMs: 0,
506
- beforeHandleMs: 0,
507
- handlerMs: 0,
508
- sendMs: 0,
509
- };
510
- const timeStage = async (stage, run) => {
511
- const stageStartedAt = performance.now();
512
- try {
513
- return await run();
514
- }
515
- finally {
516
- stages[stage] += performance.now() - stageStartedAt;
517
- }
518
- };
519
- const resolveErrorResult = async (error, ctx, path, query, headers, body, resultOptions) => {
520
- let currentError = error;
521
- const notifyCaughtError = async (caught) => {
522
- const args = {
523
- err: caught,
524
- req,
525
- ctx,
526
- contract,
527
- path,
528
- query,
529
- headers,
530
- body,
531
- };
532
- for (const hook of hooks) {
533
- if (!hook.onCaughtError)
534
- continue;
535
- try {
536
- await hook.onCaughtError(args);
537
- }
538
- catch {
539
- // Observers must not change response behavior.
540
- }
541
- }
542
- if (options.onCaughtError) {
543
- try {
544
- await options.onCaughtError(args);
545
- }
546
- catch {
547
- // Observers must not change response behavior.
548
- }
549
- }
550
- };
551
- await notifyCaughtError(currentError);
552
- if (currentError instanceof ResponseContractViolationError) {
553
- return {
554
- ctx,
555
- response: toContractViolationResponse(currentError),
556
- error: currentError,
557
- owner: "framework",
558
- };
559
- }
560
- if (isAppError(currentError)) {
561
- return {
562
- ctx,
563
- response: {
564
- status: currentError.status,
565
- ...(currentError.headers
566
- ? { headers: { ...currentError.headers } }
567
- : {}),
568
- body: toErrorResponseBody(currentError),
569
- },
570
- error: currentError,
571
- owner: resultOptions?.owner ?? "route",
572
- };
573
- }
574
- if (currentError instanceof AuthUnauthorizedError) {
575
- return {
576
- ctx,
577
- response: errorResponse(401, currentError.code, currentError.message),
578
- error: currentError,
579
- owner: "framework",
580
- };
581
- }
582
- if (currentError instanceof TenantRequiredError) {
583
- return {
584
- ctx,
585
- response: errorResponse(currentError.status, currentError.code, currentError.message),
586
- error: currentError,
587
- owner: "framework",
588
- };
589
- }
590
- if (currentError instanceof IdempotencyConflictError) {
591
- return {
592
- ctx,
593
- response: errorResponse(httpErrors.IdempotencyConflict.status, httpErrors.IdempotencyConflict.code, currentError.message, {
594
- namespace: currentError.namespace,
595
- key: currentError.key,
596
- }),
597
- error: currentError,
598
- owner: "framework",
599
- };
600
- }
601
- if (currentError instanceof IdempotencyInProgressError) {
602
- return {
603
- ctx,
604
- response: errorResponse(httpErrors.IdempotencyInProgress.status, httpErrors.IdempotencyInProgress.code, currentError.message, {
605
- namespace: currentError.namespace,
606
- key: currentError.key,
607
- }),
608
- error: currentError,
609
- owner: "framework",
610
- };
611
- }
612
- if (currentError instanceof GateAuthorizationError) {
613
- return {
614
- ctx,
615
- response: errorResponse(currentError.status, currentError.code, currentError.message, currentError.details),
616
- error: currentError,
617
- owner: "framework",
618
- };
619
- }
620
- if (currentError instanceof EntitlementRequiredError) {
621
- return {
622
- ctx,
623
- response: errorResponse(currentError.status, currentError.code, currentError.message, currentError.details),
624
- error: currentError,
625
- owner: "framework",
626
- };
627
- }
628
- for (const hook of hooks) {
629
- if (!hook.mapUnhandledError)
630
- continue;
631
- try {
632
- const handled = await hook.mapUnhandledError({
633
- err: currentError,
634
- req,
635
- ctx,
636
- contract,
637
- path,
638
- query,
639
- headers,
640
- body,
641
- });
642
- if (handled) {
643
- const response = normalizeHttpResponse(handled);
644
- return {
645
- ctx,
646
- response,
647
- error: currentError,
648
- owner: responseOwnerFor(response, "framework"),
649
- };
650
- }
651
- }
652
- catch (hookError) {
653
- currentError = hookError;
654
- await notifyCaughtError(currentError);
655
- }
656
- }
657
- if (options.mapUnhandledError) {
658
- try {
659
- const handled = await options.mapUnhandledError({
660
- err: currentError,
661
- req,
662
- ctx,
663
- contract,
664
- path,
665
- query,
666
- headers,
667
- body,
668
- });
669
- if (handled) {
670
- const response = normalizeHttpResponse(handled);
671
- return {
672
- ctx,
673
- response,
674
- error: currentError,
675
- owner: responseOwnerFor(response, "framework"),
676
- };
677
- }
678
- }
679
- catch (hookError) {
680
- currentError = hookError;
681
- await notifyCaughtError(currentError);
682
- }
683
- }
684
- return {
685
- ctx,
686
- response: defaultErrorResponse(currentError, ctx),
687
- error: currentError,
688
- owner: "framework",
689
- };
690
- };
691
- try {
692
- const url = new URL(req.url);
693
- let matchedParams;
694
- if (preMatchedParams) {
695
- matchedParams = preMatchedParams;
696
- }
697
- else {
698
- const compiled = target.compiled;
699
- const match = compiled ? compiled.pattern.exec(url.pathname) : null;
700
- if (!compiled ||
701
- !match ||
702
- contract.method.toUpperCase() !== req.method.toUpperCase()) {
703
- return errorResponse(404, "NOT_FOUND", "Not found");
704
- }
705
- try {
706
- matchedParams = decodeMatchedParams(compiled.keys, match);
707
- }
708
- catch (error) {
709
- if (error instanceof PathDecodeError) {
710
- return errorResponse(400, "INVALID_PATH", "Malformed URL path");
711
- }
712
- throw error;
713
- }
714
- }
715
- const rawHeaders = requestHeadersToRecord(req.headers);
716
- const runNativeBeforeSend = async (initialResult, nativeResponse) => {
717
- const originalView = responseForHooks(nativeResponse);
718
- const originalHeaders = originalView.headers ?? {};
719
- let transformed = originalView;
720
- for (const hook of hooks) {
721
- if (!hook.beforeSend)
722
- continue;
723
- const nextResponse = await hook.beforeSend({
724
- req,
725
- ctx: initialResult.ctx,
726
- contract,
727
- path: pathValue,
728
- query: queryValue,
729
- headers: headersValue,
730
- body: bodyValue,
731
- response: transformed,
732
- error: initialResult.error,
733
- native: true,
734
- });
735
- if (nextResponse) {
736
- if ((nextResponse.status !== nativeResponse.status ||
737
- nextResponse.body !== undefined) &&
738
- !warnedNativeReplacementHooks.has(hook) &&
739
- process.env.NODE_ENV !== "production") {
740
- warnedNativeReplacementHooks.add(hook);
741
- 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.`);
742
- }
743
- transformed = {
744
- status: nativeResponse.status,
745
- headers: nextResponse.headers,
746
- };
747
- }
748
- }
749
- return mergeNativeResponseHeaders(nativeResponse, originalHeaders, transformed.headers ?? {});
750
- };
751
- const applyTransformHooks = async (initialResult, allowRetry) => {
752
- try {
753
- if (isWebResponse(initialResult.response)) {
754
- return {
755
- ...initialResult,
756
- response: await runNativeBeforeSend(initialResult, initialResult.response),
757
- };
758
- }
759
- let transformed = normalizeResponse(initialResult.response);
760
- for (const hook of hooks) {
761
- if (!hook.beforeSend)
762
- continue;
763
- const nextResponse = await hook.beforeSend({
764
- req,
765
- ctx: initialResult.ctx,
766
- contract,
767
- path: pathValue,
768
- query: queryValue,
769
- headers: headersValue,
770
- body: bodyValue,
771
- response: transformed,
772
- error: initialResult.error,
773
- });
774
- if (nextResponse) {
775
- transformed = normalizeResponse(nextResponse);
776
- }
777
- }
778
- return {
779
- ...initialResult,
780
- response: transformed,
781
- };
782
- }
783
- catch (error) {
784
- const mapped = await resolveErrorResult(error, initialResult.ctx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
785
- if (!allowRetry) {
786
- return mapped;
787
- }
788
- return applyTransformHooks(mapped, false);
789
- }
790
- };
791
- const applyResponseFinalizerHooks = async (initialResult, allowRetry, responseValidation) => {
792
- const response = normalizeHttpResponse(initialResult.response);
793
- const native = isWebResponse(response);
794
- const owner = responseOwnerFor(response, initialResult.owner);
795
- try {
796
- for (const hook of hooks) {
797
- const finalizer = getResponseFinalizerHook(hook);
798
- if (!finalizer)
799
- continue;
800
- await finalizer({
801
- req,
802
- ctx: initialResult.ctx,
803
- contract,
804
- path: pathValue,
805
- query: queryValue,
806
- headers: headersValue,
807
- body: bodyValue,
808
- response: responseForHooks(response),
809
- error: initialResult.error,
810
- native: native ? true : undefined,
811
- owner,
812
- responseValidation,
813
- });
814
- }
815
- return {
816
- ...initialResult,
817
- response,
818
- };
819
- }
820
- catch (error) {
821
- const mapped = await resolveErrorResult(error, initialResult.ctx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
822
- if (!allowRetry) {
823
- return mapped;
824
- }
825
- const transformed = await applyTransformHooks(mapped, true);
826
- return applyResponseFinalizerHooks(transformed, false, "not-applicable");
827
- }
828
- };
829
- let result;
830
- const onRequestStartedAt = performance.now();
831
- for (const hook of hooks) {
832
- if (!hook.onRequest)
833
- continue;
834
- try {
835
- const hookResult = await hook.onRequest({
836
- req,
837
- ports: finalPorts,
838
- contract,
839
- params: matchedParams,
840
- });
841
- if (hookResult) {
842
- const response = normalizeHttpResponse(hookResult);
843
- result = {
844
- response,
845
- owner: responseOwnerFor(response, "framework"),
846
- };
847
- break;
848
- }
849
- }
850
- catch (error) {
851
- result = await resolveErrorResult(error, undefined, undefined, undefined, undefined, undefined, { owner: "framework" });
852
- break;
853
- }
854
- }
855
- stages.onRequestMs = performance.now() - onRequestStartedAt;
856
- if (!result) {
857
- if (optionsOverrides?.skipRoutePreparation) {
858
- let createdCtx;
859
- try {
860
- createdCtx = await timeStage("contextMs", () => contextRuntime.createRequestContext(req, contract));
861
- baseCtx = createdCtx;
862
- }
863
- catch (error) {
864
- result = await resolveErrorResult(error, undefined, undefined, undefined, undefined, undefined, { owner: "framework" });
865
- }
866
- if (!result) {
867
- try {
868
- result = {
869
- ctx: createdCtx,
870
- response: normalizeHttpResponse(await timeStage("handlerMs", () => userHandler({
871
- req,
872
- ctx: createdCtx,
873
- contract,
874
- path: {},
875
- query: {},
876
- headers: rawHeaders,
877
- body: undefined,
878
- }))),
879
- owner: "framework",
880
- };
881
- }
882
- catch (error) {
883
- result = await resolveErrorResult(error, createdCtx, undefined, undefined, undefined, undefined, { owner: "framework" });
884
- }
885
- }
886
- }
887
- else {
888
- const parseStartedAt = performance.now();
889
- const rawQuery = {};
890
- for (const key of new Set(url.searchParams.keys())) {
891
- const values = url.searchParams.getAll(key);
892
- rawQuery[key] = values.length === 1 ? values[0] : values;
893
- }
894
- // biome-ignore lint/suspicious/noExplicitAny: type is narrowed by schema validation below
895
- let query = rawQuery;
896
- if (contract.query) {
897
- try {
898
- query = await parseStandardSchema(contract.query, query);
899
- }
900
- catch (error) {
901
- result = {
902
- response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid query parameters", "query", error),
903
- owner: "framework",
904
- };
905
- }
906
- }
907
- // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
908
- let path = matchedParams;
909
- if (!result && contract.pathParams) {
910
- try {
911
- path = await parseStandardSchema(contract.pathParams, matchedParams);
912
- }
913
- catch (error) {
914
- result = {
915
- response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid path parameters", "path", error),
916
- owner: "framework",
917
- };
918
- }
919
- }
920
- // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
921
- let headers = rawHeaders;
922
- const headerSchemas = getContractHeaderSchemas(contract.headers);
923
- if (!result && headerSchemas.length > 0) {
924
- try {
925
- headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
926
- }
927
- catch (error) {
928
- result = {
929
- response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request headers", "headers", error),
930
- owner: "framework",
931
- };
932
- }
933
- }
934
- // biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
935
- let body;
936
- // Raw routes own body consumption: the handler reads `req` itself,
937
- // for example to verify a webhook signature over the exact bytes.
938
- if (!result && !optionsOverrides?.rawRoute) {
939
- try {
940
- body = await parseBody(req, maxRequestBodyBytes);
941
- }
942
- catch (error) {
943
- if (error instanceof RequestBodyTooLargeError) {
944
- result = {
945
- response: requestValidationError(contract, 413, "PAYLOAD_TOO_LARGE", "Request body is too large", "body", error),
946
- owner: "framework",
947
- };
948
- }
949
- else {
950
- result = {
951
- response: requestValidationError(contract, 400, "INVALID_BODY", "Malformed JSON", "body", error),
952
- owner: "framework",
953
- };
954
- }
955
- }
956
- }
957
- if (!result && contract.body) {
958
- try {
959
- body = await parseStandardSchema(contract.body, body);
960
- }
961
- catch (error) {
962
- if (body === undefined &&
963
- error instanceof SchemaValidationError) {
964
- result = {
965
- response: requestValidationError(contract, 400, "MISSING_BODY", "Request body is required", "body", error),
966
- owner: "framework",
967
- };
968
- }
969
- else {
970
- result = {
971
- response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request body", "body", error),
972
- owner: "framework",
973
- };
974
- }
975
- }
976
- }
977
- stages.parseMs = performance.now() - parseStartedAt;
978
- if (!result) {
979
- pathValue = path;
980
- queryValue = query;
981
- headersValue = headers;
982
- bodyValue = body;
983
- let createdCtx;
984
- try {
985
- createdCtx = await timeStage("contextMs", () => contextRuntime.createRequestContext(req, contract));
986
- baseCtx = createdCtx;
987
- }
988
- catch (error) {
989
- result = await resolveErrorResult(error, undefined, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
990
- }
991
- if (!result) {
992
- const baseArgs = {
993
- req,
994
- ctx: createdCtx,
995
- contract,
996
- path,
997
- query,
998
- headers,
999
- body,
1000
- };
1001
- let currentCtx = createdCtx;
1002
- const beforeHandleStartedAt = performance.now();
1003
- for (const hook of routeHooks) {
1004
- try {
1005
- const additions = await hook.resolve({
1006
- req,
1007
- ctx: currentCtx,
1008
- contract,
1009
- path,
1010
- query,
1011
- headers,
1012
- body,
1013
- });
1014
- if (additions && typeof additions === "object") {
1015
- currentCtx = contextRuntime.finalizeContext({
1016
- ...currentCtx,
1017
- ...additions,
1018
- });
1019
- }
1020
- }
1021
- catch (error) {
1022
- result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
1023
- break;
1024
- }
1025
- }
1026
- if (!result) {
1027
- for (const hook of hooks) {
1028
- if (!hook.beforeHandle)
1029
- continue;
1030
- try {
1031
- const hookResult = await hook.beforeHandle({
1032
- req,
1033
- ctx: currentCtx,
1034
- contract,
1035
- path,
1036
- query,
1037
- headers,
1038
- body,
1039
- });
1040
- if (isWebResponse(hookResult)) {
1041
- result = {
1042
- ctx: currentCtx,
1043
- response: hookResult,
1044
- owner: "transport",
1045
- };
1046
- break;
1047
- }
1048
- if (isHttpResponseLike(hookResult)) {
1049
- result = {
1050
- ctx: currentCtx,
1051
- response: normalizeResponse(hookResult),
1052
- owner: "framework",
1053
- };
1054
- break;
1055
- }
1056
- if (hookResult?.ctx !== undefined) {
1057
- currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
1058
- }
1059
- if (hookResult?.response) {
1060
- const response = normalizeHttpResponse(hookResult.response);
1061
- result = {
1062
- ctx: currentCtx,
1063
- response,
1064
- owner: responseOwnerFor(response, "framework"),
1065
- };
1066
- break;
1067
- }
1068
- }
1069
- catch (error) {
1070
- result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
1071
- break;
1072
- }
1073
- }
1074
- }
1075
- stages.beforeHandleMs = performance.now() - beforeHandleStartedAt;
1076
- if (!result) {
1077
- // Hooks may have elevated the actor or resolved a tenant.
1078
- // Refresh the ambient request context so record-time
1079
- // consumers such as createAmbientAuditLog see the finalized
1080
- // identity.
1081
- setActiveRequestIdentity({
1082
- actor: readContextActor(currentCtx),
1083
- tenant: readContextTenant(currentCtx),
1084
- });
1085
- try {
1086
- result = {
1087
- ctx: currentCtx,
1088
- response: normalizeHttpResponse(await timeStage("handlerMs", () => userHandler({ ...baseArgs, ctx: currentCtx }))),
1089
- };
1090
- }
1091
- catch (error) {
1092
- result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue);
1093
- }
1094
- }
1095
- }
1096
- }
1097
- }
1098
- }
1099
- const sendStartedAt = performance.now();
1100
- result = await applyTransformHooks(result, true);
1101
- let finalResponse = normalizeHttpResponse(result.response);
1102
- let finalError = result.error;
1103
- let finalOwner = responseOwnerFor(finalResponse, result.owner);
1104
- let responseValidation = "not-applicable";
1105
- if (finalOwner === "route" &&
1106
- !isWebResponse(finalResponse) &&
1107
- !(options.validateResponses ?? true)) {
1108
- responseValidation = "disabled";
1109
- }
1110
- else if (finalOwner === "route" &&
1111
- !isWebResponse(finalResponse) &&
1112
- (options.validateResponses ?? true)) {
1113
- try {
1114
- finalResponse = await finalizeResponse(contract, finalResponse, target.responseValidationExemptStatus);
1115
- result = {
1116
- ...result,
1117
- response: finalResponse,
1118
- };
1119
- responseValidation = "validated";
1120
- }
1121
- catch (error) {
1122
- if (error instanceof ResponseContractViolationError) {
1123
- result = {
1124
- ctx: result.ctx,
1125
- response: toContractViolationResponse(error),
1126
- error,
1127
- owner: "framework",
1128
- };
1129
- finalResponse = normalizeHttpResponse(result.response);
1130
- finalError = result.error;
1131
- finalOwner = responseOwnerFor(finalResponse, result.owner);
1132
- result = await applyTransformHooks(result, true);
1133
- finalResponse = normalizeHttpResponse(result.response);
1134
- finalError = result.error;
1135
- finalOwner = responseOwnerFor(finalResponse, result.owner);
1136
- responseValidation = "not-applicable";
1137
- }
1138
- else {
1139
- throw error;
1140
- }
1141
- }
1142
- }
1143
- result = await applyResponseFinalizerHooks(result, true, responseValidation);
1144
- finalResponse = normalizeHttpResponse(result.response);
1145
- finalError = result.error;
1146
- finalOwner = responseOwnerFor(finalResponse, result.owner);
1147
- if (!isWebResponse(finalResponse)) {
1148
- finalResponse = withFrameworkErrorOwnerHeader(finalResponse, finalOwner);
1149
- }
1150
- stages.sendMs = performance.now() - sendStartedAt;
1151
- const durationMs = Date.now() - startedAt;
1152
- const stageTimings = roundStageTimings(stages);
1153
- for (const hook of hooks) {
1154
- if (!hook.afterSend)
1155
- continue;
1156
- try {
1157
- await hook.afterSend({
1158
- req,
1159
- ctx: result.ctx,
1160
- contract,
1161
- path: pathValue,
1162
- query: queryValue,
1163
- headers: headersValue,
1164
- body: bodyValue,
1165
- response: responseForHooks(finalResponse),
1166
- error: finalError,
1167
- durationMs,
1168
- stages: stageTimings,
1169
- });
1170
- }
1171
- catch {
1172
- // Ignore after-response hook failures; they should never change the response.
1173
- }
1174
- }
1175
- return finalResponse;
1176
- }
1177
- catch (error) {
1178
- const result = await resolveErrorResult(error, baseCtx, pathValue, queryValue, headersValue, bodyValue, {
1179
- owner: "framework",
1180
- });
1181
- const response = normalizeHttpResponse(result.response);
1182
- if (isWebResponse(response)) {
1183
- return response;
1184
- }
1185
- return withFrameworkErrorOwnerHeader(response, responseOwnerFor(response, result.owner));
1186
- }
1187
- };
1188
- return async (target, req, preMatchedParams) => {
1189
- const tracing = resolveTracingPort(finalPorts);
1190
- const instrumentationOptions = options.instrumentation === false ? undefined : options.instrumentation;
1191
- const pathname = (() => {
1192
- try {
1193
- return new URL(req.url).pathname;
1194
- }
1195
- catch {
1196
- return req.url;
1197
- }
1198
- })();
1199
- const ignored = (instrumentationOptions?.ignorePaths ?? ["/api/devtools"]).some((prefix) => {
1200
- const normalized = prefix.replace(/\/+$/, "");
1201
- return pathname === normalized || pathname.startsWith(`${normalized}/`);
1202
- });
1203
- if (!tracing || ignored) {
1204
- return executeRequest(target, req, preMatchedParams);
1205
- }
1206
- const traceContextHeader = instrumentationOptions?.traceContextHeader ?? "traceparent";
1207
- const active = tracing.current();
1208
- const parsedTraceparent = traceContextHeader === false
1209
- ? undefined
1210
- : parseTraceparent(req.headers.get(traceContextHeader));
1211
- const parent = active
1212
- ? undefined
1213
- : !parsedTraceparent
1214
- ? undefined
1215
- : {
1216
- traceparent: parsedTraceparent.traceparent,
1217
- tracestate: req.headers.get("tracestate") ?? undefined,
1218
- };
1219
- const traceAttributes = {
1220
- "beignet.contract.name": target.contract.name,
1221
- "http.request.method": req.method.toUpperCase(),
1222
- "http.route": target.contract.path,
1223
- };
1224
- return await runWithTracing(tracing, {
1225
- name: `beignet.request ${target.contract.name}`,
1226
- type: "request",
1227
- kind: active ? "internal" : "server",
1228
- parent,
1229
- attributes: traceAttributes,
1230
- metricAttributes: traceAttributes,
1231
- }, async (span) => {
1232
- const response = await executeRequest(target, req, preMatchedParams);
1233
- span?.setAttribute("http.response.status_code", response.status);
1234
- if (response.status >= 500)
1235
- span?.setStatus("error");
1236
- return response;
1237
- });
1238
- };
1239
- }
1240
- /**
1241
- * Build a memo instrumentation recorder from the app ports, or undefined when
1242
- * no instrumentation sink is wired.
1243
- *
1244
- * Resolved per execution rather than at route-build time because providers
1245
- * contribute `ports.instrumentation`/`ports.devtools` during setup, after
1246
- * routes are registered.
1247
- */
1248
- function createMemoScopeRecorder(ports) {
1249
- const target = ports;
1250
- if (!resolveProviderInstrumentationPort(target))
1251
- return undefined;
1252
- const instrumentation = createProviderInstrumentation(target, {
1253
- providerName: "memo",
1254
- watcher: "memo",
1255
- });
1256
- return (event) => {
1257
- instrumentation.custom({
1258
- name: `memo.${event.kind}`,
1259
- label: event.kind === "hit" ? "Memo hit" : "Memo fill",
1260
- summary: `Memo ${event.kind} for ${event.memo}`,
1261
- details: {
1262
- memo: event.memo,
1263
- key: event.key,
1264
- ...(event.durationMs !== undefined
1265
- ? { durationMs: event.durationMs }
1266
- : {}),
1267
- ...(event.failed ? { failed: true } : {}),
1268
- },
1269
- });
1270
- };
1271
- }
1272
- function buildHandler(
1273
- // biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
1274
- options, finalPorts, contextRuntime, contract, userHandler, hooks, routeHooks = [], optionsOverrides, responseValidationExemptStatus) {
1275
- const execute = createRequestExecutor(options, finalPorts, contextRuntime, hooks, routeHooks, optionsOverrides);
1276
- const executionTarget = {
1277
- contract,
1278
- compiled: compilePath(contract.path),
1279
- handler: userHandler,
1280
- responseValidationExemptStatus,
1281
- };
1282
- // Every HTTP execution runs inside a fresh memo scope so createMemo(...)
1283
- // wrappers dedupe lookups for exactly one request.
1284
- return (req, preMatchedParams) => runWithMemoScope({ record: createMemoScopeRecorder(finalPorts) }, () => execute(executionTarget, req, preMatchedParams));
1285
- }
17
+ export { contractsFromRoutes, createRoutes, defineRoutes };
1286
18
  /**
1287
19
  * Create a Beignet server instance.
1288
20
  *
@@ -1594,16 +326,33 @@ export async function createServer(options) {
1594
326
  const url = new URL(req.url);
1595
327
  const method = req.method.toUpperCase();
1596
328
  let pathMatchedMethods;
329
+ let headCandidate;
1597
330
  for (const entry of registry) {
1598
331
  if (!entry.compiled.pattern.test(url.pathname))
1599
332
  continue;
1600
- if (entry.method === method) {
333
+ if (method !== "HEAD" && entry.method === method) {
1601
334
  return await entry.handler(req);
1602
335
  }
1603
336
  if (!pathMatchedMethods) {
1604
337
  pathMatchedMethods = new Set();
1605
338
  }
1606
339
  pathMatchedMethods.add(entry.method);
340
+ if (method === "HEAD" && entry.method === "HEAD") {
341
+ if (!headCandidate ||
342
+ (headCandidate.method === "GET" &&
343
+ headCandidate.compiled.shapeKey === entry.compiled.shapeKey)) {
344
+ headCandidate = entry;
345
+ }
346
+ }
347
+ if (entry.method === "GET") {
348
+ pathMatchedMethods.add("HEAD");
349
+ if (method === "HEAD" && !headCandidate) {
350
+ headCandidate = entry;
351
+ }
352
+ }
353
+ }
354
+ if (headCandidate) {
355
+ return await headCandidate.handler(req);
1607
356
  }
1608
357
  const pathname = url.pathname || "/";
1609
358
  if (pathMatchedMethods) {
@@ -1648,17 +397,6 @@ export async function createServer(options) {
1648
397
  ports: finalPorts,
1649
398
  };
1650
399
  }
1651
- function roundStageTimings(stages) {
1652
- const round = (value) => Math.round(value * 100) / 100;
1653
- return {
1654
- onRequestMs: round(stages.onRequestMs),
1655
- parseMs: round(stages.parseMs),
1656
- contextMs: round(stages.contextMs),
1657
- beforeHandleMs: round(stages.beforeHandleMs),
1658
- handlerMs: round(stages.handlerMs),
1659
- sendMs: round(stages.sendMs),
1660
- };
1661
- }
1662
400
  function rawRouteContract(init) {
1663
401
  return {
1664
402
  kind: "http",
@@ -1672,63 +410,4 @@ function rawRouteContract(init) {
1672
410
  metadata: init.metadata ?? {},
1673
411
  };
1674
412
  }
1675
- export function defineRoutes(routes) {
1676
- const flattened = [];
1677
- for (const route of routes) {
1678
- if (isRouteGroup(route)) {
1679
- for (const groupRoute of route.routes) {
1680
- flattened.push({
1681
- ...groupRoute,
1682
- hooks: [...(route.hooks ?? []), ...(groupRoute.hooks ?? [])],
1683
- });
1684
- }
1685
- }
1686
- else {
1687
- flattened.push(route);
1688
- }
1689
- }
1690
- return flattened;
1691
- }
1692
- /**
1693
- * Extract contract configs from a route list.
1694
- *
1695
- * Use this to drive clients, OpenAPI, and docs from the same route list passed
1696
- * to `createServer(...)`.
1697
- */
1698
- export function contractsFromRoutes(routes) {
1699
- return routes.map((route) => resolveContract(route.contract));
1700
- }
1701
- /**
1702
- * Define a named group of related route registrations.
1703
- *
1704
- * Route groups are flattened by defineRoutes, so createServer still receives
1705
- * a regular route list while app code can keep feature route wiring and scoped
1706
- * hooks colocated.
1707
- *
1708
- * @example
1709
- * ```ts
1710
- * const todoRoutes = defineRouteGroup<AppContext>()({
1711
- * name: "todos",
1712
- * hooks: [auth.optional()],
1713
- * routes: [
1714
- * { contract: listTodos, useCase: listTodosUseCase },
1715
- * ]
1716
- * });
1717
- * ```
1718
- */
1719
- export function defineRouteGroup() {
1720
- const createGroup = (input) => ({
1721
- kind: ROUTE_GROUP_KIND,
1722
- name: input.name,
1723
- hooks: input.hooks,
1724
- routes: input.routes,
1725
- });
1726
- return createGroup;
1727
- }
1728
- function isRouteGroup(route) {
1729
- return (typeof route === "object" &&
1730
- route !== null &&
1731
- "kind" in route &&
1732
- route.kind === ROUTE_GROUP_KIND);
1733
- }
1734
413
  //# sourceMappingURL=server.js.map