@rexeus/typeweaver-server 0.10.3 → 0.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/index.cjs +2 -0
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/dist/lib/BodyLimitPolicy.ts +8 -1
- package/dist/lib/FetchApiAdapter.ts +5 -1
- package/dist/lib/Middleware.ts +2 -1
- package/dist/lib/NodeAdapter.ts +332 -16
- package/dist/lib/PathMatcher.ts +54 -10
- package/dist/lib/Router.ts +24 -8
- package/dist/lib/TypeweaverApp.ts +30 -13
- package/dist/lib/errors/ConflictingPathParameterNameError.ts +20 -0
- package/dist/lib/errors/DuplicateRouteRegistrationError.ts +19 -0
- package/dist/lib/errors/MiddlewareNextAlreadyCalledError.ts +16 -0
- package/dist/lib/errors/MissingRouterForPrefixedMountError.ts +16 -0
- package/dist/lib/errors/RequestBodyClosedBeforeEndError.ts +19 -0
- package/dist/lib/errors/RequestBodyReadAbortedError.ts +19 -0
- package/dist/lib/errors/index.ts +19 -0
- package/dist/lib/middleware/basicAuth.ts +11 -2
- package/dist/lib/middleware/bearerAuth.ts +11 -2
- package/dist/lib/middleware/cors.ts +236 -48
- package/dist/lib/middleware/header.ts +59 -0
- package/dist/lib/middleware/logger.ts +4 -2
- package/dist/lib/middleware/poweredBy.ts +6 -1
- package/dist/lib/middleware/requestId.ts +8 -8
- package/dist/lib/middleware/scoped.ts +37 -12
- package/dist/lib/middleware/secureHeaders.ts +3 -1
- package/dist/templates/Router.ejs +2 -1
- package/package.json +5 -5
package/dist/lib/Router.ts
CHANGED
|
@@ -14,6 +14,10 @@ import type {
|
|
|
14
14
|
RequestValidationError,
|
|
15
15
|
ResponseValidationError,
|
|
16
16
|
} from "@rexeus/typeweaver-core";
|
|
17
|
+
import {
|
|
18
|
+
ConflictingPathParameterNameError,
|
|
19
|
+
DuplicateRouteRegistrationError,
|
|
20
|
+
} from "./errors/index.js";
|
|
17
21
|
import type { RequestHandler } from "./RequestHandler.js";
|
|
18
22
|
import type { ServerContext } from "./ServerContext.js";
|
|
19
23
|
|
|
@@ -135,6 +139,11 @@ export class Router {
|
|
|
135
139
|
* Register a route in the radix tree.
|
|
136
140
|
*/
|
|
137
141
|
public add(definition: RouteDefinition): void {
|
|
142
|
+
const method = definition.method.toUpperCase();
|
|
143
|
+
const normalizedDefinition =
|
|
144
|
+
definition.method === method
|
|
145
|
+
? definition
|
|
146
|
+
: { ...definition, method: method as HttpMethod };
|
|
138
147
|
const segments = Router.toSegments(definition.path);
|
|
139
148
|
|
|
140
149
|
let current = this.root;
|
|
@@ -143,8 +152,10 @@ export class Router {
|
|
|
143
152
|
if (segment.startsWith(":")) {
|
|
144
153
|
const paramName = segment.slice(1);
|
|
145
154
|
if (current.paramChild && current.paramChild.name !== paramName) {
|
|
146
|
-
throw new
|
|
147
|
-
|
|
155
|
+
throw new ConflictingPathParameterNameError(
|
|
156
|
+
definition.path,
|
|
157
|
+
current.paramChild.name,
|
|
158
|
+
paramName
|
|
148
159
|
);
|
|
149
160
|
}
|
|
150
161
|
if (!current.paramChild) {
|
|
@@ -161,13 +172,11 @@ export class Router {
|
|
|
161
172
|
}
|
|
162
173
|
}
|
|
163
174
|
|
|
164
|
-
if (current.methods.has(
|
|
165
|
-
throw new
|
|
166
|
-
`Route conflict: ${definition.method} ${definition.path} is already registered`
|
|
167
|
-
);
|
|
175
|
+
if (current.methods.has(method)) {
|
|
176
|
+
throw new DuplicateRouteRegistrationError(method, definition.path);
|
|
168
177
|
}
|
|
169
178
|
|
|
170
|
-
current.methods.set(
|
|
179
|
+
current.methods.set(method, normalizedDefinition);
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
/**
|
|
@@ -280,7 +289,14 @@ export class Router {
|
|
|
280
289
|
private static decodePathSegment(segment: string): string {
|
|
281
290
|
try {
|
|
282
291
|
const decoded = decodeURIComponent(segment);
|
|
283
|
-
if (
|
|
292
|
+
if (
|
|
293
|
+
decoded === ".." ||
|
|
294
|
+
decoded === "." ||
|
|
295
|
+
decoded.includes("/") ||
|
|
296
|
+
decoded.includes("\\")
|
|
297
|
+
) {
|
|
298
|
+
return segment;
|
|
299
|
+
}
|
|
284
300
|
return decoded;
|
|
285
301
|
} catch {
|
|
286
302
|
return segment;
|
|
@@ -13,13 +13,19 @@ import {
|
|
|
13
13
|
internalServerErrorDefaultError,
|
|
14
14
|
isTypedHttpResponse,
|
|
15
15
|
methodNotAllowedDefaultError,
|
|
16
|
+
normalizeHttpResponse,
|
|
16
17
|
notFoundDefaultError,
|
|
17
18
|
payloadTooLargeDefaultError,
|
|
18
19
|
RequestValidationError,
|
|
20
|
+
toHttpResponse,
|
|
19
21
|
validationDefaultError,
|
|
20
22
|
} from "@rexeus/typeweaver-core";
|
|
21
23
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
BodyParseError,
|
|
26
|
+
MissingRouterForPrefixedMountError,
|
|
27
|
+
PayloadTooLargeError,
|
|
28
|
+
} from "./errors/index.js";
|
|
23
29
|
import { executeMiddlewarePipeline } from "./Middleware.js";
|
|
24
30
|
import { Router } from "./Router.js";
|
|
25
31
|
import { StateMap } from "./StateMap.js";
|
|
@@ -152,7 +158,7 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
152
158
|
): this {
|
|
153
159
|
if (typeof prefixOrRouter === "string") {
|
|
154
160
|
if (!router) {
|
|
155
|
-
throw new
|
|
161
|
+
throw new MissingRouterForPrefixedMountError(prefixOrRouter);
|
|
156
162
|
}
|
|
157
163
|
return this.mountRouter(router, prefixOrRouter);
|
|
158
164
|
}
|
|
@@ -240,14 +246,12 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
240
246
|
const routeCtx = this.withPathParams(ctx, match.params);
|
|
241
247
|
try {
|
|
242
248
|
const response = await this.executeHandler(routeCtx, match.route);
|
|
243
|
-
return await this.validateResponse(
|
|
249
|
+
return await this.validateResponse(
|
|
250
|
+
match.route,
|
|
251
|
+
normalizeHttpResponse(response),
|
|
252
|
+
routeCtx
|
|
253
|
+
);
|
|
244
254
|
} catch (error) {
|
|
245
|
-
if (
|
|
246
|
-
isTypedHttpResponse(error) &&
|
|
247
|
-
match.route.routerConfig.validateResponses
|
|
248
|
-
) {
|
|
249
|
-
return await this.validateResponse(match.route, error, routeCtx);
|
|
250
|
-
}
|
|
251
255
|
return this.handleError(error, routeCtx, match.route);
|
|
252
256
|
}
|
|
253
257
|
}
|
|
@@ -289,7 +293,7 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
289
293
|
* - `validateResponses: true` (default) → runs validation:
|
|
290
294
|
* - Valid response → returns the stripped response (extra fields removed).
|
|
291
295
|
* - Invalid response + handler configured → calls the handler safely.
|
|
292
|
-
* If the handler throws,
|
|
296
|
+
* If the handler throws, fails closed with a sanitized 500 response.
|
|
293
297
|
* - Invalid response + `handleResponseValidationErrors: false` → returns
|
|
294
298
|
* the original (invalid) response as-is.
|
|
295
299
|
*
|
|
@@ -307,7 +311,9 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
307
311
|
|
|
308
312
|
const result = route.responseValidator.safeValidate(response);
|
|
309
313
|
|
|
310
|
-
if (result.isValid)
|
|
314
|
+
if (result.isValid) {
|
|
315
|
+
return normalizeHttpResponse(result.data);
|
|
316
|
+
}
|
|
311
317
|
|
|
312
318
|
const handler = this.resolveErrorHandler<ResponseValidationErrorHandler>(
|
|
313
319
|
route.routerConfig.handleResponseValidationErrors,
|
|
@@ -319,6 +325,11 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
319
325
|
handler(result.error, response, ctx)
|
|
320
326
|
);
|
|
321
327
|
if (handlerResponse) return handlerResponse;
|
|
328
|
+
return TypeweaverApp.defaultResponseValidationHandler(
|
|
329
|
+
result.error,
|
|
330
|
+
response,
|
|
331
|
+
ctx
|
|
332
|
+
);
|
|
322
333
|
}
|
|
323
334
|
|
|
324
335
|
return response;
|
|
@@ -378,7 +389,13 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
378
389
|
const response = await this.safelyExecuteErrorHandler(() =>
|
|
379
390
|
handler(error, ctx)
|
|
380
391
|
);
|
|
381
|
-
if (response)
|
|
392
|
+
if (response) {
|
|
393
|
+
return await this.validateResponse(
|
|
394
|
+
route,
|
|
395
|
+
normalizeHttpResponse(response),
|
|
396
|
+
ctx
|
|
397
|
+
);
|
|
398
|
+
}
|
|
382
399
|
}
|
|
383
400
|
}
|
|
384
401
|
|
|
@@ -461,7 +478,7 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
461
478
|
|
|
462
479
|
private static defaultHttpResponseHandler: HttpResponseErrorHandler = (
|
|
463
480
|
err
|
|
464
|
-
): IHttpResponse => err;
|
|
481
|
+
): IHttpResponse => toHttpResponse(err);
|
|
465
482
|
|
|
466
483
|
private readonly defaultUnknownHandler: UnknownErrorHandler = (
|
|
467
484
|
error
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class ConflictingPathParameterNameError extends Error {
|
|
9
|
+
public override readonly name = "ConflictingPathParameterNameError";
|
|
10
|
+
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly path: string,
|
|
13
|
+
public readonly existingParameterName: string,
|
|
14
|
+
public readonly conflictingParameterName: string
|
|
15
|
+
) {
|
|
16
|
+
super(
|
|
17
|
+
`Conflicting path parameter names in '${path}': existing parameter ':${existingParameterName}' conflicts with ':${conflictingParameterName}' at the same route position.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class DuplicateRouteRegistrationError extends Error {
|
|
9
|
+
public override readonly name = "DuplicateRouteRegistrationError";
|
|
10
|
+
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly method: string,
|
|
13
|
+
public readonly path: string
|
|
14
|
+
) {
|
|
15
|
+
super(
|
|
16
|
+
`Duplicate route registration refused: ${method} ${path} is already registered.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class MiddlewareNextAlreadyCalledError extends Error {
|
|
9
|
+
public override readonly name = "MiddlewareNextAlreadyCalledError";
|
|
10
|
+
|
|
11
|
+
public constructor(public readonly middlewareIndex: number) {
|
|
12
|
+
super(
|
|
13
|
+
`Middleware at index ${middlewareIndex} called next() more than once.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class MissingRouterForPrefixedMountError extends Error {
|
|
9
|
+
public override readonly name = "MissingRouterForPrefixedMountError";
|
|
10
|
+
|
|
11
|
+
public constructor(public readonly prefix: string) {
|
|
12
|
+
super(
|
|
13
|
+
`Router is required when mounting with prefix '${prefix}'. Pass both a prefix and a Typeweaver router instance.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class RequestBodyClosedBeforeEndError extends Error {
|
|
9
|
+
public override readonly name = "RequestBodyClosedBeforeEndError";
|
|
10
|
+
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly bytesRead: number,
|
|
13
|
+
public readonly maxBodySize: number
|
|
14
|
+
) {
|
|
15
|
+
super(
|
|
16
|
+
`Request closed before the body finished reading after ${bytesRead} bytes; maximum allowed body size is ${maxBodySize} bytes.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class RequestBodyReadAbortedError extends Error {
|
|
9
|
+
public override readonly name = "RequestBodyReadAbortedError";
|
|
10
|
+
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly bytesRead: number,
|
|
13
|
+
public readonly maxBodySize: number
|
|
14
|
+
) {
|
|
15
|
+
super(
|
|
16
|
+
`Request was aborted while reading the body after ${bytesRead} bytes; maximum allowed body size is ${maxBodySize} bytes.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
BodyParseError,
|
|
10
|
+
PayloadTooLargeError,
|
|
11
|
+
RequestBodyDrainTimeoutError,
|
|
12
|
+
ResponseSerializationError,
|
|
13
|
+
} from "../Errors.js";
|
|
14
|
+
export { ConflictingPathParameterNameError } from "./ConflictingPathParameterNameError.js";
|
|
15
|
+
export { DuplicateRouteRegistrationError } from "./DuplicateRouteRegistrationError.js";
|
|
16
|
+
export { MiddlewareNextAlreadyCalledError } from "./MiddlewareNextAlreadyCalledError.js";
|
|
17
|
+
export { MissingRouterForPrefixedMountError } from "./MissingRouterForPrefixedMountError.js";
|
|
18
|
+
export { RequestBodyClosedBeforeEndError } from "./RequestBodyClosedBeforeEndError.js";
|
|
19
|
+
export { RequestBodyReadAbortedError } from "./RequestBodyReadAbortedError.js";
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "@rexeus/typeweaver-core";
|
|
5
5
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
6
6
|
import { defineMiddleware } from "../TypedMiddleware.js";
|
|
7
|
+
import { readSingletonHeader } from "./header.js";
|
|
7
8
|
import type { ServerContext } from "../ServerContext.js";
|
|
8
9
|
|
|
9
10
|
export type BasicAuthOptions = {
|
|
@@ -31,10 +32,18 @@ export function basicAuth(options: BasicAuthOptions) {
|
|
|
31
32
|
options.onUnauthorized?.(ctx) ?? defaultResponse;
|
|
32
33
|
|
|
33
34
|
return defineMiddleware<{ username: string }>(async (ctx, next) => {
|
|
34
|
-
const authorization =
|
|
35
|
+
const authorization = readSingletonHeader(
|
|
36
|
+
ctx.request.header,
|
|
37
|
+
"authorization"
|
|
38
|
+
);
|
|
35
39
|
if (typeof authorization !== "string") return deny(ctx);
|
|
36
40
|
|
|
37
|
-
if (
|
|
41
|
+
if (
|
|
42
|
+
authorization.slice(0, BASIC_PREFIX.length).toLowerCase() !==
|
|
43
|
+
BASIC_PREFIX.toLowerCase()
|
|
44
|
+
) {
|
|
45
|
+
return deny(ctx);
|
|
46
|
+
}
|
|
38
47
|
|
|
39
48
|
const encoded = authorization.slice(BASIC_PREFIX.length);
|
|
40
49
|
if (encoded.length === 0) return deny(ctx);
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "@rexeus/typeweaver-core";
|
|
5
5
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
6
6
|
import { defineMiddleware } from "../TypedMiddleware.js";
|
|
7
|
+
import { readSingletonHeader } from "./header.js";
|
|
7
8
|
import type { ServerContext } from "../ServerContext.js";
|
|
8
9
|
|
|
9
10
|
export type BearerAuthOptions = {
|
|
@@ -30,10 +31,18 @@ export function bearerAuth(options: BearerAuthOptions) {
|
|
|
30
31
|
options.onUnauthorized?.(ctx) ?? defaultResponse;
|
|
31
32
|
|
|
32
33
|
return defineMiddleware<{ token: string }>(async (ctx, next) => {
|
|
33
|
-
const authorization =
|
|
34
|
+
const authorization = readSingletonHeader(
|
|
35
|
+
ctx.request.header,
|
|
36
|
+
"authorization"
|
|
37
|
+
);
|
|
34
38
|
if (typeof authorization !== "string") return deny(ctx);
|
|
35
39
|
|
|
36
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
authorization.slice(0, BEARER_PREFIX.length).toLowerCase() !==
|
|
42
|
+
BEARER_PREFIX.toLowerCase()
|
|
43
|
+
) {
|
|
44
|
+
return deny(ctx);
|
|
45
|
+
}
|
|
37
46
|
|
|
38
47
|
const token = authorization.slice(BEARER_PREFIX.length);
|
|
39
48
|
if (token.length === 0) return deny(ctx);
|