@nextrush/errors 3.0.7 → 4.0.0-beta.0

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.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @nextrush/errors - Public API surface test
3
+ *
4
+ * Locks the exported symbol set from `src/index.ts`. If this test fails, the
5
+ * public API has changed. Intentional changes require an explicit update to
6
+ * the expected list below, plus a changeset for a published package.
7
+ */
8
+ import { describe, expect, expectTypeOf, it } from 'vitest';
9
+ import * as errorsApi from '../index';
10
+ import type { ErrorHandlerOptions } from '../index';
11
+ import type { HttpErrorOptions } from '../index';
12
+ import type { ValidationIssue } from '../index';
13
+
14
+ describe('Public API surface (runtime exports)', () => {
15
+ it('exports exactly the intended runtime symbols', () => {
16
+ const actualExports = Object.keys(errorsApi).sort();
17
+
18
+ // SEALED: intentional public runtime API surface.
19
+ const expectedRuntime = [
20
+ // Base classes + helper
21
+ 'HttpError',
22
+ 'NextRushError',
23
+ 'getHttpStatusMessage',
24
+
25
+ // Central error-code registry
26
+ 'ERROR_CODES',
27
+ 'GENERIC_ERROR_CODE',
28
+ 'VALIDATION_ERROR_CODE',
29
+ 'codeForStatus',
30
+
31
+ // HTTP errors — 4xx
32
+ 'BadRequestError',
33
+ 'ConflictError',
34
+ 'ExpectationFailedError',
35
+ 'FailedDependencyError',
36
+ 'ForbiddenError',
37
+ 'GoneError',
38
+ 'ImATeapotError',
39
+ 'LengthRequiredError',
40
+ 'LockedError',
41
+ 'MethodNotAllowedError',
42
+ 'NotAcceptableError',
43
+ 'NotFoundError',
44
+ 'PayloadTooLargeError',
45
+ 'PaymentRequiredError',
46
+ 'PreconditionFailedError',
47
+ 'PreconditionRequiredError',
48
+ 'ProxyAuthRequiredError',
49
+ 'RangeNotSatisfiableError',
50
+ 'RequestHeaderFieldsTooLargeError',
51
+ 'RequestTimeoutError',
52
+ 'TooEarlyError',
53
+ 'TooManyRequestsError',
54
+ 'UnauthorizedError',
55
+ 'UnavailableForLegalReasonsError',
56
+ 'UnprocessableEntityError',
57
+ 'UnsupportedMediaTypeError',
58
+ 'UpgradeRequiredError',
59
+ 'UriTooLongError',
60
+
61
+ // HTTP errors — 5xx
62
+ 'BadGatewayError',
63
+ 'GatewayTimeoutError',
64
+ 'HttpVersionNotSupportedError',
65
+ 'InsufficientStorageError',
66
+ 'InternalServerError',
67
+ 'LoopDetectedError',
68
+ 'NetworkAuthRequiredError',
69
+ 'NotExtendedError',
70
+ 'NotImplementedError',
71
+ 'ServiceUnavailableError',
72
+ 'VariantAlsoNegotiatesError',
73
+
74
+ // Validation errors
75
+ 'InvalidEmailError',
76
+ 'InvalidUrlError',
77
+ 'LengthError',
78
+ 'PatternError',
79
+ 'RangeValidationError',
80
+ 'RequiredFieldError',
81
+ 'TypeMismatchError',
82
+ 'ValidationError',
83
+
84
+ // Factory functions
85
+ 'badGateway',
86
+ 'badRequest',
87
+ 'conflict',
88
+ 'createError',
89
+ 'forbidden',
90
+ 'gatewayTimeout',
91
+ 'getErrorStatus',
92
+ 'getSafeErrorMessage',
93
+ 'internalError',
94
+ 'isHttpError',
95
+ 'methodNotAllowed',
96
+ 'notFound',
97
+ 'serviceUnavailable',
98
+ 'tooManyRequests',
99
+ 'unauthorized',
100
+ 'unprocessableEntity',
101
+
102
+ // Middleware
103
+ 'errorHandler',
104
+ 'notFoundHandler',
105
+ ].sort();
106
+
107
+ expect(actualExports).toEqual(expectedRuntime);
108
+ });
109
+ });
110
+
111
+ describe('Public API surface (type-only exports)', () => {
112
+ it('the type-only surface stays importable from the barrel', () => {
113
+ // Compile-time only: removing/renaming any of these in src/index.ts fails
114
+ // this file to type-check.
115
+ type Surface = [HttpErrorOptions, ValidationIssue, ErrorHandlerOptions];
116
+ expectTypeOf<Surface>().not.toBeNever();
117
+ });
118
+ });
package/src/base.ts CHANGED
@@ -6,10 +6,52 @@
6
6
  * @packageDocumentation
