@sjoblomtobias/relay 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.d.ts +20 -4
- package/dist/server.js +51 -13
- package/package.json +1 -1
- package/src/server.ts +64 -15
package/dist/server.d.ts
CHANGED
|
@@ -2,19 +2,29 @@ import express from "express";
|
|
|
2
2
|
import type { Route } from "./route.js";
|
|
3
3
|
import type { RequestHandler } from "express";
|
|
4
4
|
export type RouteConstructor = new (...args: any[]) => Route;
|
|
5
|
+
export type ServerOptions = {
|
|
6
|
+
/** Include error message and stack trace in 500 responses. Never enable in production. Defaults to false. */
|
|
7
|
+
exposeErrors?: boolean;
|
|
8
|
+
/** Maximum time in milliseconds a client has to send a complete request. Defaults to 30000. */
|
|
9
|
+
requestTimeoutMs?: number;
|
|
10
|
+
};
|
|
5
11
|
export declare class Server {
|
|
6
12
|
private port;
|
|
7
13
|
private app;
|
|
14
|
+
private stack;
|
|
8
15
|
private authMiddleware?;
|
|
16
|
+
private routesRegistered;
|
|
17
|
+
private readonly options;
|
|
9
18
|
/**
|
|
10
19
|
* Creates an instance of the Server class.
|
|
11
20
|
* @param port The port number on which the server will listen.
|
|
12
21
|
* @param authMiddleware Optional authentication middleware to be used for private routes.
|
|
22
|
+
* @param options Optional server settings.
|
|
13
23
|
*/
|
|
14
|
-
constructor(port: number, authMiddleware?: RequestHandler);
|
|
24
|
+
constructor(port: number, authMiddleware?: RequestHandler, options?: ServerOptions);
|
|
15
25
|
/**
|
|
16
|
-
* Default error handler.
|
|
17
|
-
*
|
|
26
|
+
* Default error handler. Client errors (4xx) keep their status; everything else is a
|
|
27
|
+
* generic 500 and is logged. Error details are only sent when `exposeErrors` is enabled.
|
|
18
28
|
*/
|
|
19
29
|
private readonly errorHandler;
|
|
20
30
|
/**
|
|
@@ -23,7 +33,8 @@ export declare class Server {
|
|
|
23
33
|
*/
|
|
24
34
|
registerRoutes(...routeClasses: Array<RouteConstructor>): void;
|
|
25
35
|
/**
|
|
26
|
-
* Adds middleware functions to the Express application.
|
|
36
|
+
* Adds middleware functions to the Express application. Use this instead of
|
|
37
|
+
* `getApp().use()` so errors from the middleware reach the error handler.
|
|
27
38
|
* @param handlers - Array of Express request handlers (middleware functions).
|
|
28
39
|
*/
|
|
29
40
|
use(...handlers: Array<express.RequestHandler>): Promise<void>;
|
|
@@ -36,7 +47,12 @@ export declare class Server {
|
|
|
36
47
|
* Starts the Express server.
|
|
37
48
|
* @param before Optional callback invoked before the server starts.
|
|
38
49
|
* @param after Optional callback invoked after the server has started.
|
|
50
|
+
* @throws If the server cannot listen, e.g. the port is already in use.
|
|
39
51
|
*/
|
|
40
52
|
start(before?: () => void | Promise<void>, after?: () => void | Promise<void>): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Sets the authentication middleware for private routes. Must be called before
|
|
55
|
+
* registerRoutes(), since routes capture the middleware when they are registered.
|
|
56
|
+
*/
|
|
41
57
|
setAuthMiddleware(middleware: RequestHandler): void;
|
|
42
58
|
}
|
package/dist/server.js
CHANGED
|
@@ -7,29 +7,58 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { STATUS_CODES } from "node:http";
|
|
10
11
|
import express from "express";
|
|
12
|
+
const securityHeaders = (req, res, next) => {
|
|
13
|
+
res.set({
|
|
14
|
+
"X-Content-Type-Options": "nosniff",
|
|
15
|
+
"X-Frame-Options": "DENY",
|
|
16
|
+
"Referrer-Policy": "no-referrer"
|
|
17
|
+
});
|
|
18
|
+
next();
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Returns the 4xx status carried by an error (e.g. from body parsing), or 500 for anything else.
|
|
22
|
+
*/
|
|
23
|
+
function errorStatus(err) {
|
|
24
|
+
const { status, statusCode } = (err !== null && err !== void 0 ? err : {});
|
|
25
|
+
const candidate = status !== null && status !== void 0 ? status : statusCode;
|
|
26
|
+
return typeof candidate === "number" && candidate >= 400 && candidate < 500 ? candidate : 500;
|
|
27
|
+
}
|
|
11
28
|
export class Server {
|
|
12
29
|
/**
|
|
13
30
|
* Creates an instance of the Server class.
|
|
14
31
|
* @param port The port number on which the server will listen.
|
|
15
32
|
* @param authMiddleware Optional authentication middleware to be used for private routes.
|
|
33
|
+
* @param options Optional server settings.
|
|
16
34
|
*/
|
|
17
|
-
constructor(port, authMiddleware) {
|
|
35
|
+
constructor(port, authMiddleware, options = {}) {
|
|
36
|
+
this.routesRegistered = false;
|
|
18
37
|
/**
|
|
19
|
-
* Default error handler.
|
|
20
|
-
*
|
|
38
|
+
* Default error handler. Client errors (4xx) keep their status; everything else is a
|
|
39
|
+
* generic 500 and is logged. Error details are only sent when `exposeErrors` is enabled.
|
|
21
40
|
*/
|
|
22
41
|
this.errorHandler = (err, req, res, next) => {
|
|
23
42
|
if (res.headersSent) {
|
|
24
43
|
next(err);
|
|
25
44
|
return;
|
|
26
45
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
46
|
+
const status = errorStatus(err);
|
|
47
|
+
if (status === 500) {
|
|
48
|
+
console.error(err);
|
|
49
|
+
}
|
|
50
|
+
res.status(status).json(Object.assign({ error: STATUS_CODES[status] }, (status === 500 && this.options.exposeErrors && { message: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined })));
|
|
29
51
|
};
|
|
30
52
|
this.port = port;
|
|
53
|
+
this.options = Object.assign({ exposeErrors: false, requestTimeoutMs: 30000 }, options);
|
|
31
54
|
this.app = express();
|
|
32
55
|
this.app.disable("x-powered-by");
|
|
56
|
+
this.app.use(securityHeaders);
|
|
57
|
+
// Routes and middleware live in an inner router so the error handler, mounted
|
|
58
|
+
// after it, always runs last and catches errors from everything registered via this class.
|
|
59
|
+
this.stack = express.Router();
|
|
60
|
+
this.app.use(this.stack);
|
|
61
|
+
this.app.use(this.errorHandler);
|
|
33
62
|
this.authMiddleware = authMiddleware;
|
|
34
63
|
}
|
|
35
64
|
/**
|
|
@@ -37,18 +66,20 @@ export class Server {
|
|
|
37
66
|
* @param routeClasses - Route classes decorated with @Public or @Private.
|
|
38
67
|
*/
|
|
39
68
|
registerRoutes(...routeClasses) {
|
|
69
|
+
this.routesRegistered = true;
|
|
40
70
|
routeClasses.forEach((RouteClass) => {
|
|
41
71
|
const routeInstance = new RouteClass(this.authMiddleware);
|
|
42
|
-
this.
|
|
72
|
+
this.stack.use(routeInstance.router);
|
|
43
73
|
});
|
|
44
74
|
}
|
|
45
75
|
/**
|
|
46
|
-
* Adds middleware functions to the Express application.
|
|
76
|
+
* Adds middleware functions to the Express application. Use this instead of
|
|
77
|
+
* `getApp().use()` so errors from the middleware reach the error handler.
|
|
47
78
|
* @param handlers - Array of Express request handlers (middleware functions).
|
|
48
79
|
*/
|
|
49
80
|
use(...handlers) {
|
|
50
81
|
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
-
this.
|
|
82
|
+
this.stack.use(handlers);
|
|
52
83
|
});
|
|
53
84
|
}
|
|
54
85
|
/**
|
|
@@ -62,24 +93,31 @@ export class Server {
|
|
|
62
93
|
* Starts the Express server.
|
|
63
94
|
* @param before Optional callback invoked before the server starts.
|
|
64
95
|
* @param after Optional callback invoked after the server has started.
|
|
96
|
+
* @throws If the server cannot listen, e.g. the port is already in use.
|
|
65
97
|
*/
|
|
66
98
|
start(before, after) {
|
|
67
99
|
return __awaiter(this, void 0, void 0, function* () {
|
|
68
100
|
if (before) {
|
|
69
101
|
yield before();
|
|
70
102
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.app.listen(this.port, () =>
|
|
74
|
-
|
|
75
|
-
});
|
|
103
|
+
yield new Promise((resolve, reject) => {
|
|
104
|
+
// Express 5 passes listen errors (e.g. EADDRINUSE) to the callback.
|
|
105
|
+
const httpServer = this.app.listen(this.port, (err) => (err ? reject(err) : resolve()));
|
|
106
|
+
httpServer.requestTimeout = this.options.requestTimeoutMs;
|
|
76
107
|
});
|
|
77
108
|
if (after) {
|
|
78
109
|
yield after();
|
|
79
110
|
}
|
|
80
111
|
});
|
|
81
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Sets the authentication middleware for private routes. Must be called before
|
|
115
|
+
* registerRoutes(), since routes capture the middleware when they are registered.
|
|
116
|
+
*/
|
|
82
117
|
setAuthMiddleware(middleware) {
|
|
118
|
+
if (this.routesRegistered) {
|
|
119
|
+
throw new Error("setAuthMiddleware() must be called before registerRoutes(); already registered routes would not use it.");
|
|
120
|
+
}
|
|
83
121
|
this.authMiddleware = middleware;
|
|
84
122
|
}
|
|
85
123
|
}
|
package/package.json
CHANGED
package/src/server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STATUS_CODES } from "node:http";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import type { Route } from "./route.js";
|
|
3
4
|
import type { ErrorRequestHandler, RequestHandler } from "express";
|
|
@@ -5,36 +6,75 @@ import type { ErrorRequestHandler, RequestHandler } from "express";
|
|
|
5
6
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
7
|
export type RouteConstructor = new (...args: any[]) => Route;
|
|
7
8
|
|
|
9
|
+
export type ServerOptions = {
|
|
10
|
+
/** Include error message and stack trace in 500 responses. Never enable in production. Defaults to false. */
|
|
11
|
+
exposeErrors?: boolean;
|
|
12
|
+
/** Maximum time in milliseconds a client has to send a complete request. Defaults to 30000. */
|
|
13
|
+
requestTimeoutMs?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const securityHeaders: RequestHandler = (req, res, next) => {
|
|
17
|
+
res.set({
|
|
18
|
+
"X-Content-Type-Options": "nosniff",
|
|
19
|
+
"X-Frame-Options": "DENY",
|
|
20
|
+
"Referrer-Policy": "no-referrer"
|
|
21
|
+
});
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the 4xx status carried by an error (e.g. from body parsing), or 500 for anything else.
|
|
27
|
+
*/
|
|
28
|
+
function errorStatus(err: unknown): number {
|
|
29
|
+
const { status, statusCode } = (err ?? {}) as { status?: unknown; statusCode?: unknown };
|
|
30
|
+
const candidate = status ?? statusCode;
|
|
31
|
+
return typeof candidate === "number" && candidate >= 400 && candidate < 500 ? candidate : 500;
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
export class Server {
|
|
9
35
|
private port: number;
|
|
10
36
|
private app: express.Express;
|
|
37
|
+
private stack: express.Router;
|
|
11
38
|
private authMiddleware?: RequestHandler;
|
|
39
|
+
private routesRegistered = false;
|
|
40
|
+
private readonly options: Required<ServerOptions>;
|
|
12
41
|
|
|
13
42
|
/**
|
|
14
43
|
* Creates an instance of the Server class.
|
|
15
44
|
* @param port The port number on which the server will listen.
|
|
16
45
|
* @param authMiddleware Optional authentication middleware to be used for private routes.
|
|
46
|
+
* @param options Optional server settings.
|
|
17
47
|
*/
|
|
18
|
-
constructor(port: number, authMiddleware?: RequestHandler) {
|
|
48
|
+
constructor(port: number, authMiddleware?: RequestHandler, options: ServerOptions = {}) {
|
|
19
49
|
this.port = port;
|
|
50
|
+
this.options = { exposeErrors: false, requestTimeoutMs: 30_000, ...options };
|
|
20
51
|
this.app = express();
|
|
21
52
|
this.app.disable("x-powered-by");
|
|
53
|
+
this.app.use(securityHeaders);
|
|
54
|
+
// Routes and middleware live in an inner router so the error handler, mounted
|
|
55
|
+
// after it, always runs last and catches errors from everything registered via this class.
|
|
56
|
+
this.stack = express.Router();
|
|
57
|
+
this.app.use(this.stack);
|
|
58
|
+
this.app.use(this.errorHandler);
|
|
22
59
|
this.authMiddleware = authMiddleware;
|
|
23
60
|
}
|
|
24
61
|
|
|
25
62
|
/**
|
|
26
|
-
* Default error handler.
|
|
27
|
-
*
|
|
63
|
+
* Default error handler. Client errors (4xx) keep their status; everything else is a
|
|
64
|
+
* generic 500 and is logged. Error details are only sent when `exposeErrors` is enabled.
|
|
28
65
|
*/
|
|
29
66
|
private readonly errorHandler: ErrorRequestHandler = (err, req, res, next) => {
|
|
30
67
|
if (res.headersSent) {
|
|
31
68
|
next(err);
|
|
32
69
|
return;
|
|
33
70
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
error
|
|
37
|
-
|
|
71
|
+
const status = errorStatus(err);
|
|
72
|
+
if (status === 500) {
|
|
73
|
+
console.error(err);
|
|
74
|
+
}
|
|
75
|
+
res.status(status).json({
|
|
76
|
+
error: STATUS_CODES[status],
|
|
77
|
+
...(status === 500 && this.options.exposeErrors && { message: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined })
|
|
38
78
|
});
|
|
39
79
|
};
|
|
40
80
|
|
|
@@ -43,18 +83,20 @@ export class Server {
|
|
|
43
83
|
* @param routeClasses - Route classes decorated with @Public or @Private.
|
|
44
84
|
*/
|
|
45
85
|
public registerRoutes(...routeClasses: Array<RouteConstructor>): void {
|
|
86
|
+
this.routesRegistered = true;
|
|
46
87
|
routeClasses.forEach((RouteClass) => {
|
|
47
88
|
const routeInstance = new RouteClass(this.authMiddleware);
|
|
48
|
-
this.
|
|
89
|
+
this.stack.use(routeInstance.router);
|
|
49
90
|
});
|
|
50
91
|
}
|
|
51
92
|
|
|
52
93
|
/**
|
|
53
|
-
* Adds middleware functions to the Express application.
|
|
94
|
+
* Adds middleware functions to the Express application. Use this instead of
|
|
95
|
+
* `getApp().use()` so errors from the middleware reach the error handler.
|
|
54
96
|
* @param handlers - Array of Express request handlers (middleware functions).
|
|
55
97
|
*/
|
|
56
98
|
public async use(...handlers: Array<express.RequestHandler>): Promise<void> {
|
|
57
|
-
this.
|
|
99
|
+
this.stack.use(handlers);
|
|
58
100
|
}
|
|
59
101
|
|
|
60
102
|
/**
|
|
@@ -69,23 +111,30 @@ export class Server {
|
|
|
69
111
|
* Starts the Express server.
|
|
70
112
|
* @param before Optional callback invoked before the server starts.
|
|
71
113
|
* @param after Optional callback invoked after the server has started.
|
|
114
|
+
* @throws If the server cannot listen, e.g. the port is already in use.
|
|
72
115
|
*/
|
|
73
116
|
public async start(before?: () => void | Promise<void>, after?: () => void | Promise<void>): Promise<void> {
|
|
74
117
|
if (before) {
|
|
75
118
|
await before();
|
|
76
119
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.app.listen(this.port, () =>
|
|
80
|
-
|
|
81
|
-
});
|
|
120
|
+
await new Promise<void>((resolve, reject) => {
|
|
121
|
+
// Express 5 passes listen errors (e.g. EADDRINUSE) to the callback.
|
|
122
|
+
const httpServer = this.app.listen(this.port, (err?: Error) => (err ? reject(err) : resolve()));
|
|
123
|
+
httpServer.requestTimeout = this.options.requestTimeoutMs;
|
|
82
124
|
});
|
|
83
125
|
if (after) {
|
|
84
126
|
await after();
|
|
85
127
|
}
|
|
86
128
|
}
|
|
87
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Sets the authentication middleware for private routes. Must be called before
|
|
132
|
+
* registerRoutes(), since routes capture the middleware when they are registered.
|
|
133
|
+
*/
|
|
88
134
|
public setAuthMiddleware(middleware: RequestHandler) {
|
|
135
|
+
if (this.routesRegistered) {
|
|
136
|
+
throw new Error("setAuthMiddleware() must be called before registerRoutes(); already registered routes would not use it.");
|
|
137
|
+
}
|
|
89
138
|
this.authMiddleware = middleware;
|
|
90
139
|
}
|
|
91
140
|
}
|