@fishka/express 0.9.5 → 0.9.7
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/dist/cjs/api.types.d.ts +83 -0
- package/dist/cjs/api.types.js +38 -2
- package/dist/cjs/auth/auth-strategy.d.ts +42 -0
- package/dist/cjs/auth/auth-strategy.js +2 -2
- package/dist/cjs/auth/auth.types.d.ts +42 -0
- package/dist/cjs/auth/auth.utils.d.ts +31 -0
- package/dist/cjs/auth/auth.utils.js +3 -3
- package/dist/cjs/auth/bearer-auth-strategy.d.ts +38 -0
- package/dist/cjs/auth/bearer-auth-strategy.js +2 -2
- package/dist/cjs/config.d.ts +23 -0
- package/dist/cjs/error-handling.d.ts +9 -0
- package/dist/cjs/error-handling.js +4 -4
- package/dist/cjs/http-status-codes.d.ts +179 -0
- package/dist/cjs/http-status-codes.js +228 -0
- package/dist/cjs/index.d.ts +15 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/rate-limit/in-memory-rate-limiter.d.ts +35 -0
- package/dist/cjs/rate-limit/rate-limit.d.ts +19 -0
- package/dist/cjs/rate-limit/rate-limit.types.d.ts +23 -0
- package/dist/cjs/route-table.d.ts +23 -0
- package/dist/cjs/router.d.ts +110 -0
- package/dist/cjs/router.js +11 -11
- package/dist/cjs/thread-local/thread-local-storage-middleware.d.ts +9 -0
- package/dist/cjs/thread-local/thread-local-storage.d.ts +25 -0
- package/dist/cjs/utils/conversion.utils.d.ts +14 -0
- package/dist/cjs/utils/express.utils.d.ts +7 -0
- package/dist/esm/api.types.d.ts +83 -0
- package/dist/esm/api.types.js +37 -2
- package/dist/esm/auth/auth-strategy.d.ts +42 -0
- package/dist/esm/auth/auth-strategy.js +2 -2
- package/dist/esm/auth/auth.types.d.ts +42 -0
- package/dist/esm/auth/auth.utils.d.ts +31 -0
- package/dist/esm/auth/auth.utils.js +3 -3
- package/dist/esm/auth/bearer-auth-strategy.d.ts +38 -0
- package/dist/esm/auth/bearer-auth-strategy.js +2 -2
- package/dist/esm/config.d.ts +23 -0
- package/dist/esm/error-handling.d.ts +9 -0
- package/dist/esm/error-handling.js +4 -4
- package/dist/esm/http-status-codes.d.ts +179 -0
- package/dist/esm/http-status-codes.js +224 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/rate-limit/in-memory-rate-limiter.d.ts +35 -0
- package/dist/esm/rate-limit/rate-limit.d.ts +19 -0
- package/dist/esm/rate-limit/rate-limit.types.d.ts +23 -0
- package/dist/esm/route-table.d.ts +23 -0
- package/dist/esm/router.d.ts +110 -0
- package/dist/esm/router.js +12 -12
- package/dist/esm/thread-local/thread-local-storage-middleware.d.ts +9 -0
- package/dist/esm/thread-local/thread-local-storage.d.ts +25 -0
- package/dist/esm/utils/conversion.utils.d.ts +14 -0
- package/dist/esm/utils/express.utils.d.ts +7 -0
- package/package.json +1 -1
- package/dist/cjs/http.types.js +0 -38
- package/dist/esm/http.types.js +0 -35
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ValueAssertion } from '@fishka/assertions';
|
|
2
|
+
export type UrlTokensValidator = Record<string, ValueAssertion<string>>;
|
|
3
|
+
export declare class HttpError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly details?: Record<string, unknown> | undefined;
|
|
6
|
+
constructor(status: number, message: string, details?: Record<string, unknown> | undefined);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Asserts that a condition is true, throwing an HttpError with the specified status code if false.
|
|
10
|
+
* This function is designed for HTTP-specific validation where you want to throw appropriate HTTP status codes.
|
|
11
|
+
*
|
|
12
|
+
* @param condition - The condition to check. If false, an HttpError will be thrown.
|
|
13
|
+
* @param status - The HTTP status code to use in the HttpError (e.g., 400 for Bad Request, 404 for Not Found).
|
|
14
|
+
* @param message - The error message to include in the HttpError.
|
|
15
|
+
*
|
|
16
|
+
* @throws {HttpError} If the condition is false, throws an HttpError with the specified status and message.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // Validate required parameter
|
|
21
|
+
* assertHttp(typeof userId === 'string', HTTP_BAD_REQUEST, 'User ID must be a string');
|
|
22
|
+
*
|
|
23
|
+
* // Validate resource existence
|
|
24
|
+
* assertHttp(user !== null, HTTP_NOT_FOUND, 'User not found');
|
|
25
|
+
*
|
|
26
|
+
* // Validate authorization
|
|
27
|
+
* assertHttp(user.isAdmin, HTTP_FORBIDDEN, 'Admin access required');
|
|
28
|
+
*
|
|
29
|
+
* // Validate authentication
|
|
30
|
+
* assertHttp(req.headers.authorization, HTTP_UNAUTHORIZED, 'Authorization header required');
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @see {@link HttpError}
|
|
34
|
+
* @see {@link HTTP_BAD_REQUEST}
|
|
35
|
+
* @see {@link HTTP_NOT_FOUND}
|
|
36
|
+
* @see {@link HTTP_FORBIDDEN}
|
|
37
|
+
* @see {@link HTTP_UNAUTHORIZED}
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertHttp(condition: boolean, status: number, message: string): void;
|
|
40
|
+
export interface ApiResponse<ResponseEntity = unknown> {
|
|
41
|
+
/** Result of the call. A single entity for non-paginated ${by-id} requests or an array for list queries. */
|
|
42
|
+
result: ResponseEntity;
|
|
43
|
+
/**
|
|
44
|
+
* Unique ID of the request.
|
|
45
|
+
* Automatically added to every API response.
|
|
46
|
+
* May be passed via 'x-request-id' header from client.
|
|
47
|
+
*/
|
|
48
|
+
requestId?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Response status code. Same as HTTP response status.
|
|
51
|
+
* Default: 200 for successful responses or 500 for internal server errors.
|
|
52
|
+
*/
|
|
53
|
+
status?: number;
|
|
54
|
+
/** Optional error message. */
|
|
55
|
+
error?: string;
|
|
56
|
+
/** Optional structured error details. */
|
|
57
|
+
details?: Record<string, unknown>;
|
|
58
|
+
/** Offset in the result set. Save as 'offset' query parameter. */
|
|
59
|
+
offset?: number;
|
|
60
|
+
/** Number of results requested. Same as 'limit' query parameter. */
|
|
61
|
+
limit?: number;
|
|
62
|
+
}
|
|
63
|
+
/** Converts an API response value into a standardized ApiResponse structure. */
|
|
64
|
+
export declare function response<T = unknown>(result: T): ApiResponse<T>;
|
|
65
|
+
/** Globally identified URL (path or query) parameter info. */
|
|
66
|
+
export interface UrlParameterInfo {
|
|
67
|
+
/** Optional global validator for this parameter. */
|
|
68
|
+
validator?: ValueAssertion<string>;
|
|
69
|
+
/** Description for documentation. */
|
|
70
|
+
description?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Default documentation and validation for URL parameters.
|
|
74
|
+
* @Internal
|
|
75
|
+
*/
|
|
76
|
+
export declare const URL_PARAMETER_INFO: Record<string, UrlParameterInfo>;
|
|
77
|
+
/** Registers a new URL parameter. */
|
|
78
|
+
export declare function registerUrlParameter(name: string, info: UrlParameterInfo): void;
|
|
79
|
+
/**
|
|
80
|
+
* Asserts that the value is a registered URL parameter name.
|
|
81
|
+
* @Internal
|
|
82
|
+
*/
|
|
83
|
+
export declare function assertUrlParameter(name: unknown): asserts name is string;
|
package/dist/esm/api.types.js
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
import { assertString, assertTruthy } from '@fishka/assertions';
|
|
2
|
+
import { HTTP_BAD_REQUEST } from './http-status-codes';
|
|
2
3
|
export class HttpError extends Error {
|
|
3
4
|
constructor(status, message, details) {
|
|
4
5
|
super(message);
|
|
5
6
|
this.status = status;
|
|
6
7
|
this.details = details;
|
|
7
|
-
// Restore prototype chain for instanceof checks
|
|
8
|
+
// Restore the prototype chain for instanceof checks.
|
|
8
9
|
Object.setPrototypeOf(this, HttpError.prototype);
|
|
9
10
|
}
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Asserts that a condition is true, throwing an HttpError with the specified status code if false.
|
|
14
|
+
* This function is designed for HTTP-specific validation where you want to throw appropriate HTTP status codes.
|
|
15
|
+
*
|
|
16
|
+
* @param condition - The condition to check. If false, an HttpError will be thrown.
|
|
17
|
+
* @param status - The HTTP status code to use in the HttpError (e.g., 400 for Bad Request, 404 for Not Found).
|
|
18
|
+
* @param message - The error message to include in the HttpError.
|
|
19
|
+
*
|
|
20
|
+
* @throws {HttpError} If the condition is false, throws an HttpError with the specified status and message.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // Validate required parameter
|
|
25
|
+
* assertHttp(typeof userId === 'string', HTTP_BAD_REQUEST, 'User ID must be a string');
|
|
26
|
+
*
|
|
27
|
+
* // Validate resource existence
|
|
28
|
+
* assertHttp(user !== null, HTTP_NOT_FOUND, 'User not found');
|
|
29
|
+
*
|
|
30
|
+
* // Validate authorization
|
|
31
|
+
* assertHttp(user.isAdmin, HTTP_FORBIDDEN, 'Admin access required');
|
|
32
|
+
*
|
|
33
|
+
* // Validate authentication
|
|
34
|
+
* assertHttp(req.headers.authorization, HTTP_UNAUTHORIZED, 'Authorization header required');
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @see {@link HttpError}
|
|
38
|
+
* @see {@link HTTP_BAD_REQUEST}
|
|
39
|
+
* @see {@link HTTP_NOT_FOUND}
|
|
40
|
+
* @see {@link HTTP_FORBIDDEN}
|
|
41
|
+
* @see {@link HTTP_UNAUTHORIZED}
|
|
42
|
+
*/
|
|
43
|
+
export function assertHttp(condition, status, message) {
|
|
44
|
+
assertTruthy(condition, () => new HttpError(status, message));
|
|
45
|
+
}
|
|
11
46
|
/** Converts an API response value into a standardized ApiResponse structure. */
|
|
12
47
|
export function response(result) {
|
|
13
48
|
return { result };
|
|
@@ -27,5 +62,5 @@ export function registerUrlParameter(name, info) {
|
|
|
27
62
|
*/
|
|
28
63
|
export function assertUrlParameter(name) {
|
|
29
64
|
assertString(name, 'Url parameter name must be a string');
|
|
30
|
-
|
|
65
|
+
assertHttp(URL_PARAMETER_INFO[name] !== undefined, HTTP_BAD_REQUEST, `Invalid URL parameter: '${name}'. Please register it using 'registerUrlParameter('${name}', ...)'`);
|
|
31
66
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ExpressRequest } from '../utils/express.utils';
|
|
2
|
+
import { AuthStrategy, AuthUser } from './auth.types';
|
|
3
|
+
/**
|
|
4
|
+
* Basic authentication strategy using username/password validation.
|
|
5
|
+
* Parses HTTP Basic Authorization header and validates credentials.
|
|
6
|
+
*
|
|
7
|
+
* Example usage:
|
|
8
|
+
* ```
|
|
9
|
+
* const strategy = new BasicAuthStrategy(
|
|
10
|
+
* async (username, password) => {
|
|
11
|
+
* const user = await db.users.findByUsername(username);
|
|
12
|
+
* if (user && await bcrypt.compare(password, user.hash)) {
|
|
13
|
+
* return user;
|
|
14
|
+
* }
|
|
15
|
+
* return null;
|
|
16
|
+
* }
|
|
17
|
+
* );
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare class BasicAuthStrategy<User extends AuthUser = AuthUser> implements AuthStrategy<{
|
|
21
|
+
username: string;
|
|
22
|
+
password: string;
|
|
23
|
+
}, User> {
|
|
24
|
+
private readonly verifyFn;
|
|
25
|
+
constructor(verifyFn: (username: string, password: string) => Promise<User | null>);
|
|
26
|
+
/**
|
|
27
|
+
* Extracts username and password from Basic auth header.
|
|
28
|
+
* Expected format: "Basic base64(username:password)"
|
|
29
|
+
* Returns undefined if header is missing or not Basic.
|
|
30
|
+
*/
|
|
31
|
+
extractCredentials(req: ExpressRequest): {
|
|
32
|
+
username: string;
|
|
33
|
+
password: string;
|
|
34
|
+
} | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Validates the extracted credentials using the provided validation function.
|
|
37
|
+
*/
|
|
38
|
+
validateCredentials({ username, password }: {
|
|
39
|
+
username: string;
|
|
40
|
+
password: string;
|
|
41
|
+
}): Promise<User>;
|
|
42
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpError } from '../api.types';
|
|
2
|
-
import {
|
|
2
|
+
import { HTTP_UNAUTHORIZED } from '../http-status-codes';
|
|
3
3
|
/**
|
|
4
4
|
* Basic authentication strategy using username/password validation.
|
|
5
5
|
* Parses HTTP Basic Authorization header and validates credentials.
|
|
@@ -51,7 +51,7 @@ export class BasicAuthStrategy {
|
|
|
51
51
|
async validateCredentials({ username, password }) {
|
|
52
52
|
const user = await this.verifyFn(username, password);
|
|
53
53
|
if (!user) {
|
|
54
|
-
throw new HttpError(
|
|
54
|
+
throw new HttpError(HTTP_UNAUTHORIZED, 'Invalid username or password');
|
|
55
55
|
}
|
|
56
56
|
return user;
|
|
57
57
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ExpressRequest } from '../utils/express.utils';
|
|
2
|
+
/**
|
|
3
|
+
* Interface representing the authenticated user.
|
|
4
|
+
* Users of the library should use module augmentation to add fields to this interface.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
* ```ts
|
|
8
|
+
* declare module '@fishka/express' {
|
|
9
|
+
* interface AuthUser {
|
|
10
|
+
* id: string;
|
|
11
|
+
* roles: string[];
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export interface AuthUser {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generic authentication strategy interface.
|
|
20
|
+
* Allows users to implement custom authentication logic.
|
|
21
|
+
*
|
|
22
|
+
* @template Credentials - The type of credentials extracted from the request
|
|
23
|
+
* @template User - The type of the authenticated user/entity
|
|
24
|
+
*/
|
|
25
|
+
export interface AuthStrategy<Credentials = unknown, User extends AuthUser = AuthUser> {
|
|
26
|
+
/**
|
|
27
|
+
* Extracts credentials from the Express request.
|
|
28
|
+
* This might parse Authorization headers, cookies, API keys, etc.
|
|
29
|
+
*
|
|
30
|
+
* @param req - Express request object
|
|
31
|
+
* @returns Extracted credentials, or undefined if not found/applicable
|
|
32
|
+
*/
|
|
33
|
+
extractCredentials(req: ExpressRequest): Credentials | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Validates the extracted credentials and returns the authenticated user/entity.
|
|
36
|
+
*
|
|
37
|
+
* @param credentials - Credentials to validate
|
|
38
|
+
* @returns Authenticated user/entity
|
|
39
|
+
* @throws Error if credentials are invalid
|
|
40
|
+
*/
|
|
41
|
+
validateCredentials(credentials: Credentials): Promise<User>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EndpointMiddleware, RequestContext } from '../router';
|
|
2
|
+
import { AuthStrategy, AuthUser } from './auth.types';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a middleware that enforces authentication using the provided strategy.
|
|
5
|
+
* The authenticated user is stored in the context under the 'authUser' key.
|
|
6
|
+
*
|
|
7
|
+
* @template User - Type of the authenticated user
|
|
8
|
+
* @param strategy - Authentication strategy to use
|
|
9
|
+
* @param onSuccess - Optional callback to process authenticated user
|
|
10
|
+
* @returns a middleware that enforces authentication
|
|
11
|
+
*/
|
|
12
|
+
export declare function createAuthMiddleware<User extends AuthUser = AuthUser>(strategy: AuthStrategy<unknown, User>, onSuccess?: (user: User, context: RequestContext) => void): EndpointMiddleware;
|
|
13
|
+
/**
|
|
14
|
+
* Extracts the authenticated user from the request context.
|
|
15
|
+
* Throws if the user is not present (i.e., authentication was not performed).
|
|
16
|
+
*
|
|
17
|
+
* @template User - Type of the authenticated user
|
|
18
|
+
* @param context - Request context
|
|
19
|
+
* @returns The authenticated user
|
|
20
|
+
* @throws Error if user is not found in context
|
|
21
|
+
*/
|
|
22
|
+
export declare function getAuthUser<User extends AuthUser = AuthUser>(context: RequestContext): User;
|
|
23
|
+
/**
|
|
24
|
+
* Safely extracts the authenticated user from the request context.
|
|
25
|
+
* Returns undefined if the user is not present.
|
|
26
|
+
*
|
|
27
|
+
* @template User - Type of the authenticated user
|
|
28
|
+
* @param context - Request context
|
|
29
|
+
* @returns The authenticated user, or undefined if not found
|
|
30
|
+
*/
|
|
31
|
+
export declare function tryGetAuthUser<User extends AuthUser = AuthUser>(context: RequestContext): User | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpError } from '../api.types';
|
|
2
|
-
import {
|
|
2
|
+
import { HTTP_UNAUTHORIZED } from '../http-status-codes';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a middleware that enforces authentication using the provided strategy.
|
|
5
5
|
* The authenticated user is stored in the context under the 'authUser' key.
|
|
@@ -16,7 +16,7 @@ export function createAuthMiddleware(strategy, onSuccess) {
|
|
|
16
16
|
// If no credentials found (and strategy returned undefined), we must deny access here.
|
|
17
17
|
// In a composite strategy scenario, we might want to try the next strategy, but this helper is for a single strategy enforcement.
|
|
18
18
|
if (!credentials) {
|
|
19
|
-
throw new HttpError(
|
|
19
|
+
throw new HttpError(HTTP_UNAUTHORIZED, 'No credentials provided or invalid format');
|
|
20
20
|
}
|
|
21
21
|
// Validate credentials and get authenticated user
|
|
22
22
|
const user = await strategy.validateCredentials(credentials);
|
|
@@ -42,7 +42,7 @@ export function createAuthMiddleware(strategy, onSuccess) {
|
|
|
42
42
|
export function getAuthUser(context) {
|
|
43
43
|
const user = context.authUser;
|
|
44
44
|
if (!user) {
|
|
45
|
-
throw new HttpError(
|
|
45
|
+
throw new HttpError(HTTP_UNAUTHORIZED, 'User not found in context. Did you add auth middleware?');
|
|
46
46
|
}
|
|
47
47
|
return user;
|
|
48
48
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ExpressRequest } from '../utils/express.utils';
|
|
2
|
+
import { AuthStrategy, AuthUser } from './auth.types';
|
|
3
|
+
/**
|
|
4
|
+
* Bearer authentication strategy (commonly used for JWTs).
|
|
5
|
+
* Extracts the token from the 'Authorization: Bearer <token>' header.
|
|
6
|
+
*
|
|
7
|
+
* The validation logic is delegated to the `verifyFn`, which can:
|
|
8
|
+
* - Validate a JWT signature locally.
|
|
9
|
+
* - Call an external API/website to verify the token (Introspection/UserInfo).
|
|
10
|
+
*
|
|
11
|
+
* Example usage:
|
|
12
|
+
* ```ts
|
|
13
|
+
* const strategy = new BearerAuthStrategy(async (token) => {
|
|
14
|
+
* // Call external website to validate
|
|
15
|
+
* const response = await fetch('https://auth.example.com/verify', {
|
|
16
|
+
* headers: { Authorization: `Bearer ${token}` }
|
|
17
|
+
* });
|
|
18
|
+
* if (!response.ok) return null;
|
|
19
|
+
* return await response.json();
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class BearerAuthStrategy<User extends AuthUser = AuthUser> implements AuthStrategy<string, User> {
|
|
24
|
+
private readonly verifyFn;
|
|
25
|
+
/**
|
|
26
|
+
* @param verifyFn Function to validate the token. Returns the user if valid, or null if invalid.
|
|
27
|
+
*/
|
|
28
|
+
constructor(verifyFn: (token: string) => Promise<User | null>);
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the Bearer token from the Authorization header.
|
|
31
|
+
* Returns undefined if the header is missing or not a Bearer token.
|
|
32
|
+
*/
|
|
33
|
+
extractCredentials(req: ExpressRequest): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Validates the extracted token using the provided verification function.
|
|
36
|
+
*/
|
|
37
|
+
validateCredentials(token: string): Promise<User>;
|
|
38
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpError } from '../api.types';
|
|
2
|
-
import {
|
|
2
|
+
import { HTTP_UNAUTHORIZED } from '../http-status-codes';
|
|
3
3
|
/**
|
|
4
4
|
* Bearer authentication strategy (commonly used for JWTs).
|
|
5
5
|
* Extracts the token from the 'Authorization: Bearer <token>' header.
|
|
@@ -48,7 +48,7 @@ export class BearerAuthStrategy {
|
|
|
48
48
|
async validateCredentials(token) {
|
|
49
49
|
const user = await this.verifyFn(token);
|
|
50
50
|
if (!user) {
|
|
51
|
-
throw new HttpError(
|
|
51
|
+
throw new HttpError(HTTP_UNAUTHORIZED, 'Invalid token');
|
|
52
52
|
}
|
|
53
53
|
return user;
|
|
54
54
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface GlobalExpressApiConfig {
|
|
2
|
+
/**
|
|
3
|
+
* Whether to trust and use the request ID from the request header.
|
|
4
|
+
* If true, the middleware will look for 'x-request-id' and use it.
|
|
5
|
+
* If false, a new UUID will always be generated.
|
|
6
|
+
* Default: true
|
|
7
|
+
*/
|
|
8
|
+
trustRequestIdHeader: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Configure global @fishka/express settings.
|
|
12
|
+
* @param config Partial configuration to merge with current settings
|
|
13
|
+
*/
|
|
14
|
+
export declare function configureExpressApi(config: Partial<GlobalExpressApiConfig>): void;
|
|
15
|
+
/**
|
|
16
|
+
* Get current Express API configuration.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getExpressApiConfig(): GlobalExpressApiConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Reset API configuration to defaults.
|
|
21
|
+
* Useful for testing.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resetExpressApiConfig(): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NextFunction } from 'express';
|
|
2
|
+
import { ExpressFunction, ExpressRequest, ExpressResponse } from './utils/express.utils';
|
|
3
|
+
/** Catches all kinds of unprocessed exceptions thrown from a single route. */
|
|
4
|
+
export declare function catchRouteErrors(fn: ExpressFunction): ExpressFunction;
|
|
5
|
+
/**
|
|
6
|
+
* Catches all errors in Express.js and is installed as global middleware.
|
|
7
|
+
* Note that individual routes are wrapped with 'catchRouteErrors' middleware.
|
|
8
|
+
*/
|
|
9
|
+
export declare function catchAllMiddleware(error: unknown, _: ExpressRequest, res: ExpressResponse, next: NextFunction): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getMessageFromError } from '@fishka/assertions';
|
|
2
2
|
import { HttpError } from './api.types';
|
|
3
|
-
import {
|
|
3
|
+
import { HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR } from './http-status-codes';
|
|
4
4
|
import { getRequestLocalStorage } from './thread-local/thread-local-storage';
|
|
5
5
|
import { wrapAsApiResponse } from './utils/conversion.utils';
|
|
6
6
|
function buildApiResponse(error) {
|
|
@@ -20,7 +20,7 @@ function buildApiResponse(error) {
|
|
|
20
20
|
response = {
|
|
21
21
|
...wrapAsApiResponse(undefined),
|
|
22
22
|
error: errorMessage && errorMessage.length > 0 ? errorMessage : 'Internal error',
|
|
23
|
-
status:
|
|
23
|
+
status: HTTP_INTERNAL_SERVER_ERROR,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
if (requestId) {
|
|
@@ -36,7 +36,7 @@ export function catchRouteErrors(fn) {
|
|
|
36
36
|
}
|
|
37
37
|
catch (error) {
|
|
38
38
|
const apiResponse = buildApiResponse(error);
|
|
39
|
-
if (apiResponse.status >=
|
|
39
|
+
if (apiResponse.status >= HTTP_INTERNAL_SERVER_ERROR) {
|
|
40
40
|
console.error(`catchRouteErrors: ${req.path}`, error);
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
@@ -59,7 +59,7 @@ export async function catchAllMiddleware(error, _, res, next) {
|
|
|
59
59
|
// Report as critical. This kind of error should never happen.
|
|
60
60
|
console.error('catchAllMiddleware:', getMessageFromError(error));
|
|
61
61
|
const apiResponse = error instanceof SyntaxError // JSON body parsing error.
|
|
62
|
-
? buildApiResponse(`${
|
|
62
|
+
? buildApiResponse(`${HTTP_BAD_REQUEST}: Failed to parse request: ${error.message}`)
|
|
63
63
|
: buildApiResponse(error);
|
|
64
64
|
res.status(apiResponse.status);
|
|
65
65
|
res.send(apiResponse);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/** The request has succeeded (200) */
|
|
2
|
+
export declare const HTTP_OK = 200;
|
|
3
|
+
/** The request has been fulfilled, and a new resource is created (201) */
|
|
4
|
+
export declare const HTTP_CREATED = 201;
|
|
5
|
+
/** The request has been accepted for processing, but the processing is not yet complete (202) */
|
|
6
|
+
export declare const HTTP_ACCEPTED = 202;
|
|
7
|
+
/** The server successfully processed the request, and is not returning any content (204) */
|
|
8
|
+
export declare const HTTP_NO_CONTENT = 204;
|
|
9
|
+
/** The request has been successfully processed, but is returning no content (205) */
|
|
10
|
+
export declare const HTTP_RESET_CONTENT = 205;
|
|
11
|
+
/** The server is delivering only part of the resource due to a range header sent by the client (206) */
|
|
12
|
+
export declare const HTTP_PARTIAL_CONTENT = 206;
|
|
13
|
+
/** The IM used the server processed a request successfully, but the response is transformed (226) */
|
|
14
|
+
export declare const HTTP_IM_USED = 226;
|
|
15
|
+
/** The resource has been moved permanently to a new URL (301) */
|
|
16
|
+
export declare const HTTP_MOVED_PERMANENTLY = 301;
|
|
17
|
+
/** The resource is available at a different URL (302) */
|
|
18
|
+
export declare const HTTP_FOUND = 302;
|
|
19
|
+
/** The resource has not been modified since the last request (304) */
|
|
20
|
+
export declare const HTTP_NOT_MODIFIED = 304;
|
|
21
|
+
/** The server could not understand the request due to invalid syntax (400) */
|
|
22
|
+
export declare const HTTP_BAD_REQUEST = 400;
|
|
23
|
+
/** The client must authenticate itself to get the requested response (401) */
|
|
24
|
+
export declare const HTTP_UNAUTHORIZED = 401;
|
|
25
|
+
/** The client does not have access rights to the content (403) */
|
|
26
|
+
export declare const HTTP_FORBIDDEN = 403;
|
|
27
|
+
/** The server cannot find the requested resource (404) */
|
|
28
|
+
export declare const HTTP_NOT_FOUND = 404;
|
|
29
|
+
/** The request method is known by the server but is not supported by the target resource (405) */
|
|
30
|
+
export declare const HTTP_METHOD_NOT_ALLOWED = 405;
|
|
31
|
+
/** The client must authenticate using a proxy (407) */
|
|
32
|
+
export declare const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
|
|
33
|
+
/** The server timed out waiting for the request (408) */
|
|
34
|
+
export declare const HTTP_REQUEST_TIMEOUT = 408;
|
|
35
|
+
/** The request conflicts with the current state of the server (409) */
|
|
36
|
+
export declare const HTTP_CONFLICT = 409;
|
|
37
|
+
/** The resource requested is no longer available and will not be available again (410) */
|
|
38
|
+
export declare const HTTP_GONE = 410;
|
|
39
|
+
/** The request does not meet the preconditions that the server requires (412) */
|
|
40
|
+
export declare const HTTP_PRECONDITION_FAILED = 412;
|
|
41
|
+
/** The client sent a request that is too large for the server to process (413) */
|
|
42
|
+
export declare const HTTP_PAYLOAD_TOO_LARGE = 413;
|
|
43
|
+
/** The URI requested by the client is too long for the server to process (414) */
|
|
44
|
+
export declare const HTTP_URI_TOO_LONG = 414;
|
|
45
|
+
/** The media format of the requested data is not supported by the server (415) */
|
|
46
|
+
export declare const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
|
|
47
|
+
/** The range specified by the client cannot be fulfilled (416) */
|
|
48
|
+
export declare const HTTP_RANGE_NOT_SATISFIABLE = 416;
|
|
49
|
+
/** The expectation given in the request header could not be met by the server (417) */
|
|
50
|
+
export declare const HTTP_EXPECTATION_FAILED = 417;
|
|
51
|
+
/** The server refuses to brew coffee because it is a teapot (418) - an Easter egg from RFC 2324 */
|
|
52
|
+
export declare const HTTP_IM_A_TEAPOT = 418;
|
|
53
|
+
/** The request was well-formed but was unable to be followed due to semantic errors (422) */
|
|
54
|
+
export declare const HTTP_UNPROCESSABLE_ENTITY = 422;
|
|
55
|
+
/** The resource that is being accessed is locked (423) */
|
|
56
|
+
export declare const HTTP_LOCKED = 423;
|
|
57
|
+
/** The resource requested is dependent on another resource that has failed (424) */
|
|
58
|
+
export declare const HTTP_FAILED_DEPENDENCY = 424;
|
|
59
|
+
/** The server is unwilling to risk processing a request that might be replayed (425) */
|
|
60
|
+
export declare const HTTP_TOO_EARLY = 425;
|
|
61
|
+
/** The client needs to upgrade its protocol (426) */
|
|
62
|
+
export declare const HTTP_UPGRADE_REQUIRED = 426;
|
|
63
|
+
/** The server requires the request to be conditional (428) */
|
|
64
|
+
export declare const HTTP_PRECONDITION_REQUIRED = 428;
|
|
65
|
+
/** The client has sent too many requests in a given amount of time (429) */
|
|
66
|
+
export declare const HTTP_TOO_MANY_REQUESTS = 429;
|
|
67
|
+
/** The request header fields are too large (431) */
|
|
68
|
+
export declare const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
|
|
69
|
+
/** The client closed the connection with the server before the request was completed (444) */
|
|
70
|
+
export declare const HTTP_CONNECTION_CLOSED_WITHOUT_RESPONSE = 444;
|
|
71
|
+
/** The client requested an unavailable legal action (451) */
|
|
72
|
+
export declare const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
|
|
73
|
+
/** The server encountered an internal error and was unable to complete the request (500) */
|
|
74
|
+
export declare const HTTP_INTERNAL_SERVER_ERROR = 500;
|
|
75
|
+
/** The request method is not supported by the server and cannot be handled (501) */
|
|
76
|
+
export declare const HTTP_NOT_IMPLEMENTED = 501;
|
|
77
|
+
/** The server, while acting as a gateway or proxy, received an invalid response from the upstream server (502) */
|
|
78
|
+
export declare const HTTP_BAD_GATEWAY = 502;
|
|
79
|
+
/** The server is not ready to handle the request, typically due to temporary overload or maintenance (503) */
|
|
80
|
+
export declare const HTTP_SERVICE_UNAVAILABLE = 503;
|
|
81
|
+
/** The server, while acting as a gateway or proxy, did not get a response in time from the upstream server (504) */
|
|
82
|
+
export declare const HTTP_GATEWAY_TIMEOUT = 504;
|
|
83
|
+
/**
|
|
84
|
+
* An object containing common HTTP status codes.
|
|
85
|
+
* These constants can be used to improve code readability and maintainability.
|
|
86
|
+
* @deprecated Use individual HTTP_* constants for better tree-shaking
|
|
87
|
+
*/
|
|
88
|
+
export declare const HttpStatusCode: {
|
|
89
|
+
/** The request has succeeded (200) */
|
|
90
|
+
OK: number;
|
|
91
|
+
/** The request has been fulfilled, and a new resource is created (201) */
|
|
92
|
+
CREATED: number;
|
|
93
|
+
/** The request has been accepted for processing, but the processing is not yet complete (202) */
|
|
94
|
+
ACCEPTED: number;
|
|
95
|
+
/** The server successfully processed the request, and is not returning any content (204) */
|
|
96
|
+
NO_CONTENT: number;
|
|
97
|
+
/** The request has been successfully processed, but is returning no content (205) */
|
|
98
|
+
RESET_CONTENT: number;
|
|
99
|
+
/** The server is delivering only part of the resource due to a range header sent by the client (206) */
|
|
100
|
+
PARTIAL_CONTENT: number;
|
|
101
|
+
/** The IM used the server processed a request successfully, but the response is transformed (226) */
|
|
102
|
+
IM_USED: number;
|
|
103
|
+
/** The resource has been moved permanently to a new URL (301) */
|
|
104
|
+
MOVED_PERMANENTLY: number;
|
|
105
|
+
/** The resource is available at a different URL (302) */
|
|
106
|
+
FOUND: number;
|
|
107
|
+
/** The resource has not been modified since the last request (304) */
|
|
108
|
+
NOT_MODIFIED: number;
|
|
109
|
+
/** The server could not understand the request due to invalid syntax (400) */
|
|
110
|
+
BAD_REQUEST: number;
|
|
111
|
+
/** The client must authenticate itself to get the requested response (401) */
|
|
112
|
+
UNAUTHORIZED: number;
|
|
113
|
+
/** The client does not have access rights to the content (403) */
|
|
114
|
+
FORBIDDEN: number;
|
|
115
|
+
/** The server cannot find the requested resource (404) */
|
|
116
|
+
NOT_FOUND: number;
|
|
117
|
+
/** The request method is known by the server but is not supported by the target resource (405) */
|
|
118
|
+
METHOD_NOT_ALLOWED: number;
|
|
119
|
+
/** The client must authenticate using a proxy (407) */
|
|
120
|
+
PROXY_AUTHENTICATION_REQUIRED: number;
|
|
121
|
+
/** The server timed out waiting for the request (408) */
|
|
122
|
+
REQUEST_TIMEOUT: number;
|
|
123
|
+
/** The request conflicts with the current state of the server (409) */
|
|
124
|
+
CONFLICT: number;
|
|
125
|
+
/** The resource requested is no longer available and will not be available again (410) */
|
|
126
|
+
GONE: number;
|
|
127
|
+
/** The request does not meet the preconditions that the server requires (412) */
|
|
128
|
+
PRECONDITION_FAILED: number;
|
|
129
|
+
/** The client sent a request that is too large for the server to process (413) */
|
|
130
|
+
PAYLOAD_TOO_LARGE: number;
|
|
131
|
+
/** The URI requested by the client is too long for the server to process (414) */
|
|
132
|
+
URI_TOO_LONG: number;
|
|
133
|
+
/** The media format of the requested data is not supported by the server (415) */
|
|
134
|
+
UNSUPPORTED_MEDIA_TYPE: number;
|
|
135
|
+
/** The range specified by the client cannot be fulfilled (416) */
|
|
136
|
+
RANGE_NOT_SATISFIABLE: number;
|
|
137
|
+
/** The expectation given in the request header could not be met by the server (417) */
|
|
138
|
+
EXPECTATION_FAILED: number;
|
|
139
|
+
/** The server refuses to brew coffee because it is a teapot (418) - an Easter egg from RFC 2324 */
|
|
140
|
+
IM_A_TEAPOT: number;
|
|
141
|
+
/** The request was well-formed but was unable to be followed due to semantic errors (422) */
|
|
142
|
+
UNPROCESSABLE_ENTITY: number;
|
|
143
|
+
/** The resource that is being accessed is locked (423) */
|
|
144
|
+
LOCKED: number;
|
|
145
|
+
/** The resource requested is dependent on another resource that has failed (424) */
|
|
146
|
+
FAILED_DEPENDENCY: number;
|
|
147
|
+
/** The server is unwilling to risk processing a request that might be replayed (425) */
|
|
148
|
+
TOO_EARLY: number;
|
|
149
|
+
/** The client needs to upgrade its protocol (426) */
|
|
150
|
+
UPGRADE_REQUIRED: number;
|
|
151
|
+
/** The server requires the request to be conditional (428) */
|
|
152
|
+
PRECONDITION_REQUIRED: number;
|
|
153
|
+
/** The client has sent too many requests in a given amount of time (429) */
|
|
154
|
+
TOO_MANY_REQUESTS: number;
|
|
155
|
+
/** The request header fields are too large (431) */
|
|
156
|
+
REQUEST_HEADER_FIELDS_TOO_LARGE: number;
|
|
157
|
+
/** The client closed the connection with the server before the request was completed (444) */
|
|
158
|
+
CONNECTION_CLOSED_WITHOUT_RESPONSE: number;
|
|
159
|
+
/** The client requested an unavailable legal action (451) */
|
|
160
|
+
UNAVAILABLE_FOR_LEGAL_REASONS: number;
|
|
161
|
+
/** The server encountered an internal error and was unable to complete the request (500) */
|
|
162
|
+
INTERNAL_SERVER_ERROR: number;
|
|
163
|
+
/** The request method is not supported by the server and cannot be handled (501) */
|
|
164
|
+
NOT_IMPLEMENTED: number;
|
|
165
|
+
/** The server, while acting as a gateway or proxy, received an invalid response from the upstream server (502) */
|
|
166
|
+
BAD_GATEWAY: number;
|
|
167
|
+
/** The server is not ready to handle the request, typically due to temporary overload or maintenance (503) */
|
|
168
|
+
SERVICE_UNAVAILABLE: number;
|
|
169
|
+
/** The server, while acting as a gateway or proxy, did not get a response in time from the upstream server (504) */
|
|
170
|
+
GATEWAY_TIMEOUT: number;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Returns the reason phrase corresponding to the given HTTP status code.
|
|
174
|
+
* This is useful for converting status codes into human-readable messages.
|
|
175
|
+
*
|
|
176
|
+
* @param statusCode - The HTTP status code for which to get the reason phrase.
|
|
177
|
+
* @returns The reason phrase associated with the given status code, or "Unknown Status Code" if the status code is not recognized.
|
|
178
|
+
*/
|
|
179
|
+
export declare const getHttpStatusCodeReasonPhrase: (statusCode: number) => string;
|