7
7
  */
8
8
 
9
+ import { codeForStatus } from './codes';
10
+
11
+ /** Options accepted by {@link NextRushError.hydrate} when reconstructing errors. */
12
+ interface HydrateOptions {
13
+ status?: number;
14
+ code?: string;
15
+ details?: Record<string, unknown>;
16
+ requestId?: string;
17
+ traceId?: string;
18
+ timestamp?: string;
19
+ }
20
+
9
21
  const V8Error = Error as ErrorConstructor & {
10
22
  captureStackTrace?: (targetObject: object, constructorOpt?: Function) => void;
11
23
  };
12
24
 
25
+ /** Maximum depth walked when serializing a `cause` chain (audit E-2). */
26
+ const MAX_CAUSE_DEPTH = 5;
27
+
28
+ /**
29
+ * Serialize an error `cause` chain into a plain, JSON-safe object.
30
+ *
31
+ * @remarks
32
+ * Walks nested `cause` links up to {@link MAX_CAUSE_DEPTH}, guards against
33
+ * cyclic chains via a visited set, and only surfaces `name`/`message`/`code`
34
+ * so no unexpected enumerable properties (or secrets on ad-hoc error objects)
35
+ * leak. Returns `undefined` when there is nothing meaningful to serialize.
36
+ */
37
+ function serializeCause(cause: unknown, seen: Set<unknown>, depth: number): unknown {
38
+ if (cause === undefined || cause === null || depth >= MAX_CAUSE_DEPTH) return undefined;
39
+ if (typeof cause !== 'object') return { message: String(cause) };
40
+ if (seen.has(cause)) return undefined; // cycle guard
41
+ seen.add(cause);
42
+
43
+ const err = cause as { name?: unknown; message?: unknown; code?: unknown; cause?: unknown };
44
+ const out: Record<string, unknown> = {};
45
+ if (typeof err.name === 'string') out.name = err.name;
46
+ if (typeof err.message === 'string') out.message = err.message;
47
+ if (typeof err.code === 'string') out.code = err.code;
48
+
49
+ const nested = serializeCause(err.cause, seen, depth + 1);
50
+ if (nested !== undefined) out.cause = nested;
51
+
52
+ return out;
53
+ }
54
+
13
55
  /**
14
56
  * Base error class for all NextRush errors
15
57
  */
