@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.
@@ -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 Error(
147
- `Conflicting path parameter names at "${definition.path}": ":${current.paramChild.name}" vs ":${paramName}"`
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(definition.method)) {
165
- throw new Error(
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(definition.method, definition);
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 (decoded === ".." || decoded === ".") return segment;
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 { BodyParseError, PayloadTooLargeError } from "./Errors.js";
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 Error("Router is required when mounting with a prefix");
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(match.route, response, routeCtx);
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, falls back to the original response.
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) return result.data;
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) return 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 = ctx.request.header?.["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 (!authorization.startsWith(BASIC_PREFIX)) return deny(ctx);
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 = ctx.request.header?.["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 (!authorization.startsWith(BEARER_PREFIX)) return deny(ctx);
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);