Attempt to vendor lambert-server

This commit is contained in:
Rory& 2025-10-03 18:55:48 +02:00
parent efba3f245d
commit 2e365718ed
8 changed files with 354 additions and 3 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -94,7 +94,6 @@
"image-size": "^2.0.2",
"json-bigint": "^1.0.0",
"jsonwebtoken": "^9.0.2",
"lambert-server": "^1.2.12",
"missing-native-js-functions": "^2.0.0",
"module-alias": "^2.2.3",
"morgan": "^1.10.1",
@ -119,7 +118,8 @@
"@spacebar/cdn": "dist/cdn",
"@spacebar/gateway": "dist/gateway",
"@spacebar/util": "dist/util",
"@spacebar/webrtc": "dist/webrtc"
"@spacebar/webrtc": "dist/webrtc",
"lambert-server": "dist/util/util/lambert-server"
},
"optionalDependencies": {
"@sendgrid/mail": "^8.1.6",

View File

@ -0,0 +1,5 @@
export class HTTPError extends Error {
constructor(message: string, public code: number = 400) {
super(message);
}
}

View File

@ -0,0 +1,174 @@
import express, { Application, NextFunction, Request, Response, Router } from "express";
import { traverseDirectory } from "./Utils";
import { Server as HTTPServer } from "http";
import { HTTPError } from "./HTTPError";
import "express-async-errors";
import "missing-native-js-functions";
import bodyParser from "body-parser";
// import helmet from "helmet";
import http from "http";
import chalk from "chalk";
declare global {
namespace Express {
interface Request {
server: Server;
}
}
}
export type ServerOptions = {
port: number;
host: string;
production: boolean;
serverInitLogging: boolean;
errorHandler?: { (err: Error, req: Request, res: Response, next: NextFunction): any };
jsonBody: boolean;
server: http.Server;
app: Application;
};
// Overwrite default options for Router with default value true for mergeParams
const oldRouter = express.Router;
express.Router = function (options?: express.RouterOptions | undefined): Router {
if (!options) options = {};
if (options.mergeParams == null) options.mergeParams = true;
return oldRouter(options);
};
export class Server {
public app: Application;
public http: HTTPServer;
public options: ServerOptions;
public routes: Router[];
constructor(opts?: Partial<ServerOptions>) {
if (!opts) opts = {};
if (!opts.port) opts.port = 8080;
if (!opts.host) opts.host = "0.0.0.0";
if (opts.production == null) opts.production = false;
if (opts.serverInitLogging == null) opts.serverInitLogging = true;
if (opts.errorHandler == null) opts.errorHandler = this.errorHandler;
if (opts.jsonBody == null) opts.jsonBody = true;
if (opts.server) this.http = opts.server;
this.options = <ServerOptions>opts;
if (opts.app) this.app = opts.app;
else this.app = express();
}
// protected secureExpress() {
// this.app.use(helmet.contentSecurityPolicy());
// this.app.use(helmet.expectCt);
// this.app.use(helmet.originAgentCluster());
// this.app.use(helmet.referrerPolicy({ policy: "same-origin" }));
// this.app.use(helmet.hidePoweredBy());
// this.app.use(helmet.noSniff());
// this.app.use(helmet.dnsPrefetchControl({ allow: true }));
// this.app.use(helmet.ieNoOpen());
// this.app.use(helmet.frameguard({ action: "deny" }));
// this.app.use(helmet.permittedCrossDomainPolicies({ permittedPolicies: "none" }));
// }
public errorHandler = (error: Error, req: Request, res: Response, next: NextFunction) => {
try {
let code;
let message = error?.toString();
if (error instanceof HTTPError && error.code) code = error.code || 400;
else {
console.error(error);
if (this.options.production) {
message = "Internal Server Error";
}
code = 500;
}
res.status(code).json({ success: false, code: code, error: true, message });
} catch (e) {
console.error(e);
return res.status(500).json({ success: false, code: 500, error: true, message: "Internal Server Error" });
}
};
async start() {
const server = this.http || this.app;
if (!server.listening) {
await new Promise<void>((res) => {
this.http = server.listen(this.options.port, () => res());
});
if(this.options.serverInitLogging) this.log("info", `[Server] started on ${this.options.host}:${this.options.port}`);
}
}
async registerRoutes(root: string) {
this.app.use((req, res, next) => {
req.server = this;
next();
});
if (this.options.jsonBody) this.app.use(bodyParser.json());
const result = await traverseDirectory({ dirname: root, recursive: true }, this.registerRoute.bind(this, root));
if (this.options.errorHandler) this.app.use(this.options.errorHandler);
// if (this.options.production) this.secureExpress();
return result;
}
log(l: "info" | "error" | "warn" | "verbose", ...args: any[]) {
// @ts-ignore
if (!console[l]) l = "verbose";
const level = l === "verbose" ? "log" : l;
var color: "red" | "yellow" | "blue" | "reset";
switch (level) {
case "error":
color = "red";
break;
case "warn":
color = "yellow";
break;
case "info":
color = "blue";
case "log":
default:
color = "reset";
}
if (this.options.production && l === "verbose") return;
console[level](chalk[color](`[${new Date().toTimeString().split(" ")[0]}]`), ...args);
}
registerRoute(root: string, file: string): any {
if (root.endsWith("/") || root.endsWith("\\")) root = root.slice(0, -1); // removes slash at the end of the root dir
let path = file.replace(root, ""); // remove root from path and
path = path.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path
path = path.replaceAll("#", ":").replaceAll("!", "?").replaceAll("\\", "/");
if (path.endsWith("/index")) path = path.slice(0, -6); // delete index from path
if (!path.length) path = "/"; // first root index.js file must have a / path
try {
var router = require(file);
if (router.router) router = router.router;
if (router.default) router = router.default;
if (!router || router?.prototype?.constructor?.name !== "router")
throw `File doesn't export any default router`;
if (this.options.errorHandler) router.use(this.options.errorHandler);
this.app.use(path, <Router>router);
if(this.options.serverInitLogging) this.log("verbose", `[Server] Route ${path} registered`);
return router;
} catch (error) {
console.error(new Error(`[Server] Failed to register route ${path}: ${error}`));
}
}
stop() {
return new Promise<void>((res) => this.http.close(() => res()));
}
}

View File

@ -0,0 +1,40 @@
import fs from "fs";
import "missing-native-js-functions";
export interface traverseDirectoryOptions {
dirname: string;
filter?: RegExp;
excludeDirs?: RegExp;
recursive?: boolean;
}
const DEFAULT_EXCLUDE_DIR = /^\./;
const DEFAULT_FILTER = /^([^\.].*)(?<!\.d)\.(ts|js)$/;
export async function traverseDirectory<T>(
options: traverseDirectoryOptions,
action: (path: string) => T
): Promise<T[]> {
if (!options.filter) options.filter = DEFAULT_FILTER;
if (!options.excludeDirs) options.excludeDirs = DEFAULT_EXCLUDE_DIR;
const routes = fs.readdirSync(options.dirname);
const promises = <Promise<T | T[] | undefined>[]>routes
.sort((a, b) => (a.startsWith("#") ? 1 : -1)) // load #parameter routes last
.map(async (file) => {
const path = options.dirname + file;
const stat = fs.lstatSync(path);
if (path.match(<RegExp>options.excludeDirs)) return;
if (path.match(<RegExp>options.filter) && stat.isFile()) {
return action(path);
} else if (options.recursive && stat.isDirectory()) {
return traverseDirectory({ ...options, dirname: path + "/" }, action);
}
});
const result = await Promise.all(promises);
const t = <(T | undefined)[]>result.flat();
return <T[]>t.filter((x) => x != undefined);
}

View File

@ -0,0 +1,127 @@
import "missing-native-js-functions";
import { NextFunction, Request, Response } from "express";
import { HTTPError } from ".";
const OPTIONAL_PREFIX = "$";
const EMAIL_REGEX =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export function check(schema: any) {
return (req: Request, res: Response, next: NextFunction) => {
try {
const result = instanceOf(schema, req.body, { path: "body" });
if (result === true) return next();
throw result;
} catch (error) {
next(new HTTPError((error as any).toString(), 400));
}
};
}
export class Tuple {
public types: any[];
constructor(...types: any[]) {
this.types = types;
}
}
export class Email {
constructor(public email: string) {}
check() {
return !!this.email.match(EMAIL_REGEX);
}
}
export function instanceOf(
type: any,
value: any,
{ path = "", optional = false }: { path?: string; optional?: boolean } = {}
): Boolean {
if (!type) return true; // no type was specified
if (value == null) {
if (optional) return true;
throw `${path} is required`;
}
switch (type) {
case String:
if (typeof value === "string") return true;
throw `${path} must be a string`;
case Number:
value = Number(value);
if (typeof value === "number" && !isNaN(value)) return true;
throw `${path} must be a number`;
case BigInt:
try {
value = BigInt(value);
if (typeof value === "bigint") return true;
} catch (error) {}
throw `${path} must be a bigint`;
case Boolean:
if (value == "true") value = true;
if (value == "false") value = false;
if (typeof value === "boolean") return true;
throw `${path} must be a boolean`;
case Object:
if (typeof value === "object" && value !== null) return true;
throw `${path} must be a object`;
}
if (typeof type === "object") {
if (Array.isArray(type)) {
if (!Array.isArray(value)) throw `${path} must be an array`;
if (!type.length) return true; // type array didn't specify any type
return value.every((val, i) => instanceOf(type[0], val, { path: `${path}[${i}]`, optional }));
}
if (type?.constructor?.name != "Object") {
if (type instanceof Tuple) {
if (
(<Tuple>type).types.some((x) => {
try {
return instanceOf(x, value, { path, optional });
} catch (error) {
return false;
}
})
) {
return true;
}
throw `${path} must be one of ${type.types}`;
}
if (type instanceof Email) {
if ((<Email>type).check()) return true;
throw `${path} is not a valid E-Mail`;
}
if (value instanceof type) return true;
throw `${path} must be an instance of ${type}`;
}
if (typeof value !== "object") throw `${path} must be a object`;
const diff = Object.keys(value).missing(
Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x))
);
if (diff.length) throw `Unkown key ${diff}`;
return Object.keys(type).every((key) => {
let newKey = key;
const OPTIONAL = key.startsWith(OPTIONAL_PREFIX);
if (OPTIONAL) newKey = newKey.slice(OPTIONAL_PREFIX.length);
return instanceOf(type[key], value[newKey], {
path: `${path}.${newKey}`,
optional: OPTIONAL,
});
});
} else if (typeof type === "number" || typeof type === "string" || typeof type === "boolean") {
if (value === type) return true;
throw `${path} must be ${value}`;
} else if (typeof type === "bigint") {
if (BigInt(value) === type) return true;
throw `${path} must be ${value}`;
}
return type == value;
}

View File

@ -0,0 +1,4 @@
export * from "./check";
export * from "./Server";
export * from "./Utils";
export * from "./HTTPError";

View File

@ -37,7 +37,8 @@
"@spacebar/gateway*": ["./gateway"],
"@spacebar/cdn*": ["./cdn"],
"@spacebar/util*": ["./util"],
"@spacebar/webrtc*": ["./webrtc"]
"@spacebar/webrtc*": ["./webrtc"],
"lambert-server*": ["./util/util/lambert-server"]
} /* Specify a set of entries that re-map imports to additional lookup locations. */,
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */