feat: harden plugin api for separate repos, signed updates and generic external login

This commit is contained in:
LukeGus committed 2026-09-29 18:11:59 -05:00
1 parent fb9c168b6b
commit 80944301dd
310 files changed
+6577 -4927

No files matched your search

@@ -99,7 +99,7 @@ function sessionIds(): string[] {
.map((row) => String(row.id));
}
describe("AuthManager.revokeSessionsByOidc", () => {
describe("AuthManager.revokeSessionsByExternalSession", () => {
beforeAll(() => {
mocks.sqlite!.exec(`
CREATE TABLE sessions (
@@ -147,7 +147,7 @@ describe("AuthManager.revokeSessionsByOidc", () => {
});
await expect(
authManager.revokeSessionsByOidc({
authManager.revokeSessionsByExternalSession({
ssoProviderId: 7,
sub: "subject-1",
sid: "session-1",
@@ -181,7 +181,7 @@ describe("AuthManager.revokeSessionsByOidc", () => {
});
await expect(
authManager.revokeSessionsByOidc({
authManager.revokeSessionsByExternalSession({
ssoProviderId: 7,
sub: "subject-1",
}),
@@ -192,7 +192,7 @@ describe("AuthManager.revokeSessionsByOidc", () => {
it("does not persist when no session matches", async () => {
await expect(
authManager.revokeSessionsByOidc({
authManager.revokeSessionsByExternalSession({
ssoProviderId: 7,
sid: "missing",
}),
@@ -212,7 +212,7 @@ describe("AuthManager.revokeSessionsByOidc", () => {
mocks.saveDatabase.mockRejectedValueOnce(new Error("disk full"));
await expect(
authManager.revokeSessionsByOidc({
authManager.revokeSessionsByExternalSession({
ssoProviderId: 7,
sid: "session-1",
}),
@@ -1,13 +1,13 @@
import { describe, expect, it } from "vitest";
import { isOidcTokenCallback } from "../../utils/oidc-desktop-callback.js";
import { isExternalTokenCallback } from "../../utils/external-login-callback.js";
describe("isOidcTokenCallback", () => {
describe("isExternalTokenCallback", () => {
it.each([
"http://localhost:17850/oidc-callback",
"http://127.0.0.1:17850/oidc-callback",
"termix-mobile://oidc-callback",
])("recognizes app callback %s", (url) => {
expect(isOidcTokenCallback(url)).toBe(true);
expect(isExternalTokenCallback(url)).toBe(true);
});
it.each([
@@ -15,6 +15,6 @@ describe("isOidcTokenCallback", () => {
"http://example.com:17850/oidc-callback",
"http://localhost:17850/other",
])("rejects non-app callback %s", (url) => {
expect(isOidcTokenCallback(url)).toBe(false);
expect(isExternalTokenCallback(url)).toBe(false);
});
});
@@ -1,146 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
import express from "express";
import http from "http";
import type { AddressInfo } from "net";
import { listenOnServicePort } from "../../utils/service-listen.js";
import { Logger } from "../../utils/logger.js";
function testLogger() {
const logger = new Logger("TEST", "🧪", "#000000");
return {
logger,
error: vi.spyOn(logger, "error").mockImplementation(() => {}),
};
}
function occupyPort(): Promise<{ port: number; release: () => void }> {
return new Promise((resolve) => {
const blocker = http.createServer();
blocker.listen(0, "127.0.0.1", () => {
resolve({
port: (blocker.address() as AddressInfo).port,
release: () => blocker.close(),
});
});
});
}
const servers: http.Server[] = [];
afterEach(() => {
while (servers.length) servers.pop()?.close();
vi.restoreAllMocks();
});
describe("listenOnServicePort", () => {
it("starts the service and reports listening on a free port", async () => {
const { logger, error } = testLogger();
const onListening = vi.fn();
const server = listenOnServicePort({
app: express(),
port: 0,
logger,
serviceName: "test",
onListening,
});
servers.push(server);
await vi.waitFor(() => expect(onListening).toHaveBeenCalledOnce());
expect(server.listening).toBe(true);
expect(error).not.toHaveBeenCalled();
});
// The bug this helper exists for: express's app.listen(port, host, cb)
// registers cb as the server's error handler too, so a port conflict
// invoked the "service started" callback and emitted nothing at all.
it("does not report the service as started when the port is taken", async () => {
const { port, release } = await occupyPort();
const { logger } = testLogger();
const onListening = vi.fn();
const exit = vi
.spyOn(process, "exit")
.mockImplementation(() => undefined as never);
const server = listenOnServicePort({
app: express(),
port,
logger,
serviceName: "test",
onListening,
});
servers.push(server);
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
expect(onListening).not.toHaveBeenCalled();
release();
});
it("names the conflicting port so the desktop can surface it", async () => {
const { port, release } = await occupyPort();
const { logger, error } = testLogger();
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
servers.push(
listenOnServicePort({
app: express(),
port,
logger,
serviceName: "metrics",
}),
);
await vi.waitFor(() => expect(error).toHaveBeenCalled());
const [message, , context] = error.mock.calls[0];
expect(message).toContain(String(port));
expect(context).toMatchObject({
operation: "service_port_conflict",
port,
service: "metrics",
});
release();
});
it("leaves any other listen error to the process-level handlers", async () => {
const { logger } = testLogger();
const exit = vi
.spyOn(process, "exit")
.mockImplementation(() => undefined as never);
const server = listenOnServicePort({
app: express(),
port: 0,
logger,
serviceName: "test",
});
servers.push(server);
const notPortConflict = Object.assign(new Error("boom"), { code: "EPERM" });
expect(() => server.emit("error", notPortConflict)).toThrow("boom");
expect(exit).not.toHaveBeenCalled();
});
});
// express's app.listen() swallows a bind failure by design (see
// service-listen.ts), so no service may reach for it again.
describe("backend services", () => {
function sourceFiles(dir: string): string[] {
return readdirSync(dir).flatMap((entry) => {
const full = path.join(dir, entry);
if (statSync(full).isDirectory()) {
return entry === "tests" ? [] : sourceFiles(full);
}
return full.endsWith(".ts") ? [full] : [];
});
}
it("never bind a port with express's app.listen()", () => {
const offenders = sourceFiles(path.resolve("src/backend"))
// service-listen.ts names the call only to document why it is banned.
.filter((file) => !file.endsWith(path.join("utils", "service-listen.ts")))
.filter((file) => /\bapp\.listen\(/.test(readFileSync(file, "utf8")));
expect(offenders).toEqual([]);
});
});
+59 -1
View File
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import type { IncomingMessage } from "http";
import { extractWebSocketToken } from "../../utils/ws-auth.js";
import {
extractWebSocketToken,
isCookieOriginAllowed,
} from "../../utils/ws-auth.js";
function request(headers: Record<string, string>): IncomingMessage {
return { headers } as IncomingMessage;
@@ -26,3 +29,58 @@ describe("extractWebSocketToken", () => {
).toBe("cookie-token");
});
});
describe("the jwt cookie on a socket", () => {
it("counts when the page is this server", () => {
expect(
extractWebSocketToken(
request({
cookie: "jwt=cookie-token",
host: "termix.example.com",
origin: "https://termix.example.com",
}),
),
).toBe("cookie-token");
});
it("is ignored when another site opened the socket", () => {
expect(
extractWebSocketToken(
request({
cookie: "jwt=cookie-token",
host: "termix.example.com",
origin: "https://evil.example.com",
}),
),
).toBeUndefined();
});
it("still takes an explicit token from another origin", () => {
expect(
extractWebSocketToken(
request({
cookie: "jwt=cookie-token",
host: "termix.example.com",
origin: "https://evil.example.com",
"sec-websocket-protocol": "termix.jwt.protocol-token",
}),
),
).toBe("protocol-token");
});
it("allows the dev proxy, which rewrites Host to loopback", () => {
expect(
isCookieOriginAllowed(
request({ host: "127.0.0.1:30001", origin: "http://localhost:5173" }),
),
).toBe(true);
});
it("allows the desktop app, which is not a web origin", () => {
expect(
isCookieOriginAllowed(
request({ host: "termix.example.com", origin: "file://" }),
),
).toBe(true);
});
});