@@ -29,6 +71,15 @@ export class NextRushError extends Error {
29
71
  /** Original error that caused this error */
30
72
  readonly cause?: unknown;
31
73
 
74
+ /** Correlation/request identifier for tracing across boundaries (audit E-5). */
75
+ readonly requestId?: string;
76
+
77
+ /** Distributed-trace identifier (audit E-5). */
78
+ readonly traceId?: string;
79
+
80
+ /** ISO-8601 timestamp of when the error was created, when supplied. */
81
+ readonly timestamp?: string;
82
+
32
83
  constructor(
33
84
  message: string,
34
85
  options: {
@@ -37,15 +88,25 @@ export class NextRushError extends Error {
37
88
  expose?: boolean;
38
89
  details?: Record<string, unknown>;
39
90
  cause?: unknown;
91
+ requestId?: string;
92
+ traceId?: string;
93
+ timestamp?: string;
40
94
  } = {}
41
95
  ) {
42
- super(message);
96
+ // Pass cause to the native Error constructor so `util.inspect` / logging
97
+ // tooling walks the chain natively, in addition to our own `cause` field.
98
+ super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
43
99
  this.name = this.constructor.name;
44
100
  this.status = options.status ?? 500;
45
101
  this.code = options.code ?? 'INTERNAL_ERROR';
46
102
  this.expose = options.expose ?? this.status < 500;
47
- this.details = options.details;
103
+ // Freeze a shallow snapshot so the error owns immutable details and the
104
+ // caller's object cannot be mutated through the error (audit E-6).
105
+ this.details = options.details ? Object.freeze({ ...options.details }) : undefined;
48
106
  this.cause = options.cause;
107
+ this.requestId = options.requestId;
108
+ this.traceId = options.traceId;
109
+ this.timestamp = options.timestamp;
49
110
 
50
111
  // Maintain proper stack trace for V8
51
112
  // Skip for common client errors (4xx with expose=true) to reduce overhead.
@@ -71,9 +132,66 @@ export class NextRushError extends Error {
71
132
  json.details = this.details;
72
133
  }
73
134
 
135
+ // Surface the cause chain for diagnosability, but only on exposed errors —
136
+ // a non-exposed 5xx cause may contain internal detail (audit E-2). Server
137
+ // -side logging should read `error.cause` directly, which is never gated.
138
+ if (this.expose && this.cause !== undefined) {
139
+ const serialized = serializeCause(this.cause, new Set(), 0);
140
+ if (serialized !== undefined) {
141
+ json.cause = serialized;
142
+ }
143
+ }
144
+
145
+ // Correlation identifiers are safe to surface and are the primary handle
146
+ // for cross-service debugging (audit E-5). Only emitted when set, so the
147
+ // default serialized shape is unchanged.
148
+ if (this.requestId !== undefined) json.requestId = this.requestId;
149
+ if (this.traceId !== undefined) json.traceId = this.traceId;
150
+ if (this.timestamp !== undefined) json.timestamp = this.timestamp;
151
+
74
152
  return json;
75
153
  }
76
154
 
155
+ /**
156
+ * Reconstruct a {@link NextRushError} from a serialized {@link toJSON} payload
157
+ * (audit E-7).
158
+ *
159
+ * @remarks
160
+ * Enables cross-service transport: a downstream service can rebuild a typed
161
+ * error (with a working `instanceof NextRushError`) from the JSON it received,
162
+ * rather than being left with an opaque plain object. `message` falls back to
163
+ * a generic string when the source error was not exposed.
164
+ *
165
+ * @param json - A payload previously produced by {@link toJSON}.
166
+ * @returns A reconstructed {@link NextRushError}.
167
+ */
168
+ static fromJSON(json: Record<string, unknown>): NextRushError {
169
+ return NextRushError.hydrate(json, (message, options) => new NextRushError(message, options));
170
+ }
171
+
172
+ /**
173
+ * Shared hydration logic for {@link fromJSON} across the hierarchy.
174
+ * @internal
175
+ */
176
+ protected static hydrate<T extends NextRushError>(
177
+ json: Record<string, unknown>,
178
+ build: (message: string, options: HydrateOptions) => T
179
+ ): T {
180
+ const status = typeof json.status === 'number' ? json.status : 500;
181
+ const message = typeof json.message === 'string' ? json.message : getHttpStatusMessage(status);
182
+ return build(message, {
183
+ status,
184
+ code: typeof json.code === 'string' ? json.code : undefined,
185
+ details:
186
+ json.details !== null && typeof json.details === 'object'
187
+ ? (json.details as Record<string, unknown>)
188
+ : undefined,
189
+ requestId: typeof json.requestId === 'string' ? json.requestId : undefined,
190
+ traceId: typeof json.traceId === 'string' ? json.traceId : undefined,
191
+ timestamp: typeof json.timestamp === 'string' ? json.timestamp : undefined,
192
+ });
193
+ }
194
+
77
195
  /**
78
196
  * Create a response-safe version of the error
79
197
  */
@@ -149,14 +267,41 @@ export class HttpError extends NextRushError {
149
267
  expose?: boolean;
150
268
  details?: Record<string, unknown>;
151
269
  cause?: unknown;
270
+ requestId?: string;
271
+ traceId?: string;
272
+ timestamp?: string;
152
273
  } = {}
153
274
  ) {
154
275
  super(message ?? getHttpStatusMessage(status), {
155
276
  status,
156
- code: options.code ?? `HTTP_${status}`,
277
+ code: options.code ?? codeForStatus(status),
157
278
  expose: options.expose ?? status < 500,
158
279
  details: options.details,
159
280
  cause: options.cause,
281
+ requestId: options.requestId,
282
+ traceId: options.traceId,
283
+ timestamp: options.timestamp,
160
284
  });
161
285
  }
286
+
287
+ /**
288
+ * Reconstruct an {@link HttpError} from a serialized {@link NextRushError.toJSON}
289
+ * payload (audit E-7).
290
+ *
291
+ * @param json - A payload previously produced by `toJSON()`.
292
+ * @returns A reconstructed {@link HttpError}.
293
+ */
294
+ static override fromJSON(json: Record<string, unknown>): HttpError {
295
+ return NextRushError.hydrate(
296
+ json,
297
+ (message, options) =>
298
+ new HttpError(options.status ?? 500, message, {
299
+ code: options.code,
300
+ details: options.details,
301
+ requestId: options.requestId,
302
+ traceId: options.traceId,
303
+ timestamp: options.timestamp,
304
+ })
305
+ );
306
+ }
162
307
  }
