fix(browser): fail closed browser auth bootstrap

This commit is contained in:
Agent
2026-03-01 21:39:39 +00:00
parent 3a93a7bb1e
commit e4d22fb07a
3 changed files with 104 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
import { createServer, type AddressInfo } from "node:net";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
controlPort: 0,
ensureBrowserControlAuth: vi.fn(async () => {
throw new Error("read-only config");
}),
resolveBrowserControlAuth: vi.fn(() => ({})),
ensureExtensionRelayForProfiles: vi.fn(async () => {}),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => ({
browser: {
enabled: true,
},
}),
};
});
vi.mock("./config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./config.js")>();
return {
...actual,
resolveBrowserConfig: vi.fn(() => ({
enabled: true,
controlPort: mocks.controlPort,
})),
};
});
vi.mock("./control-auth.js", () => ({
ensureBrowserControlAuth: mocks.ensureBrowserControlAuth,
resolveBrowserControlAuth: mocks.resolveBrowserControlAuth,
}));
vi.mock("./routes/index.js", () => ({
registerBrowserRoutes: vi.fn(() => {}),
}));
vi.mock("./server-context.js", () => ({
createBrowserRouteContext: vi.fn(() => ({})),
}));
vi.mock("./server-lifecycle.js", () => ({
ensureExtensionRelayForProfiles: mocks.ensureExtensionRelayForProfiles,
stopKnownBrowserProfiles: vi.fn(async () => {}),
}));
vi.mock("./pw-ai-state.js", () => ({
isPwAiLoaded: vi.fn(() => false),
}));
const { startBrowserControlServerFromConfig, stopBrowserControlServer } =
await import("./server.js");
async function getFreePort(): Promise<number> {
const probe = createServer();
await new Promise<void>((resolve, reject) => {
probe.once("error", reject);
probe.listen(0, "127.0.0.1", () => resolve());
});
const addr = probe.address() as AddressInfo;
await new Promise<void>((resolve) => probe.close(() => resolve()));
return addr.port;
}
describe("browser control auth bootstrap failures", () => {
beforeEach(async () => {
mocks.controlPort = await getFreePort();
mocks.ensureBrowserControlAuth.mockClear();
mocks.resolveBrowserControlAuth.mockClear();
mocks.ensureExtensionRelayForProfiles.mockClear();
});
afterEach(async () => {
await stopBrowserControlServer();
});
it("fails closed when auth bootstrap throws and no auth is configured", async () => {
const started = await startBrowserControlServerFromConfig();
expect(started).toBeNull();
expect(mocks.ensureBrowserControlAuth).toHaveBeenCalledTimes(1);
expect(mocks.resolveBrowserControlAuth).toHaveBeenCalledTimes(1);
expect(mocks.ensureExtensionRelayForProfiles).not.toHaveBeenCalled();
});
});

View File

@@ -30,6 +30,7 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
}
let browserAuth = resolveBrowserControlAuth(cfg);
let browserAuthBootstrapFailed = false;
try {
const ensured = await ensureBrowserControlAuth({ cfg });
browserAuth = ensured.auth;
@@ -38,6 +39,16 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
}
} catch (err) {
logServer.warn(`failed to auto-configure browser auth: ${String(err)}`);
browserAuthBootstrapFailed = true;
}
// Fail closed: if auth bootstrap failed and no explicit auth is available,
// do not start the browser control HTTP server.
if (browserAuthBootstrapFailed && !browserAuth.token && !browserAuth.password) {
logServer.error(
"browser control startup aborted: authentication bootstrap failed and no fallback auth is configured.",
);
return null;
}
const app = express();