diff --git a/src/Request.ts b/src/Request.ts
index eab3bc1..a434cd1 100644
--- a/src/Request.ts
+++ b/src/Request.ts
@@ -2,11 +2,15 @@ import {IPAddress, IPv4, IPv6} from "@cldn/ip";
import {Multipart} from "multipart-ts";
import http, {OutgoingHttpHeader} from "node:http";
import stream from "node:stream";
+import {Authenticator} from "./auth/Authenticator.js";
+import {Authorisation} from "./auth/Authorisation.js";
+import {AuthenticatedRequest} from "./auth/AuthenticatedRequest.js";
+import {Server} from "./Server.js";
/**
* An incoming HTTP request from a connected client.
*/
-export class Request {
+export class Request {
/**
* The request method.
*/
@@ -32,6 +36,11 @@ export class Request {
*/
public readonly ip: IPv4 | IPv6;
+ /**
+ * The {@link Server} from which this request was received.
+ */
+ public readonly server: Server;
+
/**
* The parsed request cookies from the {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cookie|Cookie} request header.
*/
@@ -44,19 +53,22 @@ export class Request {
* @param headers See {@link Request#headers}.
* @param bodyStream See {@link Request#bodyStream}.
* @param ip See {@link Request#ip}.
+ * @param server See {@link Request#server}.
*/
- protected constructor(
- method: Request["method"],
- url: Request["url"],
- headers: Request["headers"],
- bodyStream: Request["bodyStream"],
- ip: Request["ip"],
+ public constructor(
+ method: Request["method"],
+ url: Request["url"],
+ headers: Request["headers"],
+ bodyStream: Request["bodyStream"],
+ ip: Request["ip"],
+ server: Request["server"]
) {
this.method = method;
this.url = url;
this.headers = headers;
this.bodyStream = bodyStream;
this.ip = ip;
+ this.server = server;
this.cookies = new Map(
this.headers.get("cookie")
@@ -80,7 +92,7 @@ export class Request {
* @throws {@link Request.BadUrlError} If the request URL is invalid.
* @throws {@link Request.SocketClosedError} If the request socket was closed before the request could be handled.
*/
- public static incomingMessage(incomingMessage: http.IncomingMessage) {
+ public static incomingMessage(incomingMessage: http.IncomingMessage, server: Server) {
const auth =
incomingMessage.headers.authorization
?.toLowerCase()
@@ -101,7 +113,7 @@ export class Request {
if (remoteAddress === undefined)
throw new Request.SocketClosedError();
- return new Request(incomingMessage.method as Request.Method, new URL(url), headers, incomingMessage, IPAddress.fromString(remoteAddress));
+ return new Request(incomingMessage.method as Request.Method, new URL(url), headers, incomingMessage, IPAddress.fromString(remoteAddress), server);
}
/**
@@ -118,6 +130,34 @@ export class Request {
);
}
+ /**
+ * Attempt to obtain authorisation for this request with one of the {@link Server}’s {@link Authenticator}s.
+ * @returns `null` if the request lacks authorisation information.
+ */
+ public async getAuthorisation(): Promise | null> {
+ const authenticator = this.server._authenticators.find(a => a.canAuthenticate(this));
+ if (authenticator === undefined) return null;
+ return await authenticator.authenticate(this);
+ }
+
+ /**
+ * Attempt to authenticate this request with one of the {@link Server}’s {@link Authenticator}s.
+ * @returns `null` if the request lacks authorisation information.
+ */
+ public async authenticate(): Promise | null> {
+ const authorisation = await this.getAuthorisation();
+ if (authorisation === null) return null;
+ return new AuthenticatedRequest(
+ authorisation,
+ this.method,
+ this.url,
+ this.headers,
+ this.bodyStream,
+ this.ip,
+ this.server,
+ );
+ }
+
/**
* Returns a boolean value that declares whether the body has been read yet.
*/
diff --git a/src/Server.ts b/src/Server.ts
index 6307a1b..159d69b 100644
--- a/src/Server.ts
+++ b/src/Server.ts
@@ -1,6 +1,7 @@
import EventEmitter from "node:events";
import http from "node:http";
import packageJson from "../package.json" with {type: "json"};
+import {Authenticator} from "./auth/Authenticator.js";
import {Request} from "./Request.js";
import {EmptyResponse} from "./response/index.js";
import {Response} from "./response/Response.js";
@@ -12,19 +13,24 @@ import {ServerErrorRegistry} from "./ServerErrorRegistry.js";
* An HTTP server.
* @see {@link Server.Events} for events.
*/
-class Server extends EventEmitter {
+class Server extends EventEmitter {
/**
* Headers sent with every response.
*/
public readonly globalHeaders: Headers;
+
/**
* This server's route registry.
*/
- public readonly routes = new RouteRegistry();
+ public readonly routes = new RouteRegistry();
+
+ /** @internal */
+ public readonly _authenticators: Authenticator[];
+
/**
* This server's error registry.
*/
- public readonly errors = new ServerErrorRegistry();
+ public readonly errors = new ServerErrorRegistry();
private readonly server: http.Server;
private readonly port?: number;
private readonly copyOrigin: boolean;
@@ -34,7 +40,7 @@ class Server extends EventEmitter {
* Create a new HTTP server.
* @param options Server options.
*/
- public constructor(options?: Server.Options) {
+ public constructor(options?: Server.Options) {
super();
this.server = http.createServer({
joinDuplicateHeaders: true,
@@ -42,11 +48,12 @@ class Server extends EventEmitter {
this.globalHeaders = new Headers(options?.globalHeaders);
if (!this.globalHeaders.has("server"))
- this.globalHeaders.set("Server", `cldn/${packageJson.version}`);
+ this.globalHeaders.set("Server", `${packageJson.name}/${packageJson.version}`);
this.port = options?.port;
this.copyOrigin = options?.copyOrigin ?? false;
this.handleConditionalRequests = options?.handleConditionalRequests ?? true;
+ this._authenticators = options?.authenticators ?? [];
if (this.port !== undefined) this.listen(this.port).then();
@@ -101,13 +108,13 @@ class Server extends EventEmitter {
}
private async listener(req: http.IncomingMessage, res: http.ServerResponse) {
- let apiRequest: Request;
+ let apiRequest: Request;
try {
- apiRequest = Request.incomingMessage(req);
+ apiRequest = Request.incomingMessage(req, this);
}
catch (e) {
if (e instanceof Request.BadUrlError) {
- this.errors._get(ServerErrorRegistry.ErrorCodes.BAD_URL, null)._send(res, this);
+ await this.errors._get(ServerErrorRegistry.ErrorCodes.BAD_URL, null)._send(res);
return;
}
@@ -115,7 +122,7 @@ class Server extends EventEmitter {
return;
this.emit("error", e as any);
- this.errors._get(ServerErrorRegistry.ErrorCodes.INTERNAL, null)._send(res, this);
+ await this.errors._get(ServerErrorRegistry.ErrorCodes.INTERNAL, null)._send(res);
return;
}
@@ -127,7 +134,7 @@ class Server extends EventEmitter {
apiRequest._responseHeaders.set("vary", "origin");
}
- let response: Response;
+ let response: Response;
try {
response = await this.routes.handle(apiRequest);
}
@@ -148,13 +155,13 @@ class Server extends EventEmitter {
await this.sendResponse(response, res, apiRequest);
}
- private async sendResponse(response: Response, res: http.ServerResponse, req: Request): Promise {
+ private async sendResponse(response: Response, res: http.ServerResponse, req: Request): Promise {
conditional: if (
this.handleConditionalRequests
&& response.statusCode === 200
&& [Request.Method.GET, Request.Method.HEAD].includes(req.method)
) {
- const responseHeaders = response.allHeaders(res, this, req);
+ const responseHeaders = response.allHeaders(res, req);
const etag = responseHeaders.get("etag");
const lastModified = responseHeaders.has("last-modified")
? new Date(responseHeaders.get("last-modified")!)
@@ -166,26 +173,26 @@ class Server extends EventEmitter {
if (!this.getETags(req.headers.get("if-match")!)
.filter(t => !t.startsWith("W/"))
.includes(etag!))
- return this.errors._get(ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED, req)._send(res, this, req);
+ return this.errors._get(ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED, req)._send(res, req);
}
else if (req.headers.has("if-unmodified-since")) {
if (lastModified === null
|| lastModified.getTime() > new Date(req.headers.get("if-unmodified-since")!).getTime())
- return this.errors._get(ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED, req)._send(res, this, req);
+ return this.errors._get(ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED, req)._send(res, req);
}
if (req.headers.has("if-none-match")) {
if (this.getETags(req.headers.get("if-none-match")!)
.includes(etag!))
- return new EmptyResponse(responseHeaders, 304)._send(res, this, req);
+ return new EmptyResponse(responseHeaders, 304)._send(res, req);
}
else if (req.headers.has("if-modified-since")) {
if (lastModified !== null
&& lastModified.getTime() <= new Date(req.headers.get("if-modified-since")!).getTime())
- return new EmptyResponse(responseHeaders, 304)._send(res, this, req);
+ return new EmptyResponse(responseHeaders, 304)._send(res, req);
}
}
- response._send(res, this, req);
+ await response._send(res, req);
}
private getETags(header: string) {
@@ -199,7 +206,7 @@ namespace Server {
/**
* Server options
*/
- export interface Options {
+ export interface Options {
/**
* The HTTP listener port. From 1 to 65535. Ports 1–1023 require
* privileges. If not set, {@link Server#listen|Server.listen()} must be called manually.
@@ -225,6 +232,11 @@ namespace Server {
* @default true
*/
readonly handleConditionalRequests?: boolean;
+
+ /**
+ * Authenticators for handling request authentication.
+ */
+ readonly authenticators?: Authenticator[];
}
/**
diff --git a/src/ServerErrorRegistry.ts b/src/ServerErrorRegistry.ts
index dc2e542..10243de 100644
--- a/src/ServerErrorRegistry.ts
+++ b/src/ServerErrorRegistry.ts
@@ -5,8 +5,8 @@ import {TextResponse} from "./response/TextResponse.js";
/**
* A registry for server errors.
*/
-class ServerErrorRegistry {
- private readonly responses: Record Response)>;
+class ServerErrorRegistry {
+ private readonly responses: Record | ((req?: Request) => Response)>;
/**
* Create a new server error registry initialised with default responses.
@@ -24,6 +24,9 @@ class ServerErrorRegistry {
[ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED]:
new TextResponse("Precondition failed.", 412),
+
+ [ServerErrorRegistry.ErrorCodes.NO_PERMISSION]:
+ new TextResponse("You do not have permission to perform this action.", 403),
};
}
@@ -32,12 +35,12 @@ class ServerErrorRegistry {
* @param code The server error code.
* @param response The response to send.
*/
- public register(code: ServerErrorRegistry.ErrorCodes, response: Response | ((req?: Request) => Response)) {
+ public register(code: ServerErrorRegistry.ErrorCodes, response: Response | ((req?: Request) => Response)) {
this.responses[code] = response;
}
/** @internal */
- public _get(code: ServerErrorRegistry.ErrorCodes, req: Request | null): Response {
+ public _get(code: ServerErrorRegistry.ErrorCodes, req: Request | null): Response {
const r = this.responses[code];
if (typeof r === "function") return r(req ?? void 0);
return r;
@@ -53,6 +56,7 @@ namespace ServerErrorRegistry {
NO_ROUTE,
INTERNAL,
PRECONDITION_FAILED,
+ NO_PERMISSION,
}
}
diff --git a/src/auth/AuthenticatedRequest.ts b/src/auth/AuthenticatedRequest.ts
new file mode 100644
index 0000000..5d5b952
--- /dev/null
+++ b/src/auth/AuthenticatedRequest.ts
@@ -0,0 +1,69 @@
+import {Request} from "../Request.js";
+import {Response, ThrowableResponse} from "../response/index.js";
+import {ServerErrorRegistry} from "../ServerErrorRegistry.js";
+import {Authorisation} from "./Authorisation.js";
+import {Permissible} from "./Permissible.js";
+import {Permission} from "./Permission.js";
+
+/**
+ * A request with available {@link Authorisation}.
+ */
+export class AuthenticatedRequest extends Request implements Permissible {
+ /**
+ * This request’s authorisation.
+ */
+ public readonly authorisation: Authorisation;
+
+ /**
+ * Create a new authenticated request.
+ * @param authorisation
+ * @param args The arguments to pass to the {@link Request} constructor.
+ */
+ public constructor(
+ authorisation: Authorisation,
+ ...args: ConstructorParameters>
+ ) {
+ super(...args);
+ this.authorisation = authorisation;
+ }
+
+ /**
+ * Check if the request has the specified permission.
+ * @param permission
+ */
+ public has(permission: Permission): boolean {
+ return this.authorisation.has(permission);
+ }
+
+ /**
+ * Require the request to have all the specified permissions.
+ * @param permissions The required permission.
+ * @param [response] Throw this response if the request does not have the permission. Defaults to 403 from
+ * {@link ServerErrorRegistry}.
+ * @throws {@link ThrowableResponse} If the request does not have the permission.
+ */
+ public require(permissions: Iterable, response?: Response): void;
+
+ /**
+ * Require the request to have the specified permission.
+ * @param permission The required permission.
+ * @param [response] Throw this response if the request does not have the permission. Defaults to 403 from
+ * {@link ServerErrorRegistry}.
+ * @throws {@link ThrowableResponse} If the request does not have the permission.
+ */
+ public require(permission: Permission, response?: Response): void;
+ public require(required: Permission | Iterable, response?: Response): void {
+ if (required instanceof Permission) {
+ if (!this.has(required))
+ throw new ThrowableResponse(
+ response ?? this.server.errors._get(ServerErrorRegistry.ErrorCodes.NO_PERMISSION, this)
+ );
+ }
+ else for (const permission of required) {
+ if (!this.has(permission))
+ throw new ThrowableResponse(
+ response ?? this.server.errors._get(ServerErrorRegistry.ErrorCodes.NO_PERMISSION, this)
+ );
+ }
+ }
+}
diff --git a/src/auth/Authenticator.ts b/src/auth/Authenticator.ts
new file mode 100644
index 0000000..35e0a68
--- /dev/null
+++ b/src/auth/Authenticator.ts
@@ -0,0 +1,22 @@
+import {Request} from "../Request.js";
+import {Authorisation} from "./Authorisation.js";
+
+/**
+ * Handles authentication for requests.
+ */
+export interface Authenticator {
+ /**
+ * Check whether this can handle authentication for the given request. The authenticator should return `false` if
+ * the request lacks the information required to begin authentication.
+ * @param request
+ */
+ canAuthenticate(request: Request): boolean;
+
+ /**
+ * Authenticate the given request. If authenticate fails, e.g. due to missing or invalid information, such as
+ * credentials, the authenticator should return `null`, which can be communicated to the client by implementing
+ * applications using a 401 status response.
+ * @param request
+ */
+ authenticate(request: Request): Promise | null>;
+}
diff --git a/src/auth/Authorisation.ts b/src/auth/Authorisation.ts
new file mode 100644
index 0000000..cca0fb3
--- /dev/null
+++ b/src/auth/Authorisation.ts
@@ -0,0 +1,22 @@
+import {Permission} from "./Permission.js";
+import {PermissionGroup} from "./PermissionGroup.js";
+
+/**
+ * A permission group with additional data.
+ */
+export class Authorisation extends PermissionGroup {
+ /**
+ * Additional authentication data.
+ */
+ public readonly data: T;
+
+ /**
+ * Create a new authorisation.
+ * @param permissions The permissions of the authorisation.
+ * @param data Additional authentication data.
+ */
+ public constructor(permissions: Iterable, data: T) {
+ super(permissions);
+ this.data = data;
+ }
+}
diff --git a/src/auth/Permissible.ts b/src/auth/Permissible.ts
new file mode 100644
index 0000000..f10ea48
--- /dev/null
+++ b/src/auth/Permissible.ts
@@ -0,0 +1,12 @@
+import {Permission} from "./Permission.js";
+
+/**
+ * Represents an entity that can be checked for permissions.
+ */
+export interface Permissible {
+ /**
+ * Check whether this entity has the specified permission.
+ * @param permission The permission to check.
+ */
+ has(permission: Permission): boolean;
+}
diff --git a/src/auth/Permission.ts b/src/auth/Permission.ts
new file mode 100644
index 0000000..4c1f60b
--- /dev/null
+++ b/src/auth/Permission.ts
@@ -0,0 +1,31 @@
+import {Permissible} from "./Permissible.js";
+
+/**
+ * Represents a permission with a unique name.
+ */
+export class Permission implements Permissible {
+ private readonly name: string;
+
+ /**
+ * Create a new permission with the specified name.
+ * @param name The name of the permission.
+ */
+ public constructor(name: string) {
+ this.name = name;
+ }
+
+ /**
+ * Get the name of this permission.
+ */
+ public getName(): string {
+ return this.name;
+ }
+
+ /**
+ * Checks if this permission matches another.
+ * @param permission The permission to compare with.
+ */
+ public has(permission: Permission): boolean {
+ return this.name === permission.name;
+ }
+}
diff --git a/src/auth/PermissionGroup.ts b/src/auth/PermissionGroup.ts
new file mode 100644
index 0000000..34eaa59
--- /dev/null
+++ b/src/auth/PermissionGroup.ts
@@ -0,0 +1,44 @@
+import {Permissible} from "./Permissible.js";
+import {Permission} from "./Permission.js";
+
+/**
+ * A collection of permissions.
+ */
+export class PermissionGroup implements Permissible, Iterable {
+ protected readonly permissions = new Set();
+
+ /**
+ * Create a new permission group.
+ * @param permissions The permissions in this group.
+ */
+ public constructor(permissions: Iterable) {
+ for (const permission of permissions)
+ if (!this.has(permission))
+ this.permissions.add(permission);
+ }
+
+ /**
+ * Check if the group has a specific permission.
+ * @param permission The permission to check.
+ */
+ public has(permission: Permission): boolean {
+ for (const existingPermission of this.permissions)
+ if (existingPermission.has(permission))
+ return true;
+ return false;
+ }
+
+ /**
+ * An iterator over the permissions in this group.
+ */
+ public *[Symbol.iterator]() {
+ yield* this.permissions;
+ }
+
+ /**
+ * All permissions in this group.
+ */
+ public getAll(): ReadonlyArray {
+ return Array.from(this);
+ }
+}
diff --git a/src/auth/index.ts b/src/auth/index.ts
new file mode 100644
index 0000000..c18c030
--- /dev/null
+++ b/src/auth/index.ts
@@ -0,0 +1,7 @@
+/* Auto-generated by generateIndices.sh */
+export * from "./AuthenticatedRequest.js";
+export * from "./Authenticator.js";
+export * from "./Authorisation.js";
+export * from "./Permissible.js";
+export * from "./Permission.js";
+export * from "./PermissionGroup.js";
diff --git a/src/index.ts b/src/index.ts
index fe40c35..d085108 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,5 +3,6 @@ export * from "./Cookie.js";
export * from "./Request.js";
export * from "./Server.js";
export * from "./ServerErrorRegistry.js";
+export * from "./auth/index.js";
export * from "./response/index.js";
export * from "./routing/index.js";
diff --git a/src/response/BufferResponse.ts b/src/response/BufferResponse.ts
index fca0a72..9ea2f98 100644
--- a/src/response/BufferResponse.ts
+++ b/src/response/BufferResponse.ts
@@ -1,18 +1,17 @@
import http from "node:http";
import {Request} from "../Request.js";
-import {Server} from "../Server.js";
import {Response} from "./Response.js";
/**
* A response that contains buffered data.
*/
-export abstract class BufferResponse extends Response {
+export abstract class BufferResponse extends Response {
/**
* Fetch the buffer to send in the response body.
*/
protected abstract readBuffer(): Uint8Array | Promise;
- protected override async send(res: http.ServerResponse, server: Server, req?: Request): Promise {
+ protected override async send(res: http.ServerResponse, req?: Request): Promise {
const buffer = await this.readBuffer();
if (req !== undefined) {
if (res.chunkedEncoding)
@@ -20,7 +19,7 @@ export abstract class BufferResponse extends Response {
else
req._responseHeaders.set("content-length", buffer.byteLength.toString());
}
- this.writeHead(res, server, req);
+ this.writeHead(res, req);
res.end(buffer);
}
}
diff --git a/src/response/EmptyResponse.ts b/src/response/EmptyResponse.ts
index 536a2f6..aca8b0e 100644
--- a/src/response/EmptyResponse.ts
+++ b/src/response/EmptyResponse.ts
@@ -1,12 +1,11 @@
import http from "node:http";
import {Request} from "../Request.js";
-import {Server} from "../Server.js";
import {Response} from "./Response.js";
/**
* A server response without body (204).
*/
-export class EmptyResponse extends Response {
+export class EmptyResponse extends Response {
/**
* Construct a new EmptyResponse.
* @param [headers] The HTTP response headers to send.
@@ -16,15 +15,10 @@ export class EmptyResponse extends Response {
super(status, headers);
}
- /*protected override send(res: http.ServerResponse): void {
- this.writeHead(res);
- res.end();
- }*/
-
- protected override send(res: http.ServerResponse, server: Server, req?: Request): void {
+ protected override async send(res: http.ServerResponse, req?: Request): Promise {
if (req !== undefined)
req._responseHeaders.set("content-length", "0");
- this.writeHead(res, server, req);
+ this.writeHead(res, req);
res.end();
}
}
diff --git a/src/response/JsonResponse.ts b/src/response/JsonResponse.ts
index 1d41f48..f3e2d33 100644
--- a/src/response/JsonResponse.ts
+++ b/src/response/JsonResponse.ts
@@ -1,6 +1,6 @@
import {TextResponse} from "./TextResponse.js";
-export class JsonResponse extends TextResponse {
+export class JsonResponse extends TextResponse {
/**
* Construct a new JsonResponse.
* @param json The JSON data to send in the response body.
diff --git a/src/response/Response.ts b/src/response/Response.ts
index 034f6f3..8662d29 100644
--- a/src/response/Response.ts
+++ b/src/response/Response.ts
@@ -1,12 +1,11 @@
import http from "node:http";
import {Cookie} from "../Cookie.js";
import {Request} from "../Request.js";
-import {Server} from "../Server.js";
/**
* An outgoing HTTP response.
*/
-export abstract class Response {
+export abstract class Response {
/**
* The HTTP response status code to send.
*/
@@ -22,7 +21,7 @@ export abstract class Response {
* @param statusCode The HTTP response status code to send.
* @param [headers] The HTTP response headers to send.
*/
- protected constructor(statusCode: Response["statusCode"], headers: HeadersInit = {}) {
+ protected constructor(statusCode: Response["statusCode"], headers: HeadersInit = {}) {
this.statusCode = statusCode;
this.headers = new Headers(headers);
}
@@ -38,7 +37,7 @@ export abstract class Response {
/**
* @internal
*/
- public _send(...args: Parameters): ReturnType {
+ public _send(...args: Parameters["send"]>): ReturnType["send"]> {
return this.send(...args);
}
@@ -46,7 +45,7 @@ export abstract class Response {
* All (final) headers to send to the client.
* @internal
*/
- public allHeaders(res: http.ServerResponse, server: Server, req?: Request) {
+ public allHeaders(res: http.ServerResponse, req?: Request) {
const headers = new Headers(this.headers);
if (req !== undefined)
for (const [key, value] of req._responseHeaders)
@@ -61,18 +60,19 @@ export abstract class Response {
headers.set("connection", "close");
else {
headers.set("connection", "keep-alive");
- headers.set("keep-alive", "timeout=" + server._keepAliveTimeout);
+ headers.set("keep-alive", "timeout=" + req.server._keepAliveTimeout);
}
+
return headers;
}
/**
* Set the HTTP response status code and headers.
*/
- protected writeHead(res: http.ServerResponse, server: Server, req?: Request) {
- const headers = this.allHeaders(res, server, req);
+ protected writeHead(res: http.ServerResponse, req?: Request) {
+ const headers = this.allHeaders(res, req);
for (const [key, value] of Array.from(headers.entries())
- .sort((a, b) => a[0].localeCompare(b[0])))
+ .sort(([a], [b]) => a.localeCompare(b)))
res.setHeader(key, value);
res.writeHead(this.statusCode);
}
@@ -80,5 +80,5 @@ export abstract class Response {
/**
* Called once by the server to send the response.
*/
- protected abstract send(res: http.ServerResponse, server: Server, req?: Request): void | Promise;
+ protected abstract send(res: http.ServerResponse, req?: Request): Promise;
}
diff --git a/src/response/TextResponse.ts b/src/response/TextResponse.ts
index 54d1d15..f41781f 100644
--- a/src/response/TextResponse.ts
+++ b/src/response/TextResponse.ts
@@ -3,7 +3,7 @@ import {BufferResponse} from "./BufferResponse.js";
/**
* An HTTP response with a plain text body.
*/
-export class TextResponse extends BufferResponse {
+export class TextResponse extends BufferResponse {
/**
* The plain text body of the response.
*/
diff --git a/src/response/ThrowableResponse.ts b/src/response/ThrowableResponse.ts
index 776f196..7aefb6f 100644
--- a/src/response/ThrowableResponse.ts
+++ b/src/response/ThrowableResponse.ts
@@ -3,7 +3,7 @@ import {Response} from "./Response.js";
/**
* An (error) response that is thrown. Will be caught by the server and sent to the client.
*/
-export class ThrowableResponse extends Error {
+export class ThrowableResponse> extends Error {
public override name = ThrowableResponse.name;
/**
diff --git a/src/routing/EndpointRoute.ts b/src/routing/EndpointRoute.ts
index f2a6141..7f1e827 100644
--- a/src/routing/EndpointRoute.ts
+++ b/src/routing/EndpointRoute.ts
@@ -5,7 +5,7 @@ import {Route} from "./Route.js";
/**
* Routes requests based on HTTP method and path.
*/
-export abstract class EndpointRoute implements Route {
+export abstract class EndpointRoute implements Route {
private readonly method: Request.Method;
private readonly path: string;
@@ -18,7 +18,7 @@ export abstract class EndpointRoute implements Route {
this.path = path;
}
- public match(req: Request): boolean {
+ public match(req: Request): boolean {
return this.path === req.url.pathname
&& (
this.method === req.method
@@ -26,5 +26,5 @@ export abstract class EndpointRoute implements Route {
);
}
- public abstract handle(req: Request): Response | Promise;
+ public abstract handle(req: Request): Promise>;
}
diff --git a/src/routing/Route.ts b/src/routing/Route.ts
index 8ef05d9..dbe3f5f 100644
--- a/src/routing/Route.ts
+++ b/src/routing/Route.ts
@@ -4,17 +4,17 @@ import {Response} from "../response/Response.js";
/**
* A route that can handle HTTP requests.
*/
-export interface Route {
+export interface Route {
/**
* Handles an incoming HTTP request and returns a response.
* @param req The request to handle.
* @return The response generated by handling the request.
*/
- handle(req: Request): Response | Promise;
+ handle(req: Request): Promise>;
/**
* Determines whether this route should/can handle the given request.
* @param req The request to check.
*/
- match(req: Request): boolean;
+ match(req: Request): boolean;
}
diff --git a/src/routing/RouteRegistry.ts b/src/routing/RouteRegistry.ts
index 9e437ff..d1619a2 100644
--- a/src/routing/RouteRegistry.ts
+++ b/src/routing/RouteRegistry.ts
@@ -5,15 +5,15 @@ import {Route} from "./Route.js";
/**
* A registry that manages multiple routes and delegates request handling.
*/
-class RouteRegistry implements Route {
- private readonly routes = new Set();
+class RouteRegistry implements Route {
+ private readonly routes = new Set>();
/**
* Registers one or more routes into the registry.
*
* @param routes The routes to register.
*/
- public register(...routes: Route[]) {
+ public register(...routes: Route[]) {
for (const route of routes)
this.routes.add(route);
}
@@ -22,7 +22,7 @@ class RouteRegistry implements Route {
* Check if any registered route matches the given request.
* @param req The request to check.
*/
- public match(req: Request): boolean {
+ public match(req: Request): boolean {
for (const route of this.routes)
if (route.match(req))
return true;
@@ -35,7 +35,7 @@ class RouteRegistry implements Route {
* @return The response generated by handling the request.
* @throws {@link RouteRegistry.NoRouteError} If no route matches the request.
*/
- public handle(req: Request): Response | Promise {
+ public handle(req: Request): Promise> {
for (const route of this.routes)
if (route.match(req))
return route.handle(req);