diff --git a/src/bundle/Server.ts b/src/bundle/Server.ts
index 9cc99369..d7686692 100644
--- a/src/bundle/Server.ts
+++ b/src/bundle/Server.ts
@@ -40,7 +40,7 @@ server.on("request", app);
const api = new SpacebarServer({ server, port, production, app });
const cdn = new CDNServer({ server, port, production, app });
-const gateway = new GatewayServer({ server, port, production });
+const gateway = new GatewayServer({ server, port, production, app });
const webrtc = new WebrtcServer({
server: undefined,
port: wrtcWsPort,
diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts
index ff1fe7ab..cb2dd0d1 100644
--- a/src/gateway/Server.ts
+++ b/src/gateway/Server.ts
@@ -17,150 +17,40 @@
*/
import http from "node:http";
-import { setInterval } from "node:timers";
import ws from "ws";
+import { Server, ServerOptions } from "lambert-server";
import { initDatabase } from "@spacebar/database";
import { Random } from "@spacebar/extensions";
-import { checkToken, Config, initEvent, JwtKeypairManager, Rights } from "@spacebar/util";
+import { Config, initEvent, JwtKeypairManager } from "@spacebar/util";
import { ProcessLifecycle, SystemdLifecycle } from "../util/util/ProcessLifecycle";
import { Monitoring } from "../util/monitoring/Monitoring";
-import { Connection, openConnections } from "./events/Connection";
+import { Connection } from "./events/Connection";
import { cleanupOnStartup } from "./util";
-export class GatewayServer {
+export class GatewayServer extends Server {
public ws: ws.Server;
public port: number;
public server: http.Server;
public production: boolean;
private monitoringLoop: NodeJS.Timeout;
- constructor({ port, server, production }: { port: number; server?: http.Server; production?: boolean }) {
- this.port = port;
- this.production = production || false;
+ constructor(options?: Partial<ServerOptions>) {
+ super(options);
- if (server) this.server = server;
- else {
- const elu = [1, 5, 15].map(() => performance.eventLoopUtilization());
- const eluP = [1, 5, 15].map(() => performance.eventLoopUtilization());
- const cpu = [1, 5, 15].map(() => process.cpuUsage());
- let sec = 0;
- const monitoringLoop = setInterval(() => {
- sec += 1;
- // for some reason this behaves differently from cpuUsage, so we need an absolute reference as "previous"
- const eluC = performance.eventLoopUtilization();
+ this.server = http.createServer(async (req, res) => {
+ if (!req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid="))) {
+ res.setHeader(
+ "Set-Cookie",
+ `__sb_sessid=${Random.getString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 32)}; Secure; HttpOnly; SameSite=None; Path=/`,
+ );
+ }
+ const requestUrl = new URL(`http://${req.headers.host}${req.url}`);
+ if (requestUrl.pathname === "/metrics") {
+ return await Monitoring.handleRawRequest(req, res);
+ }
- cpu[0] = process.cpuUsage(cpu[0]);
- elu[0] = performance.eventLoopUtilization(eluP[0]);
- eluP[0] = eluC;
- if (sec % 5 === 0) {
- cpu[1] = process.cpuUsage(cpu[1]);
- elu[1] = performance.eventLoopUtilization(eluP[1]);
- eluP[1] = eluC;
- }
- if (sec % 15 === 0) {
- cpu[2] = process.cpuUsage(cpu[2]);
- elu[2] = performance.eventLoopUtilization(eluP[2]);
- eluP[2] = eluC;
- }
- }, 1000);
-
- this.server = http.createServer();
- this.server = http.createServer(async (req, res) => {
- if (!req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid="))) {
- res.setHeader(
- "Set-Cookie",
- `__sb_sessid=${Random.getString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 32)}; Secure; HttpOnly; SameSite=None; Path=/`,
- );
- }
- const requestUrl = new URL(`http://${req.headers.host}${req.url}`);
- if (requestUrl.pathname === "/metrics") {
- return await Monitoring.handleRawRequest(req, res);
- } else if (requestUrl.pathname === "/_spacebar/gateway/admin/introspect") {
- if (!req.headers.authorization) {
- return res.writeHead(401).end("Unauthorized");
- } else {
- const auth = req.headers.authorization.split(" ");
- const sess = await checkToken(auth[1]);
- if ((BigInt(sess.user.rights) & BigInt(Rights.FLAGS.OPERATOR)) === BigInt(0)) {
- return res.writeHead(401).end("Unauthorized");
- }
- }
- const useFullWsObj = requestUrl.searchParams.get("fullWs") === "true";
- res.setHeader("Content-Type", "application/json")
- .writeHead(200)
- .end(
- JSON.stringify(
- {
- uptime: process.uptime(),
- resourceUsage: process.resourceUsage(),
- eventLoop: elu,
- cpu: cpu.map((x) => ({
- user: x.user / 1000,
- system: x.system / 1000,
- })),
- socketStates: {
- open: openConnections.length,
- sessions: openConnections.map((x) =>
- // console.log(x);
- useFullWsObj
- ? {
- ...x,
- ...{
- _events: undefined,
- _closeTimer: undefined,
- accessToken: x.accessToken?.split(".")[0] + "." + x.accessToken?.split(".")[1] + ".***",
- },
- }
- : {
- wsReadystate: x.readyState,
- version: x.version,
- user_id: x.user_id,
- session_id: x.session_id,
- accessToken: x.accessToken?.split(".")[0] + "." + x.accessToken?.split(".")[1] + +".***",
- encoding: x.encoding,
- compress: x.compress,
- ipAddress: x.ipAddress,
- userAgent: x.userAgent,
- fingerprint: x.fingerprint,
- shard_count: x.shard_count,
- shard_id: x.shard_id,
- deflate: x.deflate != null,
- inflate: x.inflate != null,
- zstdEncoder: x.zstdEncoder != null,
- zstdDecoder: x.zstdDecoder != null,
- heartbeatTimeout: x.heartbeatTimeout,
- readyTimeout: x.readyTimeout,
- intents: x.intents,
- sequence: x.sequence,
- permissions: x.permissions,
- events: x.events,
- member_events: x.member_events,
- listen_options: x.listen_options,
- capabilities: x.capabilities,
- large_threshold: x.large_threshold,
- qos: x.qos,
- session: x.session,
- },
- ),
- },
- },
- (key, value) => {
- if (value === null || value === undefined) return value;
- if (Object.getPrototypeOf(value)?.constructor?.name === "Timeout") return `[Timeout] ${value._idleTimeout}ms, repeat: ${value._repeat}`;
- if (Object.getPrototypeOf(value)?.constructor?.name === "BigInt") return value.toString() + "n";
- return value;
- },
- 2,
- ),
- );
- return;
- }
-
- res.writeHead(200).end("Online");
- });
-
- ProcessLifecycle.eventEmitter.on("stopping", () => clearTimeout(monitoringLoop));
- }
+ res.writeHead(200).end("Online");
+ });
this.server.on("upgrade", (request, socket, head) => {
this.ws.handleUpgrade(request, socket, head, (socket) => {
diff --git a/src/gateway/routes/_spacebar/gateway/admin/introspect.ts b/src/gateway/routes/_spacebar/gateway/admin/introspect.ts
new file mode 100644
index 00000000..f6ab1e7b
--- /dev/null
+++ b/src/gateway/routes/_spacebar/gateway/admin/introspect.ts
@@ -0,0 +1,133 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2026 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { route } from "@spacebar/api/middlewares";
+import { Request, Response, Router } from "express";
+import { ProcessLifecycle } from "@spacebar/util/util/ProcessLifecycle";
+import { openConnections } from "@spacebar/gateway/events/Connection";
+
+const router: Router = Router({ mergeParams: true });
+
+const elu = [1, 5, 15].map(() => performance.eventLoopUtilization());
+const eluP = [1, 5, 15].map(() => performance.eventLoopUtilization());
+const cpu = [1, 5, 15].map(() => process.cpuUsage());
+let sec = 0;
+const monitoringLoop = setInterval(() => {
+ sec += 1;
+ // for some reason this behaves differently from cpuUsage, so we need an absolute reference as "previous"
+ const eluC = performance.eventLoopUtilization();
+
+ cpu[0] = process.cpuUsage(cpu[0]);
+ elu[0] = performance.eventLoopUtilization(eluP[0]);
+ eluP[0] = eluC;
+ if (sec % 5 === 0) {
+ cpu[1] = process.cpuUsage(cpu[1]);
+ elu[1] = performance.eventLoopUtilization(eluP[1]);
+ eluP[1] = eluC;
+ }
+ if (sec % 15 === 0) {
+ cpu[2] = process.cpuUsage(cpu[2]);
+ elu[2] = performance.eventLoopUtilization(eluP[2]);
+ eluP[2] = eluC;
+ }
+}, 1000);
+
+ProcessLifecycle.eventEmitter.on("stopping", () => clearTimeout(monitoringLoop));
+
+router.get(
+ "/",
+ route({
+ right: "OPERATOR",
+ responses: {
+ 200: {},
+ 403: {
+ body: "APIErrorResponse",
+ },
+ },
+ }),
+ (req: Request, res: Response) => {
+ const useFullWsObj = req.params.fullWs == "true";
+ res.set("Content-Type", "application/json").send(
+ JSON.stringify(
+ {
+ uptime: process.uptime(),
+ resourceUsage: process.resourceUsage(),
+ eventLoop: elu,
+ cpu: cpu.map((x) => ({
+ user: x.user / 1000,
+ system: x.system / 1000,
+ })),
+ socketStates: {
+ open: openConnections.length,
+ sessions: openConnections.map((x) =>
+ // console.log(x);
+ useFullWsObj
+ ? {
+ ...x,
+ ...{
+ _events: undefined,
+ _closeTimer: undefined,
+ accessToken: x.accessToken?.split(".")[0] + "." + x.accessToken?.split(".")[1] + ".***",
+ },
+ }
+ : {
+ wsReadystate: x.readyState,
+ version: x.version,
+ user_id: x.user_id,
+ session_id: x.session_id,
+ accessToken: x.accessToken?.split(".")[0] + "." + x.accessToken?.split(".")[1] + +".***",
+ encoding: x.encoding,
+ compress: x.compress,
+ ipAddress: x.ipAddress,
+ userAgent: x.userAgent,
+ fingerprint: x.fingerprint,
+ shard_count: x.shard_count,
+ shard_id: x.shard_id,
+ deflate: x.deflate != null,
+ inflate: x.inflate != null,
+ zstdEncoder: x.zstdEncoder != null,
+ zstdDecoder: x.zstdDecoder != null,
+ heartbeatTimeout: x.heartbeatTimeout,
+ readyTimeout: x.readyTimeout,
+ intents: x.intents,
+ sequence: x.sequence,
+ permissions: x.permissions,
+ events: x.events,
+ member_events: x.member_events,
+ listen_options: x.listen_options,
+ capabilities: x.capabilities,
+ large_threshold: x.large_threshold,
+ qos: x.qos,
+ session: x.session,
+ },
+ ),
+ },
+ },
+ (key, value) => {
+ if (value === null || value === undefined) return value;
+ if (Object.getPrototypeOf(value)?.constructor?.name === "Timeout") return `[Timeout] ${value._idleTimeout}ms, repeat: ${value._repeat}`;
+ if (Object.getPrototypeOf(value)?.constructor?.name === "BigInt") return value.toString() + "n";
+ return value;
+ },
+ 2,
+ ),
+ );
+ },
+);
+
+export default router;
|