@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.
- package/CHANGELOG.md +24 -0
- package/README.md +207 -30
- package/dist/agent-capabilities/index.d.ts +167 -0
- package/dist/agent-capabilities/index.d.ts.map +1 -0
- package/dist/agent-capabilities/index.js +275 -0
- package/dist/agent-capabilities/index.js.map +1 -0
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +13 -3
- package/dist/application/index.js.map +1 -1
- package/dist/client/client.d.ts +2 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +33 -16
- package/dist/client/client.js.map +1 -1
- package/dist/contracts/contract-builder.d.ts +5 -7
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +3 -18
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts +2 -2
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +2 -1
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/metadata.d.ts +3 -0
- package/dist/contracts/metadata.d.ts.map +1 -0
- package/dist/contracts/metadata.js +14 -0
- package/dist/contracts/metadata.js.map +1 -0
- package/dist/contracts/types.d.ts +20 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/events/index.d.ts +4 -4
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +5 -5
- package/dist/events/index.js.map +1 -1
- package/dist/flags/index.d.ts +0 -4
- package/dist/flags/index.d.ts.map +1 -1
- package/dist/flags/index.js +0 -4
- package/dist/flags/index.js.map +1 -1
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +14 -9
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +10 -4
- package/dist/jobs/index.js.map +1 -1
- package/dist/notifications/index.d.ts +194 -10
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +397 -59
- package/dist/notifications/index.js.map +1 -1
- package/dist/openapi/index.js +15 -8
- package/dist/openapi/index.js.map +1 -1
- package/dist/ports/auth.d.ts +2 -0
- package/dist/ports/auth.d.ts.map +1 -1
- package/dist/ports/auth.js +2 -0
- package/dist/ports/auth.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/provider.d.ts +10 -2
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +6 -13
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +24 -44
- package/dist/schedules/index.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/request-executor.d.ts +72 -0
- package/dist/server/request-executor.d.ts.map +1 -0
- package/dist/server/request-executor.js +781 -0
- package/dist/server/request-executor.js.map +1 -0
- package/dist/server/request-preparation.d.ts +39 -0
- package/dist/server/request-preparation.d.ts.map +1 -0
- package/dist/server/request-preparation.js +248 -0
- package/dist/server/request-preparation.js.map +1 -0
- package/dist/server/response-finalization.d.ts +34 -0
- package/dist/server/response-finalization.d.ts.map +1 -0
- package/dist/server/response-finalization.js +279 -0
- package/dist/server/response-finalization.js.map +1 -0
- package/dist/server/route-definitions.d.ts +139 -0
- package/dist/server/route-definitions.d.ts.map +1 -0
- package/dist/server/route-definitions.js +94 -0
- package/dist/server/route-definitions.js.map +1 -0
- package/dist/server/route-matching.d.ts +15 -0
- package/dist/server/route-matching.d.ts.map +1 -0
- package/dist/server/route-matching.js +46 -0
- package/dist/server/route-matching.js.map +1 -0
- package/dist/server/server.d.ts +6 -157
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +29 -1340
- package/dist/server/server.js.map +1 -1
- package/dist/server/use-case-route.d.ts +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/index.d.ts +1 -1
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js.map +1 -1
- package/dist/uploads/client.js +4 -7
- package/dist/uploads/client.js.map +1 -1
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +59 -12
- package/dist/uploads/index.js.map +1 -1
- package/package.json +5 -5
- package/skills/app-architecture/SKILL.md +31 -1
- package/src/agent-capabilities/index.ts +578 -0
- package/src/application/index.ts +19 -5
- package/src/client/client.ts +41 -17
- package/src/contracts/contract-builder.ts +8 -23
- package/src/contracts/contract-group.ts +4 -6
- package/src/contracts/metadata.ts +20 -0
- package/src/contracts/types.ts +39 -0
- package/src/events/index.ts +16 -20
- package/src/flags/index.ts +0 -5
- package/src/idempotency/index.ts +17 -9
- package/src/jobs/index.ts +8 -3
- package/src/notifications/index.ts +678 -70
- package/src/openapi/index.ts +24 -12
- package/src/ports/auth.ts +2 -0
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +0 -1
- package/src/providers/index.ts +1 -0
- package/src/providers/provider.ts +18 -3
- package/src/schedules/index.ts +57 -63
- package/src/server/index.ts +4 -2
- package/src/server/request-executor.ts +1170 -0
- package/src/server/request-preparation.ts +409 -0
- package/src/server/response-finalization.ts +385 -0
- package/src/server/route-definitions.ts +338 -0
- package/src/server/route-matching.ts +64 -0
- package/src/server/server.ts +63 -2171
- package/src/server/use-case-route.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/tracing/index.ts +1 -0
- package/src/uploads/client.ts +4 -7
- package/src/uploads/index.ts +70 -12
package/src/server/server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
}
|