feat: serve plugin http and websockets from one mount instead of ports

This commit is contained in:
LukeGus committed 2026-09-22 16:39:41 -05:00
1 parent c496a093ca
commit 8431570ff7
102 files changed
+2894 -2308

No files matched your search

@@ -1,94 +0,0 @@
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import ts from "typescript";
import { beforeEach, expect, it, vi } from "vitest";
// Load the production route without starting metrics polling or SSH services.
const source = readFileSync(
new URL("../../src/backend/routes.ts", import.meta.url),
"utf8",
);
const ast = ts.createSourceFile(
"routes.ts",
source,
ts.ScriptTarget.Latest,
true,
);
const route = ast.statements.find(
(node) =>
ts.isExpressionStatement(node) &&
ts.isCallExpression(node.expression) &&
node.expression.expression.getText(ast) === "app.post" &&
node.expression.arguments[0]?.getText(ast) === '"/internal/login-alert"',
);
if (!route) throw new Error("Login event route is missing");
const notify = vi.fn();
let handler: (req: unknown, res: unknown) => Promise<void>;
runInNewContext(
ts.transpileModule(route.getText(ast), {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
},
}).outputText,
{
app: {
post: (_path: string, callback: typeof handler) => {
handler = callback;
},
},
require: () => ({
SystemCrypto: {
getInstance: () => ({
getInternalAuthToken: async () => "internal-test-token",
}),
},
}),
notifyAutomationInternalEvent: notify,
},
);
const body = {
hostId: 42,
userId: "owner",
sshUser: "alice",
fromIp: "192.0.2.4",
};
const response = () => ({ status: vi.fn().mockReturnThis(), json: vi.fn() });
beforeEach(() => vi.clearAllMocks());
it("delivers successful SSH login details to automations", async () => {
const res = response();
await handler(
{
socket: { remoteAddress: "127.0.0.1" },
headers: { "x-internal-auth": "internal-test-token" },
body,
},
res,
);
expect(notify).toHaveBeenCalledExactlyOnceWith("user_login", "owner", 42, {
sshUser: "alice",
fromIp: "192.0.2.4",
});
expect(res.json).toHaveBeenCalledWith({ ok: true });
});
it.each([
["192.0.2.9", "internal-test-token"],
["127.0.0.1", "incorrect"],
])(
"rejects untrusted login notifications (%s)",
async (remoteAddress, token) => {
const res = response();
await handler(
{
socket: { remoteAddress },
headers: { "x-internal-auth": token },
body,
},
res,
);
expect(res.status).toHaveBeenCalledWith(403);
expect(notify).not.toHaveBeenCalled();
},
);
@@ -1,27 +0,0 @@
import { describe, it, expect } from "vitest";
import fs from "fs";
import path from "path";
// Regression guard for: the /internal/login-alert route was registered
// after the global JWT auth middleware, so every service-to-service login
// alert got rejected with 401 before the route's own IP+token check ever
// ran. Spinning up the full metrics-service Express app (DB, SSH clients,
// polling managers, etc.) just to hit this one route is out of scope, so
// this asserts the registration order directly against the source instead.
describe("metrics service /internal/login-alert route order", () => {
it("is registered before the global auth middleware", () => {
const source = fs.readFileSync(
path.resolve(__dirname, "../../src/backend/routes.ts"),
"utf8",
);
const routeIndex = source.indexOf('app.post("/internal/login-alert"');
const authMiddlewareIndex = source.indexOf(
"app.use(authManager.createAuthMiddleware())",
);
expect(routeIndex).toBeGreaterThan(-1);
expect(authMiddlewareIndex).toBeGreaterThan(-1);
expect(routeIndex).toBeLessThan(authMiddlewareIndex);
});
});
@@ -0,0 +1,84 @@
/**
* SSH login alerts reach automations.
*
* This used to be POST /internal/login-alert on this plugin's own port,
* guarded by a localhost IP check plus a shared internal token, and the tests
* here covered both of those plus the middleware ordering bug that once made
* every alert 401. A4 replaced the call with an in-process event, so the
* transport, the shared secret and the ordering hazard are all gone; what is
* left to assert is that the handler forwards the right thing and ignores a
* payload it cannot use.
*
* The route module pulls in the whole metrics stack, so this extracts the
* handler from source rather than importing it, the same way the old test did.
*/
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import ts from "typescript";
import { beforeEach, expect, it, vi } from "vitest";
const source = readFileSync(
new URL("../../src/backend/routes.ts", import.meta.url),
"utf8",
);
const ast = ts.createSourceFile(
"routes.ts",
source,
ts.ScriptTarget.Latest,
true,
);
const declaration = ast.statements.find(
(node) =>
ts.isFunctionDeclaration(node) && node.name?.getText(ast) === "onHostLogin",
);
if (!declaration) throw new Error("onHostLogin is missing");
const notify = vi.fn();
const context: Record<string, unknown> = {
notifyAutomationInternalEvent: notify,
exports: {},
};
runInNewContext(
ts.transpileModule(declaration.getText(ast), {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
},
}).outputText,
context,
);
const onHostLogin = context.onHostLogin as (payload: {
hostId: number;
userId: string;
sshUser: string;
fromIp: string;
}) => void;
beforeEach(() => vi.clearAllMocks());
it("delivers successful SSH login details to automations", () => {
onHostLogin({
hostId: 42,
userId: "owner",
sshUser: "alice",
fromIp: "192.0.2.4",
});
expect(notify).toHaveBeenCalledExactlyOnceWith("user_login", "owner", 42, {
sshUser: "alice",
fromIp: "192.0.2.4",
});
});
it.each([
["no host", { hostId: 0, userId: "owner", sshUser: "a", fromIp: "1.2.3.4" }],
["no user", { hostId: 42, userId: "", sshUser: "a", fromIp: "1.2.3.4" }],
])("ignores an unusable payload (%s)", (_label, payload) => {
onHostLogin(payload as Parameters<typeof onHostLogin>[0]);
expect(notify).not.toHaveBeenCalled();
});