package/src/codes.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @nextrush/errors - Central Error Code Registry
3
+ *
4
+ * Single source of truth mapping HTTP status codes to their canonical,
5
+ * machine-readable error codes (audit E-4). Both the typed error classes and
6
+ * the {@link createError} factory resolve codes through this registry, so a
7
+ * given status maps to exactly one code regardless of construction path.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ /**
13
+ * Canonical error code for each supported HTTP status.
14
+ *
15
+ * @remarks
16
+ * Keep this in sync with the typed classes in `http-errors.ts`. The
17
+ * `audit-fixes` test suite asserts `createError(status).code === ERROR_CODES[status]`
18
+ * for every entry, so drift between a class and this registry fails CI.
19
+ */
20
+ export const ERROR_CODES: Readonly<Record<number, string>> = Object.freeze({
21
+ 400: 'BAD_REQUEST',
22
+ 401: 'UNAUTHORIZED',
23
+ 402: 'PAYMENT_REQUIRED',
24
+ 403: 'FORBIDDEN',
25
+ 404: 'NOT_FOUND',
26
+ 405: 'METHOD_NOT_ALLOWED',
27
+ 406: 'NOT_ACCEPTABLE',
28
+ 407: 'PROXY_AUTH_REQUIRED',
29
+ 408: 'REQUEST_TIMEOUT',
30
+ 409: 'CONFLICT',
31
+ 410: 'GONE',
32
+ 411: 'LENGTH_REQUIRED',
33
+ 412: 'PRECONDITION_FAILED',
34
+ 413: 'PAYLOAD_TOO_LARGE',
35
+ 414: 'URI_TOO_LONG',
36
+ 415: 'UNSUPPORTED_MEDIA_TYPE',
37
+ 416: 'RANGE_NOT_SATISFIABLE',
38
+ 417: 'EXPECTATION_FAILED',
39
+ 418: 'IM_A_TEAPOT',
40
+ 422: 'UNPROCESSABLE_ENTITY',
41
+ 423: 'LOCKED',
42
+ 424: 'FAILED_DEPENDENCY',
43
+ 425: 'TOO_EARLY',
44
+ 426: 'UPGRADE_REQUIRED',
45
+ 428: 'PRECONDITION_REQUIRED',
46
+ 429: 'TOO_MANY_REQUESTS',
47
+ 431: 'REQUEST_HEADER_FIELDS_TOO_LARGE',
48
+ 451: 'UNAVAILABLE_FOR_LEGAL_REASONS',
49
+ 500: 'INTERNAL_SERVER_ERROR',
50
+ 501: 'NOT_IMPLEMENTED',
51
+ 502: 'BAD_GATEWAY',
52
+ 503: 'SERVICE_UNAVAILABLE',
53
+ 504: 'GATEWAY_TIMEOUT',
54
+ 505: 'HTTP_VERSION_NOT_SUPPORTED',
55
+ 506: 'VARIANT_ALSO_NEGOTIATES',
56
+ 507: 'INSUFFICIENT_STORAGE',
57
+ 508: 'LOOP_DETECTED',
58
+ 510: 'NOT_EXTENDED',
59
+ 511: 'NETWORK_AUTH_REQUIRED',
60
+ });
61
+
62
+ /**
63
+ * Resolve the canonical error code for an HTTP status.
64
+ *
65
+ * @param status - HTTP status code.
66
+ * @returns The registered canonical code, or `HTTP_<status>` for statuses with
67
+ * no dedicated class.
68
+ */
69
+ export function codeForStatus(status: number): string {
70
+ return ERROR_CODES[status] ?? `HTTP_${status}`;
71
+ }
72
+
73
+ /** Generic internal-error code used when no status-specific code applies. */
74
+ export const GENERIC_ERROR_CODE = 'INTERNAL_ERROR';
75
+
76
+ /** Canonical code for validation failures. */
77
+ export const VALIDATION_ERROR_CODE = 'VALIDATION_ERROR';
package/src/factory.ts CHANGED
@@ -11,35 +11,92 @@ import {
11
11
  BadGatewayError,
12
12
  BadRequestError,
13
13
  ConflictError,
14
+ ExpectationFailedError,
15
+ FailedDependencyError,
14
16
  ForbiddenError,
15
17
  GatewayTimeoutError,
18
+ GoneError,
19
+ HttpVersionNotSupportedError,
20
+ ImATeapotError,
21
+ InsufficientStorageError,
16
22
  InternalServerError,
23
+ LengthRequiredError,
24
+ LockedError,
25
+ LoopDetectedError,
17
26
  MethodNotAllowedError,
27
+ NetworkAuthRequiredError,
28
+ NotAcceptableError,
29
+ NotExtendedError,
18
30
  NotFoundError,
19
31
  NotImplementedError,
32
+ PayloadTooLargeError,
33
+ PaymentRequiredError,
34
+ PreconditionFailedError,
35
+ PreconditionRequiredError,
36
+ ProxyAuthRequiredError,
37
+ RangeNotSatisfiableError,
38
+ RequestHeaderFieldsTooLargeError,
39
+ RequestTimeoutError,
20
40
  ServiceUnavailableError,
41
+ TooEarlyError,
21
42
  TooManyRequestsError,
22
43
  UnauthorizedError,
44
+ UnavailableForLegalReasonsError,
23
45
  UnprocessableEntityError,
46
+ UnsupportedMediaTypeError,
47
+ UpgradeRequiredError,
48
+ UriTooLongError,
49
+ VariantAlsoNegotiatesError,
24
50
  type HttpErrorOptions,
25
51
  } from './http-errors';
