Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion mcp/src/lab/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ function bridge(factory: () => Promise<{ app: { fetch: any } }>) {
* Registration MUST happen before `express.json()` so vein receives the
* raw request stream (same constraint as the graph SSE routes).
*/
/**
* Gate every /lab route behind the mcp-wide API_TOKEN (unset = dev mode =
* open, the same posture as the /events route). Two accepted credentials:
* HTTP Basic `admin:<API_TOKEN>` — the browser prompts once for the UI and
* then attaches it to every request including EventSource streams, which
* cannot carry custom headers — and the `x-api-token` header, matching the
* rest of mcp for server-to-server callers.
*/
function labAuth(req: Request, res: Response, next: NextFunction): void {
const apiToken = process.env.API_TOKEN;
if (!apiToken) return next();
if (req.header("x-api-token") === apiToken) return next();
const header = req.header("authorization") ?? "";
if (header.startsWith("Basic ")) {
const decoded = Buffer.from(header.slice(6), "base64").toString();
const sep = decoded.indexOf(":");
const user = decoded.slice(0, sep);
const pass = decoded.slice(sep + 1);
if (sep > 0 && user === "admin" && pass === apiToken) return next();
}
res.set("WWW-Authenticate", 'Basic realm="stakgraph-lab"');
res.status(401).json({ error: "Unauthorized" });
}

export function mountLab(app: Express): void {
// Trailing slash so the SPA's relative asset URLs resolve under /lab/.
// Express routing is non-strict, so `/lab` also matches `/lab/`; guard
Expand All @@ -45,5 +69,5 @@ export function mountLab(app: Express): void {
if (req.path === "/lab/") return next();
res.redirect(308, "/lab/");
});
app.use("/lab", bridge(() => createLabVein({ serveUi: true })));
app.use("/lab", labAuth, bridge(() => createLabVein({ serveUi: true })));
}
Loading