mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 15:12:48 +00:00
fix(server): return 404 for missing well-known metadata (#8622)
* fix(server): return 404 for unserved well-known paths The daemon web UI middleware fell back to the SPA's index.html for every unknown GET, including /.well-known/* URIs. MCP clients that run OAuth discovery (RFC 9728 protected resource metadata, RFC 8414 authorization server metadata) received a 200 text/html body, treated it as malformed metadata and retried instead of concluding the server has no OAuth. Well-known paths now serve a real file from the dist directory if one exists and otherwise return a JSON 404. Other paths keep the SPA fallback. * fix(server): verify metadata routing through the daemon Retain the contributed well-known routing fix with named resolver options and a full daemon regression that fails when metadata requests receive app HTML. Co-authored-by: HomicideZero <219062565+HomicideZero@users.noreply.github.com> --------- Co-authored-by: HomicideZero <219062565+HomicideZero@users.noreply.github.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
HomicideZero
parent
e546497a21
commit
b507fc1986
@@ -84,6 +84,23 @@ describe("daemon web UI bootstrap", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("returns 404 for missing metadata while serving web UI deep links", async () => {
|
||||
const distDir = await createWebUiDist();
|
||||
daemonHandle = await createTestPaseoDaemon({
|
||||
mcpEnabled: true,
|
||||
webUi: { enabled: true, distDir },
|
||||
});
|
||||
const origin = `http://127.0.0.1:${daemonHandle.port}`;
|
||||
const metadata = await fetch(`${origin}/.well-known/oauth-protected-resource/mcp/agents`);
|
||||
expect(metadata.status).toBe(404);
|
||||
expect(metadata.headers.get("content-type")).toBe("application/json; charset=utf-8");
|
||||
expect(await metadata.json()).toEqual({ error: "Not found" });
|
||||
|
||||
const deepLink = await fetch(`${origin}/h/example/agent/123`);
|
||||
expect(deepLink.status).toBe(200);
|
||||
expect(await deepLink.text()).toContain("<body>app</body>");
|
||||
});
|
||||
|
||||
test("advertises ComputerName as the hostname over HTTP and WebSocket", async () => {
|
||||
daemonHandle = await createTestPaseoDaemon({ mcpEnabled: false });
|
||||
const response = await fetch(`http://127.0.0.1:${daemonHandle.port}/api/status`);
|
||||
|
||||
@@ -187,6 +187,33 @@ describe("daemon web UI route module", () => {
|
||||
expect(res.body).toContain("app");
|
||||
});
|
||||
|
||||
test.each([
|
||||
"/.well-known/oauth-protected-resource",
|
||||
"/.well-known/oauth-protected-resource/mcp/agents",
|
||||
"/.well-known/oauth-authorization-server",
|
||||
"/.well-known/openid-configuration",
|
||||
])("returns a JSON 404 instead of the SPA for missing %s", async (requestPath) => {
|
||||
const app = createApp({ enabled: true, distDir, publicDir });
|
||||
|
||||
const res = await request(app, "GET", requestPath);
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.headers["content-type"]).toBe("application/json; charset=utf-8");
|
||||
expect(JSON.parse(res.body)).toEqual({ error: "Not found" });
|
||||
});
|
||||
|
||||
test("serves well-known files that exist in the dist directory", async () => {
|
||||
await mkdir(path.join(distDir, ".well-known"), { recursive: true });
|
||||
await writeFile(path.join(distDir, ".well-known", "security.json"), '{"ok":true}');
|
||||
const app = createApp({ enabled: true, distDir, publicDir });
|
||||
|
||||
const res = await request(app, "GET", "/.well-known/security.json");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/json; charset=utf-8");
|
||||
expect(res.body).toBe('{"ok":true}');
|
||||
});
|
||||
|
||||
test("serves static assets", async () => {
|
||||
const app = createApp({ enabled: true, distDir, publicDir });
|
||||
|
||||
|
||||
@@ -15,6 +15,13 @@ function isExcludedPath(requestPath: string): boolean {
|
||||
return EXCLUDED_PATHS.has(requestPath);
|
||||
}
|
||||
|
||||
// RFC 8615 well-known URIs are machine-read metadata. Clients such as MCP OAuth
|
||||
// discovery (RFC 9728, RFC 8414) need a 404 for documents the daemon does not
|
||||
// serve; the SPA's index.html with a 200 reads as malformed metadata.
|
||||
function isWellKnownPath(requestPath: string): boolean {
|
||||
return requestPath === "/.well-known" || requestPath.startsWith("/.well-known/");
|
||||
}
|
||||
|
||||
const CONTENT_TYPES: Record<string, string> = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "application/javascript; charset=utf-8",
|
||||
@@ -71,7 +78,14 @@ interface ResolvedTarget {
|
||||
isIndexHtml: boolean;
|
||||
}
|
||||
|
||||
function resolveTargetFile(distDir: string, requestPath: string): ResolvedTarget | null {
|
||||
interface ResolveTargetFileOptions {
|
||||
distDir: string;
|
||||
requestPath: string;
|
||||
spaFallback: boolean;
|
||||
}
|
||||
|
||||
function resolveTargetFile(options: ResolveTargetFileOptions): ResolvedTarget | null {
|
||||
const { distDir, requestPath, spaFallback } = options;
|
||||
const safePath = path.normalize(requestPath).replace(/^(\.\.[/\\])+/, "");
|
||||
let filePath = path.join(distDir, safePath);
|
||||
|
||||
@@ -82,6 +96,9 @@ function resolveTargetFile(distDir: string, requestPath: string): ResolvedTarget
|
||||
|
||||
const finalStat = safeStat(filePath);
|
||||
if (!finalStat?.isFile()) {
|
||||
if (!spaFallback) {
|
||||
return null;
|
||||
}
|
||||
filePath = path.join(distDir, "index.html");
|
||||
const fallbackStat = safeStat(filePath);
|
||||
if (!fallbackStat?.isFile()) {
|
||||
@@ -190,8 +207,13 @@ interface ServeWebUiFileOptions {
|
||||
function serveWebUiFile(options: ServeWebUiFileOptions): void {
|
||||
const { distDir, requestPath, label, req, res } = options;
|
||||
|
||||
const target = resolveTargetFile(distDir, requestPath);
|
||||
const wellKnown = isWellKnownPath(requestPath);
|
||||
const target = resolveTargetFile({ distDir, requestPath, spaFallback: !wellKnown });
|
||||
if (!target) {
|
||||
if (wellKnown) {
|
||||
res.status(404).json({ error: "Not found" });
|
||||
return;
|
||||
}
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user