26
52
 
27
53
  /**
28
- * HTTP status code to error class mapping
54
+ * HTTP status code to error class mapping.
55
+ *
56
+ * @remarks
57
+ * Covers every status that has a dedicated typed class so {@link createError}
58
+ * returns the correctly-coded instance (audit E-3). `405` is handled separately
59
+ * because {@link MethodNotAllowedError} has a divergent constructor signature.
29
60
  */
30
61
  const ERROR_MAP: Record<number, new (message?: string, options?: HttpErrorOptions) => HttpError> = {
31
62
  400: BadRequestError,
32
63
  401: UnauthorizedError,
64
+ 402: PaymentRequiredError,
33
65
  403: ForbiddenError,
34
66
  404: NotFoundError,
67
+ 406: NotAcceptableError,
68
+ 407: ProxyAuthRequiredError,
69
+ 408: RequestTimeoutError,
35
70
  409: ConflictError,
71
+ 410: GoneError,
72
+ 411: LengthRequiredError,
73
+ 412: PreconditionFailedError,
74
+ 413: PayloadTooLargeError,
75
+ 414: UriTooLongError,
76
+ 415: UnsupportedMediaTypeError,
77
+ 416: RangeNotSatisfiableError,
78
+ 417: ExpectationFailedError,
79
+ 418: ImATeapotError,
36
80
  422: UnprocessableEntityError,
81
+ 423: LockedError,
82
+ 424: FailedDependencyError,
83
+ 425: TooEarlyError,
84
+ 426: UpgradeRequiredError,
85
+ 428: PreconditionRequiredError,
37
86
  429: TooManyRequestsError,
87
+ 431: RequestHeaderFieldsTooLargeError,
88
+ 451: UnavailableForLegalReasonsError,
38
89
  500: InternalServerError,
39
90
  501: NotImplementedError,
40
91
  502: BadGatewayError,
41
92
  503: ServiceUnavailableError,
42
93
  504: GatewayTimeoutError,
94
+ 505: HttpVersionNotSupportedError,
95
+ 506: VariantAlsoNegotiatesError,
96
+ 507: InsufficientStorageError,
97
+ 508: LoopDetectedError,
98
+ 510: NotExtendedError,
99
+ 511: NetworkAuthRequiredError,
43
100
  };
44
101
 
45
102
  /**
@@ -16,6 +16,12 @@ export interface HttpErrorOptions {
16
16
  expose?: boolean;
17
17
  details?: Record<string, unknown>;
18
18
  cause?: unknown;
19
+ /** Correlation/request identifier (audit E-5). */
20
+ requestId?: string;
21
+ /** Distributed-trace identifier (audit E-5). */
22
+ traceId?: string;
23
+ /** ISO-8601 creation timestamp (audit E-5). */
24
+ timestamp?: string;
19
25
  }
