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);