@nextrush/errors 3.0.7 → 4.0.0-beta.1

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/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
  /**
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @nextrush/errors - Header Validation Error
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ import { NextRushError } from './base';
8
+
9
+ /**
10
+ * Thrown when a header field name or value fails RFC 9110 grammar
11
+ * validation (field-name token grammar, field-value grammar — no control
12
+ * characters, no leading/trailing whitespace, no obs-fold).
13
+ *
14
+ * @remarks
15
+ * A rejected write here means the application (or a framework internal) is
16
+ * constructing an invalid header, not that a client sent bad input — so this
17
+ * is a 500-class programming error, not a validation-issue-list shape like
18
+ * {@link ValidationError}.
19
+ */
20
+ export class HeaderValidationError extends NextRushError {
21
+ constructor(message: string) {
22
+ super(message, { status: 500, code: 'HEADER_VALIDATION_ERROR', expose: false });
23
+ }
24
+ }
@@ -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,12 @@
9
9
  // Base classes
10
10
  export { HttpError, NextRushError, getHttpStatusMessage } from './base';
11
11
 
12
+ // Header validation
13
+ export { HeaderValidationError } from './header-validation';
14
+
15
+ // Central error-code registry
16
+ export { ERROR_CODES, GENERIC_ERROR_CODE, VALIDATION_ERROR_CODE, codeForStatus } from './codes';
17
+
12
18
  // HTTP errors - 4xx
13
19
  export {
14
20
  BadRequestError,
@@ -91,11 +97,4 @@ export {
91
97
  } from './factory';
92
98
 
93
99
  // Middleware
94
- export {
95
- catchAsync,
96
- errorHandler,
97
- notFoundHandler,
98
- type ErrorContext,
99
- type ErrorHandlerOptions,
100
- type ErrorMiddleware,
101
- } from './middleware';
100
+ export { errorHandler, notFoundHandler, type ErrorHandlerOptions } from './middleware';
package/src/middleware.ts CHANGED
@@ -7,29 +7,9 @@
7
7
  */
8
8
 
9
9
  import type { Context, Middleware, Next } from '@nextrush/types';
10
+ import { SECURITY_AUDIT, type SecurityAuditVerdict } from '@nextrush/types';
10
11
  import { HttpError, NextRushError, getHttpStatusMessage } from './base';
11
12
 
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
13
  /**
34
14
  * Error handler options
35
15
  */
@@ -37,6 +17,21 @@ export interface ErrorHandlerOptions {
37
17
  /** Include stack trace in development */
38
18
  includeStack?: boolean;
39
19
 
20
+ /**
21
+ * Whether the application is running in production (SEC-14).
22
+ *
23
+ * @remarks
24
+ * `@nextrush/errors` has no access to `app.isProduction` on its own — the
25
+ * caller threads it through explicitly, mirroring `@nextrush/core`'s
26
+ * `writeDefaultErrorResponse(opts.isProduction)`. When `true`, a truthy
27
+ * `includeStack` is ignored (fail closed) and a single warning is logged
28
+ * once per process, rather than once per request. Defaults to `false` so
29
+ * existing callers that don't pass this option keep today's behavior.
30
+ *
31
+ * @default false
32
+ */
33
+ isProduction?: boolean;
34
+
40
35
  /** Custom error logger */
41
36
  logger?: (error: Error, ctx: Context) => void;
42
37
 
@@ -75,9 +70,14 @@ function defaultLogger(error: Error, ctx: Context): void {
75
70
  * ```
76
71
  */
77
72
  export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
78
- const { includeStack = false, logger = defaultLogger, transform, handlers } = options;
73
+ const { includeStack = false, isProduction = false, logger = defaultLogger, transform, handlers } = options;
79
74
 
80
- return async (ctx: Context, next: Next): Promise<void> => {
75
+ // SEC-14: warn once per process, not once per request — a per-request
76
+ // warning on a hot error path floods logs for no added value once the
77
+ // misconfiguration is known.
78
+ let warnedIncludeStackInProduction = false;
79
+
80
+ const handler: Middleware = async (ctx: Context, next: Next): Promise<void> => {
81
81
  try {
82
82
  await next();
83
83
  } catch (error) {
@@ -116,6 +116,12 @@ export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
116
116
 
117
117
  if (transform) {
118
118
  body = transform(err, ctx);
119
+ } else if (err instanceof NextRushError) {
120
+ // Delegate to the error's own toJSON() — this is the single source of
121
+ // truth for what a given error type serializes to (e.g. ValidationError
122
+ // adds `issues` while stripping `received`). Duplicating that shape here
123
+ // would silently drift out of sync with subclass overrides.
124
+ body = err.toJSON();
119
125
  } else {
120
126
  body = {
121
127
  error: expose ? err.name : getHttpStatusMessage(status),
@@ -127,8 +133,21 @@ export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
127
133
  if (expose && details) {
128
134
  body.details = details;
129
135
  }
136
+ }
130
137
 
131
- if (includeStack && err.stack) {
138
+ if (includeStack && err.stack) {
139
+ // SEC-14: fail closed — a stack trace is ignored in production
140
+ // regardless of the caller's configuration, since it maps internal
141
+ // paths, package layout, and dependency versions for the client.
142
+ if (isProduction) {
143
+ if (!warnedIncludeStackInProduction) {
144
+ warnedIncludeStackInProduction = true;
145
+ console.warn(
146
+ '[@nextrush/errors] includeStack: true was ignored because isProduction is true. ' +
147
+ 'Stack traces are never emitted in production regardless of configuration.'
148
+ );
149
+ }
150
+ } else {
132
151
  body.stack = err.stack.split('\n').map((line) => line.trim());
133
152
  }
134
153
  }
@@ -136,6 +155,34 @@ export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
136
155
  ctx.json(body);
137
156
  }
138
157
  };
158
+
159
+ /**
160
+ * Boot-time verdict for `includeStack: true` (task 8.1): the per-request
161
+ * guard (SEC-14, line ~140 above) only fires when `isProduction` is
162
+ * explicitly passed to this call — an app that never threads
163
+ * `app.options.env` through never gets that guard's warning even in a real
164
+ * production deployment. This is the gap the boot audit closes.
165
+ */
166
+ function auditVerdict(): SecurityAuditVerdict {
167
+ if (includeStack && !isProduction) {
168
+ return {
169
+ level: 'warn',
170
+ message:
171
+ 'errorHandler({ includeStack: true }) was constructed without isProduction — the ' +
172
+ 'per-request guard that ignores includeStack in production never fires unless ' +
173
+ "isProduction is explicitly threaded through (e.g. isProduction: app.isProduction). " +
174
+ 'Wire it, or this instance will leak stack traces in production.',
175
+ };
176
+ }
177
+ return { level: 'ok' };
178
+ }
179
+
180
+ Object.defineProperty(handler, SECURITY_AUDIT, {
181
+ value: auditVerdict,
182
+ enumerable: false,
183
+ });
184
+
185
+ return handler;
139
186
  }
140
187
 
141
188
  /**
@@ -162,22 +209,3 @@ export function notFoundHandler(message = 'Not Found'): Middleware {
162
209
  };
163
210
  }
164
211
 
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
  /**