@fishka/express 0.9.5 → 0.9.6
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 +51 -0
- package/dist/cjs/auth/auth-strategy.d.ts +42 -0
- package/dist/cjs/auth/auth.types.d.ts +42 -0
- package/dist/cjs/auth/auth.utils.d.ts +31 -0
- package/dist/cjs/auth/bearer-auth-strategy.d.ts +38 -0
- package/dist/cjs/config.d.ts +23 -0
- package/dist/cjs/error-handling.d.ts +9 -0
- package/dist/cjs/http.types.d.ts +35 -0
- package/dist/cjs/index.d.ts +15 -0
- 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/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 +51 -0
- package/dist/esm/auth/auth-strategy.d.ts +42 -0
- package/dist/esm/auth/auth.types.d.ts +42 -0
- package/dist/esm/auth/auth.utils.d.ts +31 -0
- package/dist/esm/auth/bearer-auth-strategy.d.ts +38 -0
- package/dist/esm/config.d.ts +23 -0
- package/dist/esm/error-handling.d.ts +9 -0
- package/dist/esm/http.types.d.ts +35 -0
- package/dist/esm/index.d.ts +15 -0
- 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/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
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
export interface ApiResponse<ResponseEntity = unknown> {
|
|
9
|
+
/** Result of the call. A single entity for non-paginated ${by-id} requests or an array for list queries. */
|
|
10
|
+
result: ResponseEntity;
|
|
11
|
+
/**
|
|
12
|
+
* Unique ID of the request.
|
|
13
|
+
* Automatically added to every API response.
|
|
14
|
+
* May be passed via 'x-request-id' header from client.
|
|
15
|
+
*/
|
|
16
|
+
requestId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Response status code. Same as HTTP response status.
|
|
19
|
+
* Default: 200 for successful responses or 500 for internal server errors.
|
|
20
|
+
*/
|
|
21
|
+
status?: number;
|
|
22
|
+
/** Optional error message. */
|
|
23
|
+
error?: string;
|
|
24
|
+
/** Optional structured error details. */
|
|
25
|
+
details?: Record<string, unknown>;
|
|
26
|
+
/** Offset in the result set. Save as 'offset' query parameter. */
|
|
27
|
+
offset?: number;
|
|
28
|
+
/** Number of results requested. Same as 'limit' query parameter. */
|
|
29
|
+
limit?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Converts an API response value into a standardized ApiResponse structure. */
|
|
32
|
+
export declare function response<T = unknown>(result: T): ApiResponse<T>;
|
|
33
|
+
/** Globally identified URL (path or query) parameter info. */
|
|
34
|
+
export interface UrlParameterInfo {
|
|
35
|
+
/** Optional global validator for this parameter. */
|
|
36
|
+
validator?: ValueAssertion<string>;
|
|
37
|
+
/** Description for documentation. */
|
|
38
|
+
description?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Default documentation and validation for URL parameters.
|
|
42
|
+
* @Internal
|
|
43
|
+
*/
|
|
44
|
+
export declare const URL_PARAMETER_INFO: Record<string, UrlParameterInfo>;
|
|
45
|
+
/** Registers a new URL parameter. */
|
|
46
|
+
export declare function registerUrlParameter(name: string, info: UrlParameterInfo): void;
|
|
47
|
+
/**
|
|
48
|
+
* Asserts that the value is a registered URL parameter name.
|
|
49
|
+
* @Internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function assertUrlParameter(name: unknown): asserts name is string;
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common HTTP status codes as numbers.
|
|
3
|
+
* @Internal
|
|
4
|
+
*/
|
|
5
|
+
export declare const INTERNAL_ERROR_STATUS = 500;
|
|
6
|
+
/**
|
|
7
|
+
* Common HTTP status codes as numbers.
|
|
8
|
+
* @Internal
|
|
9
|
+
*/
|
|
10
|
+
export declare const BAD_REQUEST_STATUS = 400;
|
|
11
|
+
/**
|
|
12
|
+
* Common HTTP status codes as numbers.
|
|
13
|
+
* @Internal
|
|
14
|
+
*/
|
|
15
|
+
export declare const UNAUTHORIZED_STATUS = 401;
|
|
16
|
+
/**
|
|
17
|
+
* Common HTTP status codes as numbers.
|
|
18
|
+
* @Internal
|
|
19
|
+
*/
|
|
20
|
+
export declare const FORBIDDEN_STATUS = 403;
|
|
21
|
+
/**
|
|
22
|
+
* Common HTTP status codes as numbers.
|
|
23
|
+
* @Internal
|
|
24
|
+
*/
|
|
25
|
+
export declare const NOT_FOUND_STATUS = 404;
|
|
26
|
+
/**
|
|
27
|
+
* Common HTTP status codes as numbers.
|
|
28
|
+
* @Internal
|
|
29
|
+
*/
|
|
30
|
+
export declare const OK_STATUS = 200;
|
|
31
|
+
/**
|
|
32
|
+
* Common HTTP status codes as numbers.
|
|
33
|
+
* @Internal
|
|
34
|
+
*/
|
|
35
|
+
export declare const TOO_MANY_REQUESTS_STATUS = 429;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './api.types';
|
|
2
|
+
export * from './auth/auth-strategy';
|
|
3
|
+
export * from './auth/auth.types';
|
|
4
|
+
export * from './auth/auth.utils';
|
|
5
|
+
export * from './auth/bearer-auth-strategy';
|
|
6
|
+
export * from './config';
|
|
7
|
+
export * from './error-handling';
|
|
8
|
+
export * from './http.types';
|
|
9
|
+
export * from './rate-limit/in-memory-rate-limiter';
|
|
10
|
+
export * from './rate-limit/rate-limit.types';
|
|
11
|
+
export * from './route-table';
|
|
12
|
+
export * from './router';
|
|
13
|
+
export * from './thread-local/thread-local-storage';
|
|
14
|
+
export * from './thread-local/thread-local-storage-middleware';
|
|
15
|
+
export * from './utils/express.utils';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ExpressFunction } from '../utils/express.utils';
|
|
2
|
+
import { RateLimitConfig, RateLimitResult } from './rate-limit.types';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory rate limiter using sliding window counter.
|
|
5
|
+
* Tracks request counts per key with time-based window expiration.
|
|
6
|
+
*/
|
|
7
|
+
declare class InMemoryRateLimiter {
|
|
8
|
+
private readonly limits;
|
|
9
|
+
private readonly points;
|
|
10
|
+
private readonly durationMs;
|
|
11
|
+
constructor(points: number, durationSeconds: number);
|
|
12
|
+
/**
|
|
13
|
+
* Try to consume points from the rate limit.
|
|
14
|
+
* Returns result if successful, throws if limit exceeded.
|
|
15
|
+
*
|
|
16
|
+
* @param key - Unique identifier for the client (e.g., IP address)
|
|
17
|
+
* @returns Rate limit result with remaining points and ms until reset
|
|
18
|
+
* @throws RateLimitResult if limit exceeded
|
|
19
|
+
*/
|
|
20
|
+
consume(key: string): RateLimitResult;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a rate limiter middleware using in-memory implementation.
|
|
24
|
+
*
|
|
25
|
+
* Separate limiters are used for read (GET) and write (POST/PATCH/PUT/DELETE) requests.
|
|
26
|
+
*
|
|
27
|
+
* @param config - Rate limit configuration
|
|
28
|
+
* @returns Express middleware function
|
|
29
|
+
*/
|
|
30
|
+
export declare function createRateLimiterMiddleware(config: RateLimitConfig): Promise<ExpressFunction>;
|
|
31
|
+
/**
|
|
32
|
+
* Export the in-memory limiter class for advanced use cases where
|
|
33
|
+
* you might want direct control over rate limit management.
|
|
34
|
+
*/
|
|
35
|
+
export { InMemoryRateLimiter };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ExpressResponse } from '../utils/express.utils';
|
|
2
|
+
import { RateLimitResult } from './rate-limit.types';
|
|
3
|
+
/**
|
|
4
|
+
* Adds rate limit state headers to the response.
|
|
5
|
+
* See https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
|
|
6
|
+
*
|
|
7
|
+
* Headers added:
|
|
8
|
+
* - X-RateLimit-Limit: Server's quota for requests in the time window
|
|
9
|
+
* - X-RateLimit-Remaining: Remaining quota in the current window
|
|
10
|
+
* - X-RateLimit-Reset: Time remaining in the current window (in seconds)
|
|
11
|
+
* - X-RateLimit-Policy: Quota policies associated with the client
|
|
12
|
+
* @Internal
|
|
13
|
+
*/
|
|
14
|
+
export declare function addRateLimitHeaders(res: ExpressResponse, result: RateLimitResult, limitPoints: number, duration: number): ExpressResponse;
|
|
15
|
+
/**
|
|
16
|
+
* Converts milliseconds to seconds, rounding up.
|
|
17
|
+
* @Internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function msToSeconds(ms: number): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for rate limiting.
|
|
3
|
+
*/
|
|
4
|
+
export interface RateLimitConfig {
|
|
5
|
+
/** Rate limit points per time window */
|
|
6
|
+
points: {
|
|
7
|
+
read: number;
|
|
8
|
+
write: number;
|
|
9
|
+
};
|
|
10
|
+
/** Duration of the rate limit window in seconds */
|
|
11
|
+
duration: number;
|
|
12
|
+
/** Key prefix for rate limit data (optional, defaults to 'rate_limit') */
|
|
13
|
+
keyPrefix?: string;
|
|
14
|
+
/** Paths that should bypass rate limiting (default: ['/v1', '/health']) */
|
|
15
|
+
rateLimitWhitelist?: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Simple in-memory rate limiter result.
|
|
19
|
+
*/
|
|
20
|
+
export interface RateLimitResult {
|
|
21
|
+
remainingPoints: number;
|
|
22
|
+
msBeforeNext: number;
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ExpressApplication } from './utils/express.utils';
|
|
2
|
+
import { DeleteEndpoint, GetEndpoint, PatchEndpoint, PostEndpoint, PutEndpoint, RequestContext, ResponseOrValue } from './router';
|
|
3
|
+
/**
|
|
4
|
+
* Helper utility for organizing and mounting routes.
|
|
5
|
+
* Provides a fluent interface for registering multiple handlers.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RouteTable {
|
|
8
|
+
private readonly app;
|
|
9
|
+
constructor(app: ExpressApplication);
|
|
10
|
+
get<T>(path: string, endpoint: GetEndpoint<T> | GetEndpoint<T[]>): this;
|
|
11
|
+
get<T>(path: string, run: (ctx: RequestContext) => Promise<ResponseOrValue<T>>): this;
|
|
12
|
+
post<Body, Result>(path: string, endpoint: PostEndpoint<Body, Result>): this;
|
|
13
|
+
patch<Body, Result>(path: string, endpoint: PatchEndpoint<Body, Result>): this;
|
|
14
|
+
put<Body, Result>(path: string, endpoint: PutEndpoint<Body, Result>): this;
|
|
15
|
+
delete(path: string, endpoint: DeleteEndpoint): this;
|
|
16
|
+
delete(path: string, run: (ctx: RequestContext) => Promise<void>): this;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Factory function to create a new route table.
|
|
20
|
+
* @param app Express application instance
|
|
21
|
+
* @returns RouteTable instance with fluent API
|
|
22
|
+
*/
|
|
23
|
+
export declare function createRouteTable(app: ExpressApplication): RouteTable;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Assertion, ObjectAssertion } from '@fishka/assertions';
|
|
2
|
+
import { ApiResponse, UrlTokensValidator } from './api.types';
|
|
3
|
+
import { AuthUser } from './auth/auth.types';
|
|
4
|
+
import { ExpressApplication, ExpressRequest, ExpressResponse } from './utils/express.utils';
|
|
5
|
+
/** Express API allows handlers to return response in the raw form. */
|
|
6
|
+
export type ResponseOrValue<ResponseEntity> = ApiResponse<ResponseEntity> | ResponseEntity;
|
|
7
|
+
/**
|
|
8
|
+
* Generic middleware hook for endpoint execution.
|
|
9
|
+
* Allows custom logic like transaction management, authorization checks, etc.
|
|
10
|
+
*/
|
|
11
|
+
export type EndpointMiddleware<Context = RequestContext> = (run: () => Promise<unknown>, context: Context) => Promise<unknown>;
|
|
12
|
+
/** Generic request context passed to all handlers. Database-agnostic and extensible. */
|
|
13
|
+
export interface RequestContext<Body = void> {
|
|
14
|
+
/** Parsed and validated request body (for POST/PATCH/PUT handlers). */
|
|
15
|
+
body: Body;
|
|
16
|
+
/** Express Request object. */
|
|
17
|
+
req: ExpressRequest;
|
|
18
|
+
/** Express Response object. */
|
|
19
|
+
res: ExpressResponse;
|
|
20
|
+
/** Authenticated user (if any). Populated by auth middleware. */
|
|
21
|
+
authUser?: AuthUser;
|
|
22
|
+
/**
|
|
23
|
+
* Generic parameter access with lazy validation.
|
|
24
|
+
* Provides type-safe access to URL path and query parameters.
|
|
25
|
+
*/
|
|
26
|
+
params: {
|
|
27
|
+
get(key: string): string;
|
|
28
|
+
tryGet(key: string): string | undefined;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Query parameter access.
|
|
32
|
+
*/
|
|
33
|
+
query: {
|
|
34
|
+
get(key: string): string | undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Generic state storage for middleware to attach data.
|
|
38
|
+
* Allows middleware to pass information to handlers and other middleware.
|
|
39
|
+
*/
|
|
40
|
+
state: Map<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/** Base interface with common endpoint properties. */
|
|
43
|
+
export interface EndpointBase<Context = RequestContext, Result = unknown> {
|
|
44
|
+
/** Path parameter validator. */
|
|
45
|
+
$path?: UrlTokensValidator;
|
|
46
|
+
/** Query parameter validator. */
|
|
47
|
+
$query?: UrlTokensValidator;
|
|
48
|
+
/** Optional middleware to execute before the handler. */
|
|
49
|
+
middlewares?: Array<EndpointMiddleware>;
|
|
50
|
+
/** Handler function. */
|
|
51
|
+
run: (ctx: Context) => Promise<ResponseOrValue<Result>>;
|
|
52
|
+
}
|
|
53
|
+
/** Descriptor for GET list routes. */
|
|
54
|
+
export type GetListEndpoint<ResultElementType = unknown> = EndpointBase<RequestContext, Array<ResultElementType>>;
|
|
55
|
+
/** Descriptor for GET routes. */
|
|
56
|
+
export type GetEndpoint<Result = unknown> = EndpointBase<RequestContext, Result>;
|
|
57
|
+
/** Descriptor for POST routes. */
|
|
58
|
+
export interface PostEndpoint<Body = unknown, Result = unknown> extends EndpointBase<RequestContext<Body>, Result> {
|
|
59
|
+
/** Request body validator. */
|
|
60
|
+
$body: Body extends object ? ObjectAssertion<Body> : Assertion<Body>;
|
|
61
|
+
}
|
|
62
|
+
/** Same as POST. Used for full object updates. */
|
|
63
|
+
export type PutEndpoint<Body = unknown, Result = unknown> = PostEndpoint<Body, Result>;
|
|
64
|
+
/** Same as PUT. While PUT is used for the whole object update, PATCH is used for a partial update. */
|
|
65
|
+
export type PatchEndpoint<Body = unknown, Result = unknown> = PutEndpoint<Body, Result>;
|
|
66
|
+
/** Descriptor for DELETE routes. */
|
|
67
|
+
export type DeleteEndpoint = EndpointBase<RequestContext, void>;
|
|
68
|
+
/** Union type for all route registration info objects. */
|
|
69
|
+
export type RouteRegistrationInfo = ({
|
|
70
|
+
method: 'get';
|
|
71
|
+
route: GetEndpoint | GetListEndpoint;
|
|
72
|
+
} | {
|
|
73
|
+
method: 'post';
|
|
74
|
+
route: PostEndpoint;
|
|
75
|
+
} | {
|
|
76
|
+
method: 'patch';
|
|
77
|
+
route: PatchEndpoint;
|
|
78
|
+
} | {
|
|
79
|
+
method: 'put';
|
|
80
|
+
route: PutEndpoint;
|
|
81
|
+
} | {
|
|
82
|
+
method: 'delete';
|
|
83
|
+
route: DeleteEndpoint;
|
|
84
|
+
}) & {
|
|
85
|
+
path: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Registers a GET route.
|
|
89
|
+
*/
|
|
90
|
+
export declare const mountGet: (app: ExpressApplication, path: string, endpoint: GetEndpoint | GetListEndpoint) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Registers a POST route.
|
|
93
|
+
*/
|
|
94
|
+
export declare const mountPost: <Body, Result>(app: ExpressApplication, path: string, endpoint: PostEndpoint<Body, Result>) => void;
|
|
95
|
+
/**
|
|
96
|
+
* Registers a PATCH route.
|
|
97
|
+
*/
|
|
98
|
+
export declare const mountPatch: <Body, Result>(app: ExpressApplication, path: string, endpoint: PatchEndpoint<Body, Result>) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Registers a PUT route.
|
|
101
|
+
*/
|
|
102
|
+
export declare const mountPut: <Body, Result>(app: ExpressApplication, path: string, endpoint: PutEndpoint<Body, Result>) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Registers a DELETE route.
|
|
105
|
+
*/
|
|
106
|
+
export declare const mountDelete: (app: ExpressApplication, path: string, endpoint: DeleteEndpoint) => void;
|
|
107
|
+
/**
|
|
108
|
+
* Mounts a route with the given method, endpoint, and path.
|
|
109
|
+
*/
|
|
110
|
+
export declare function mount(app: ExpressApplication, { method, route, path }: RouteRegistrationInfo): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ExpressFunction } from '../utils/express.utils';
|
|
2
|
+
/**
|
|
3
|
+
* Creates middleware that initializes thread-local storage for each request.
|
|
4
|
+
* Automatically generates a unique request ID and makes it available throughout
|
|
5
|
+
* the request lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* @returns Express middleware function
|
|
8
|
+
*/
|
|
9
|
+
export declare function createTlsMiddleware(): ExpressFunction;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread-local storage data for per-request context.
|
|
3
|
+
* Stores information that should be available throughout the request lifecycle.
|
|
4
|
+
*/
|
|
5
|
+
export interface ThreadLocalData {
|
|
6
|
+
/** Unique request identifier */
|
|
7
|
+
requestId: string;
|
|
8
|
+
/** Additional custom fields can be stored */
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Gets all thread-local data for the current request context.
|
|
13
|
+
* Returns undefined if called outside an async context managed by API.
|
|
14
|
+
* @Internal
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRequestLocalStorage(): ThreadLocalData | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Executes a callback within a request context with the given thread-local data.
|
|
19
|
+
* Used by middleware to set up the context for handlers.
|
|
20
|
+
* @Internal
|
|
21
|
+
* @param data - Thread-local data to establish
|
|
22
|
+
* @param callback - Function to execute within the context
|
|
23
|
+
* @returns Result of the callback
|
|
24
|
+
*/
|
|
25
|
+
export declare function runWithRequestTlsData<T>(data: ThreadLocalData, callback: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ApiResponse } from '../api.types';
|
|
2
|
+
/**
|
|
3
|
+
* Converts JS timestamp or date to ISO 8601 format (without milliseconds).
|
|
4
|
+
* Example: "2012-07-20T01:19:13Z".
|
|
5
|
+
* @Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare function toApiDateString(value: number | Date): string;
|
|
8
|
+
/**
|
|
9
|
+
* Wraps the response into the correct API form.
|
|
10
|
+
* Add necessary fields, like 'requestId'.
|
|
11
|
+
* If the response is already in the correct form, returns it as-is.
|
|
12
|
+
* @Internal
|
|
13
|
+
*/
|
|
14
|
+
export declare function wrapAsApiResponse<T = unknown>(apiResponseOrResultValue: T | ApiResponse<T>): ApiResponse<T>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
/** Shortcut for express.* types with no namespace: so the type can be found/imported by IDE. */
|
|
3
|
+
export type ExpressRequest = express.Request;
|
|
4
|
+
export type ExpressResponse = express.Response;
|
|
5
|
+
export type ExpressNextFunction = express.NextFunction;
|
|
6
|
+
export type ExpressApplication = express.Application;
|
|
7
|
+
export type ExpressFunction = (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
export interface ApiResponse<ResponseEntity = unknown> {
|
|
9
|
+
/** Result of the call. A single entity for non-paginated ${by-id} requests or an array for list queries. */
|
|
10
|
+
result: ResponseEntity;
|
|
11
|
+
/**
|
|
12
|
+
* Unique ID of the request.
|
|
13
|
+
* Automatically added to every API response.
|
|
14
|
+
* May be passed via 'x-request-id' header from client.
|
|
15
|
+
*/
|
|
16
|
+
requestId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Response status code. Same as HTTP response status.
|
|
19
|
+
* Default: 200 for successful responses or 500 for internal server errors.
|
|
20
|
+
*/
|
|
21
|
+
status?: number;
|
|
22
|
+
/** Optional error message. */
|
|
23
|
+
error?: string;
|
|
24
|
+
/** Optional structured error details. */
|
|
25
|
+
details?: Record<string, unknown>;
|
|
26
|
+
/** Offset in the result set. Save as 'offset' query parameter. */
|
|
27
|
+
offset?: number;
|
|
28
|
+
/** Number of results requested. Same as 'limit' query parameter. */
|
|
29
|
+
limit?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Converts an API response value into a standardized ApiResponse structure. */
|
|
32
|
+
export declare function response<T = unknown>(result: T): ApiResponse<T>;
|
|
33
|
+
/** Globally identified URL (path or query) parameter info. */
|
|
34
|
+
export interface UrlParameterInfo {
|
|
35
|
+
/** Optional global validator for this parameter. */
|
|
36
|
+
validator?: ValueAssertion<string>;
|
|
37
|
+
/** Description for documentation. */
|
|
38
|
+
description?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Default documentation and validation for URL parameters.
|
|
42
|
+
* @Internal
|
|
43
|
+
*/
|
|
44
|
+
export declare const URL_PARAMETER_INFO: Record<string, UrlParameterInfo>;
|
|
45
|
+
/** Registers a new URL parameter. */
|
|
46
|
+
export declare function registerUrlParameter(name: string, info: UrlParameterInfo): void;
|
|
47
|
+
/**
|
|
48
|
+
* Asserts that the value is a registered URL parameter name.
|
|
49
|
+
* @Internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function assertUrlParameter(name: unknown): asserts name is string;
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common HTTP status codes as numbers.
|
|
3
|
+
* @Internal
|
|
4
|
+
*/
|
|
5
|
+
export declare const INTERNAL_ERROR_STATUS = 500;
|
|
6
|
+
/**
|
|
7
|
+
* Common HTTP status codes as numbers.
|
|
8
|
+
* @Internal
|
|
9
|
+
*/
|
|
10
|
+
export declare const BAD_REQUEST_STATUS = 400;
|
|
11
|
+
/**
|
|
12
|
+
* Common HTTP status codes as numbers.
|
|
13
|
+
* @Internal
|
|
14
|
+
*/
|
|
15
|
+
export declare const UNAUTHORIZED_STATUS = 401;
|
|
16
|
+
/**
|
|
17
|
+
* Common HTTP status codes as numbers.
|
|
18
|
+
* @Internal
|
|
19
|
+
*/
|
|
20
|
+
export declare const FORBIDDEN_STATUS = 403;
|
|
21
|
+
/**
|
|
22
|
+
* Common HTTP status codes as numbers.
|
|
23
|
+
* @Internal
|
|
24
|
+
*/
|
|
25
|
+
export declare const NOT_FOUND_STATUS = 404;
|
|
26
|
+
/**
|
|
27
|
+
* Common HTTP status codes as numbers.
|
|
28
|
+
* @Internal
|
|
29
|
+
*/
|
|
30
|
+
export declare const OK_STATUS = 200;
|
|
31
|
+
/**
|
|
32
|
+
* Common HTTP status codes as numbers.
|
|
33
|
+
* @Internal
|
|
34
|
+
*/
|
|
35
|
+
export declare const TOO_MANY_REQUESTS_STATUS = 429;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './api.types';
|
|
2
|
+
export * from './auth/auth-strategy';
|
|
3
|
+
export * from './auth/auth.types';
|
|
4
|
+
export * from './auth/auth.utils';
|
|
5
|
+
export * from './auth/bearer-auth-strategy';
|
|
6
|
+
export * from './config';
|
|
7
|
+
export * from './error-handling';
|
|
8
|
+
export * from './http.types';
|
|
9
|
+
export * from './rate-limit/in-memory-rate-limiter';
|
|
10
|
+
export * from './rate-limit/rate-limit.types';
|
|
11
|
+
export * from './route-table';
|
|
12
|
+
export * from './router';
|
|
13
|
+
export * from './thread-local/thread-local-storage';
|
|
14
|
+
export * from './thread-local/thread-local-storage-middleware';
|
|
15
|
+
export * from './utils/express.utils';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ExpressFunction } from '../utils/express.utils';
|
|
2
|
+
import { RateLimitConfig, RateLimitResult } from './rate-limit.types';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory rate limiter using sliding window counter.
|
|
5
|
+
* Tracks request counts per key with time-based window expiration.
|
|
6
|
+
*/
|
|
7
|
+
declare class InMemoryRateLimiter {
|
|
8
|
+
private readonly limits;
|
|
9
|
+
private readonly points;
|
|
10
|
+
private readonly durationMs;
|
|
11
|
+
constructor(points: number, durationSeconds: number);
|
|
12
|
+
/**
|
|
13
|
+
* Try to consume points from the rate limit.
|
|
14
|
+
* Returns result if successful, throws if limit exceeded.
|
|
15
|
+
*
|
|
16
|
+
* @param key - Unique identifier for the client (e.g., IP address)
|
|
17
|
+
* @returns Rate limit result with remaining points and ms until reset
|
|
18
|
+
* @throws RateLimitResult if limit exceeded
|
|
19
|
+
*/
|
|
20
|
+
consume(key: string): RateLimitResult;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a rate limiter middleware using in-memory implementation.
|
|
24
|
+
*
|
|
25
|
+
* Separate limiters are used for read (GET) and write (POST/PATCH/PUT/DELETE) requests.
|
|
26
|
+
*
|
|
27
|
+
* @param config - Rate limit configuration
|
|
28
|
+
* @returns Express middleware function
|
|
29
|
+
*/
|
|
30
|
+
export declare function createRateLimiterMiddleware(config: RateLimitConfig): Promise<ExpressFunction>;
|
|
31
|
+
/**
|
|
32
|
+
* Export the in-memory limiter class for advanced use cases where
|
|
33
|
+
* you might want direct control over rate limit management.
|
|
34
|
+
*/
|
|
35
|
+
export { InMemoryRateLimiter };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ExpressResponse } from '../utils/express.utils';
|
|
2
|
+
import { RateLimitResult } from './rate-limit.types';
|
|
3
|
+
/**
|
|
4
|
+
* Adds rate limit state headers to the response.
|
|
5
|
+
* See https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
|
|
6
|
+
*
|
|
7
|
+
* Headers added:
|
|
8
|
+
* - X-RateLimit-Limit: Server's quota for requests in the time window
|
|
9
|
+
* - X-RateLimit-Remaining: Remaining quota in the current window
|
|
10
|
+
* - X-RateLimit-Reset: Time remaining in the current window (in seconds)
|
|
11
|
+
* - X-RateLimit-Policy: Quota policies associated with the client
|
|
12
|
+
* @Internal
|
|
13
|
+
*/
|
|
14
|
+
export declare function addRateLimitHeaders(res: ExpressResponse, result: RateLimitResult, limitPoints: number, duration: number): ExpressResponse;
|
|
15
|
+
/**
|
|
16
|
+
* Converts milliseconds to seconds, rounding up.
|
|
17
|
+
* @Internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function msToSeconds(ms: number): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for rate limiting.
|
|
3
|
+
*/
|
|
4
|
+
export interface RateLimitConfig {
|
|
5
|
+
/** Rate limit points per time window */
|
|
6
|
+
points: {
|
|
7
|
+
read: number;
|
|
8
|
+
write: number;
|
|
9
|
+
};
|
|
10
|
+
/** Duration of the rate limit window in seconds */
|
|
11
|
+
duration: number;
|
|
12
|
+
/** Key prefix for rate limit data (optional, defaults to 'rate_limit') */
|
|
13
|
+
keyPrefix?: string;
|
|
14
|
+
/** Paths that should bypass rate limiting (default: ['/v1', '/health']) */
|
|
15
|
+
rateLimitWhitelist?: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Simple in-memory rate limiter result.
|
|
19
|
+
*/
|
|
20
|
+
export interface RateLimitResult {
|
|
21
|
+
remainingPoints: number;
|
|
22
|
+
msBeforeNext: number;
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ExpressApplication } from './utils/express.utils';
|
|
2
|
+
import { DeleteEndpoint, GetEndpoint, PatchEndpoint, PostEndpoint, PutEndpoint, RequestContext, ResponseOrValue } from './router';
|
|
3
|
+
/**
|
|
4
|
+
* Helper utility for organizing and mounting routes.
|
|
5
|
+
* Provides a fluent interface for registering multiple handlers.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RouteTable {
|
|
8
|
+
private readonly app;
|
|
9
|
+
constructor(app: ExpressApplication);
|
|
10
|
+
get<T>(path: string, endpoint: GetEndpoint<T> | GetEndpoint<T[]>): this;
|
|
11
|
+
get<T>(path: string, run: (ctx: RequestContext) => Promise<ResponseOrValue<T>>): this;
|
|
12
|
+
post<Body, Result>(path: string, endpoint: PostEndpoint<Body, Result>): this;
|
|
13
|
+
patch<Body, Result>(path: string, endpoint: PatchEndpoint<Body, Result>): this;
|
|
14
|
+
put<Body, Result>(path: string, endpoint: PutEndpoint<Body, Result>): this;
|
|
15
|
+
delete(path: string, endpoint: DeleteEndpoint): this;
|
|
16
|
+
delete(path: string, run: (ctx: RequestContext) => Promise<void>): this;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Factory function to create a new route table.
|
|
20
|
+
* @param app Express application instance
|
|
21
|
+
* @returns RouteTable instance with fluent API
|
|
22
|
+
*/
|
|
23
|
+
export declare function createRouteTable(app: ExpressApplication): RouteTable;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Assertion, ObjectAssertion } from '@fishka/assertions';
|
|
2
|
+
import { ApiResponse, UrlTokensValidator } from './api.types';
|
|
3
|
+
import { AuthUser } from './auth/auth.types';
|
|
4
|
+
import { ExpressApplication, ExpressRequest, ExpressResponse } from './utils/express.utils';
|
|
5
|
+
/** Express API allows handlers to return response in the raw form. */
|
|
6
|
+
export type ResponseOrValue<ResponseEntity> = ApiResponse<ResponseEntity> | ResponseEntity;
|
|
7
|
+
/**
|
|
8
|
+
* Generic middleware hook for endpoint execution.
|
|
9
|
+
* Allows custom logic like transaction management, authorization checks, etc.
|
|
10
|
+
*/
|
|
11
|
+
export type EndpointMiddleware<Context = RequestContext> = (run: () => Promise<unknown>, context: Context) => Promise<unknown>;
|
|
12
|
+
/** Generic request context passed to all handlers. Database-agnostic and extensible. */
|
|
13
|
+
export interface RequestContext<Body = void> {
|
|
14
|
+
/** Parsed and validated request body (for POST/PATCH/PUT handlers). */
|
|
15
|
+
body: Body;
|
|
16
|
+
/** Express Request object. */
|
|
17
|
+
req: ExpressRequest;
|
|
18
|
+
/** Express Response object. */
|
|
19
|
+
res: ExpressResponse;
|
|
20
|
+
/** Authenticated user (if any). Populated by auth middleware. */
|
|
21
|
+
authUser?: AuthUser;
|
|
22
|
+
/**
|
|
23
|
+
* Generic parameter access with lazy validation.
|
|
24
|
+
* Provides type-safe access to URL path and query parameters.
|
|
25
|
+
*/
|
|
26
|
+
params: {
|
|
27
|
+
get(key: string): string;
|
|
28
|
+
tryGet(key: string): string | undefined;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Query parameter access.
|
|
32
|
+
*/
|
|
33
|
+
query: {
|
|
34
|
+
get(key: string): string | undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Generic state storage for middleware to attach data.
|
|
38
|
+
* Allows middleware to pass information to handlers and other middleware.
|
|
39
|
+
*/
|
|
40
|
+
state: Map<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/** Base interface with common endpoint properties. */
|
|
43
|
+
export interface EndpointBase<Context = RequestContext, Result = unknown> {
|
|
44
|
+
/** Path parameter validator. */
|
|
45
|
+
$path?: UrlTokensValidator;
|
|
46
|
+
/** Query parameter validator. */
|
|
47
|
+
$query?: UrlTokensValidator;
|
|
48
|
+
/** Optional middleware to execute before the handler. */
|
|
49
|
+
middlewares?: Array<EndpointMiddleware>;
|
|
50
|
+
/** Handler function. */
|
|
51
|
+
run: (ctx: Context) => Promise<ResponseOrValue<Result>>;
|
|
52
|
+
}
|
|
53
|
+
/** Descriptor for GET list routes. */
|
|
54
|
+
export type GetListEndpoint<ResultElementType = unknown> = EndpointBase<RequestContext, Array<ResultElementType>>;
|
|
55
|
+
/** Descriptor for GET routes. */
|
|
56
|
+
export type GetEndpoint<Result = unknown> = EndpointBase<RequestContext, Result>;
|
|
57
|
+
/** Descriptor for POST routes. */
|
|
58
|
+
export interface PostEndpoint<Body = unknown, Result = unknown> extends EndpointBase<RequestContext<Body>, Result> {
|
|
59
|
+
/** Request body validator. */
|
|
60
|
+
$body: Body extends object ? ObjectAssertion<Body> : Assertion<Body>;
|
|
61
|
+
}
|
|
62
|
+
/** Same as POST. Used for full object updates. */
|
|
63
|
+
export type PutEndpoint<Body = unknown, Result = unknown> = PostEndpoint<Body, Result>;
|
|
64
|
+
/** Same as PUT. While PUT is used for the whole object update, PATCH is used for a partial update. */
|
|
65
|
+
export type PatchEndpoint<Body = unknown, Result = unknown> = PutEndpoint<Body, Result>;
|
|
66
|
+
/** Descriptor for DELETE routes. */
|
|
67
|
+
export type DeleteEndpoint = EndpointBase<RequestContext, void>;
|
|
68
|
+
/** Union type for all route registration info objects. */
|
|
69
|
+
export type RouteRegistrationInfo = ({
|
|
70
|
+
method: 'get';
|
|
71
|
+
route: GetEndpoint | GetListEndpoint;
|
|
72
|
+
} | {
|
|
73
|
+
method: 'post';
|
|
74
|
+
route: PostEndpoint;
|
|
75
|
+
} | {
|
|
76
|
+
method: 'patch';
|
|
77
|
+
route: PatchEndpoint;
|
|
78
|
+
} | {
|
|
79
|
+
method: 'put';
|
|
80
|
+
route: PutEndpoint;
|
|
81
|
+
} | {
|
|
82
|
+
method: 'delete';
|
|
83
|
+
route: DeleteEndpoint;
|
|
84
|
+
}) & {
|
|
85
|
+
path: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Registers a GET route.
|
|
89
|
+
*/
|
|
90
|
+
export declare const mountGet: (app: ExpressApplication, path: string, endpoint: GetEndpoint | GetListEndpoint) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Registers a POST route.
|
|
93
|
+
*/
|
|
94
|
+
export declare const mountPost: <Body, Result>(app: ExpressApplication, path: string, endpoint: PostEndpoint<Body, Result>) => void;
|
|
95
|
+
/**
|
|
96
|
+
* Registers a PATCH route.
|
|
97
|
+
*/
|
|
98
|
+
export declare const mountPatch: <Body, Result>(app: ExpressApplication, path: string, endpoint: PatchEndpoint<Body, Result>) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Registers a PUT route.
|
|
101
|
+
*/
|
|
102
|
+
export declare const mountPut: <Body, Result>(app: ExpressApplication, path: string, endpoint: PutEndpoint<Body, Result>) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Registers a DELETE route.
|
|
105
|
+
*/
|
|
106
|
+
export declare const mountDelete: (app: ExpressApplication, path: string, endpoint: DeleteEndpoint) => void;
|
|
107
|
+
/**
|
|
108
|
+
* Mounts a route with the given method, endpoint, and path.
|
|
109
|
+
*/
|
|
110
|
+
export declare function mount(app: ExpressApplication, { method, route, path }: RouteRegistrationInfo): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ExpressFunction } from '../utils/express.utils';
|
|
2
|
+
/**
|
|
3
|
+
* Creates middleware that initializes thread-local storage for each request.
|
|
4
|
+
* Automatically generates a unique request ID and makes it available throughout
|
|
5
|
+
* the request lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* @returns Express middleware function
|
|
8
|
+
*/
|
|
9
|
+
export declare function createTlsMiddleware(): ExpressFunction;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread-local storage data for per-request context.
|
|
3
|
+
* Stores information that should be available throughout the request lifecycle.
|
|
4
|
+
*/
|
|
5
|
+
export interface ThreadLocalData {
|
|
6
|
+
/** Unique request identifier */
|
|
7
|
+
requestId: string;
|
|
8
|
+
/** Additional custom fields can be stored */
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Gets all thread-local data for the current request context.
|
|
13
|
+
* Returns undefined if called outside an async context managed by API.
|
|
14
|
+
* @Internal
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRequestLocalStorage(): ThreadLocalData | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Executes a callback within a request context with the given thread-local data.
|
|
19
|
+
* Used by middleware to set up the context for handlers.
|
|
20
|
+
* @Internal
|
|
21
|
+
* @param data - Thread-local data to establish
|
|
22
|
+
* @param callback - Function to execute within the context
|
|
23
|
+
* @returns Result of the callback
|
|
24
|
+
*/
|
|
25
|
+
export declare function runWithRequestTlsData<T>(data: ThreadLocalData, callback: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ApiResponse } from '../api.types';
|
|
2
|
+
/**
|
|
3
|
+
* Converts JS timestamp or date to ISO 8601 format (without milliseconds).
|
|
4
|
+
* Example: "2012-07-20T01:19:13Z".
|
|
5
|
+
* @Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare function toApiDateString(value: number | Date): string;
|
|
8
|
+
/**
|
|
9
|
+
* Wraps the response into the correct API form.
|
|
10
|
+
* Add necessary fields, like 'requestId'.
|
|
11
|
+
* If the response is already in the correct form, returns it as-is.
|
|
12
|
+
* @Internal
|
|
13
|
+
*/
|
|
14
|
+
export declare function wrapAsApiResponse<T = unknown>(apiResponseOrResultValue: T | ApiResponse<T>): ApiResponse<T>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
/** Shortcut for express.* types with no namespace: so the type can be found/imported by IDE. */
|
|
3
|
+
export type ExpressRequest = express.Request;
|
|
4
|
+
export type ExpressResponse = express.Response;
|
|
5
|
+
export type ExpressNextFunction = express.NextFunction;
|
|
6
|
+
export type ExpressApplication = express.Application;
|
|
7
|
+
export type ExpressFunction = (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => Promise<void>;
|