20
26
 
21
27
  // =============================================================================
package/src/index.ts CHANGED
@@ -9,6 +9,9 @@
9
9
  // Base classes
10
10
  export { HttpError, NextRushError, getHttpStatusMessage } from './base';
11
11
 
12
+ // Central error-code registry
13
+ export { ERROR_CODES, GENERIC_ERROR_CODE, VALIDATION_ERROR_CODE, codeForStatus } from './codes';
14
+
12
15
  // HTTP errors - 4xx
13
16
  export {
14
17
  BadRequestError,
@@ -91,11 +94,4 @@ export {
91
94
  } from './factory';
92
95
 
93
96
  // Middleware
94
- export {
95
- catchAsync,
96
- errorHandler,
97
- notFoundHandler,
98
- type ErrorContext,
99
- type ErrorHandlerOptions,
100
- type ErrorMiddleware,
101
- } from './middleware';
97
+ export { errorHandler, notFoundHandler, type ErrorHandlerOptions } from './middleware';
package/src/middleware.ts CHANGED
@@ -9,27 +9,6 @@
9
9
  import type { Context, Middleware, Next } from '@nextrush/types';
10
10
  import { HttpError, NextRushError, getHttpStatusMessage } from './base';
11
11
 
12
- /**
13
- * Minimal context interface for error handling.
14
- *
15
- * @deprecated Use `Context` from `@nextrush/types` instead.
16
- * Kept for backward compatibility — will be removed in v4.
17
- */
18
- export interface ErrorContext {
19
- method: string;
20
- path: string;
21
- status: number;
22
- json: (data: unknown) => void;
23
- }
24
-
25
- /**
26
- * Error handler middleware function type.
27
- *
28
- * @deprecated Use `Middleware` from `@nextrush/types` instead.
29
- * Kept for backward compatibility — will be removed in v4.
30
- */
31
- export type ErrorMiddleware = Middleware;
32
-
33
12
  /**
34
13
  * Error handler options
35
14
  */
@@ -116,6 +95,12 @@ export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
116
95
 
117
96
  if (transform) {
118
97
  body = transform(err, ctx);
98
+ } else if (err instanceof NextRushError) {
99
+ // Delegate to the error's own toJSON() — this is the single source of
100
+ // truth for what a given error type serializes to (e.g. ValidationError
101
+ // adds `issues` while stripping `received`). Duplicating that shape here
102
+ // would silently drift out of sync with subclass overrides.
103
+ body = err.toJSON();
119
104
  } else {
120
105
  body = {
121
106
  error: expose ? err.name : getHttpStatusMessage(status),
@@ -127,10 +112,10 @@ export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
127
112
  if (expose && details) {
128
113
  body.details = details;
129
114
  }
115
+ }
130
116
 
131
- if (includeStack && err.stack) {
132
- body.stack = err.stack.split('\n').map((line) => line.trim());
133
- }
117
+ if (includeStack && err.stack) {
118
+ body.stack = err.stack.split('\n').map((line) => line.trim());
134
119
  }
135
120
 
136
121
  ctx.json(body);
@@ -162,22 +147,3 @@ export function notFoundHandler(message = 'Not Found'): Middleware {
162
147
  };
163
148
  }
164
149
 
165
- /**
166
- * Catch async errors wrapper for route handlers
167
- *
168
- * @example
169
- * ```typescript
170
- * router.get('/users/:id', catchAsync(async (ctx) => {
171
- * const user = await db.users.findById(ctx.params.id);
172
- * if (!user) throw new NotFoundError('User not found');
173
- * ctx.json(user);
174
- * }));
175
- * ```
176
- *
177
- * @deprecated This wrapper is redundant — async errors propagate naturally
178
- * through the middleware chain and are caught by `errorHandler()`. Use the
179
- * handler directly instead.
180
- */
181
- export function catchAsync(handler: (ctx: Context, next: Next) => Promise<void>): Middleware {
182
- return handler;
183
- }
package/src/validation.ts CHANGED
@@ -37,7 +37,8 @@ export class ValidationError extends NextRushError {
37
37
  code: 'VALIDATION_ERROR',
38
38
  expose: true,
39
39
  });
40
- this.issues = issues;
40
+ // Freeze a snapshot so issues are immutable after construction (audit E-6).
41
+ this.issues = Object.freeze([...issues]) as ValidationIssue[];
41
42
  }
42
43
 
43
44
  /**