import { createCoreClient, createOAuthHandler, PublicError } from "forkhorse-mcp";
import type { OAuthEnv, OwnerIdentity } from "forkhorse-mcp";
import { dispatchApi } from "./api";
import { verifyOwner } from "./access";
import { errorResponse, HttpError, json, operation, slug } from "./http";
import type { Env } from "./types";
import brand from "../../../brand.json";
import { markIconUrl } from "../../forkhorse-dashboard/src/mark";
function settings(env: Env) {
const origin = env.CORE_PUBLIC_ORIGIN;
const path = env.MCP_PATH ?? "/mcp";
const project = env.MCP_PROJECT;
if (
!origin ||
new URL(origin).origin !== origin ||
!origin.startsWith("https://") ||
!/^\/[a-z0-9-]+$/.test(path) ||
["/app", "/projects", "/authorize", "/oauth"].includes(path) ||
!slug(project)
)
throw new HttpError(503, "Public core configuration is incomplete.");
return { origin, path, project };
}
function requireHost(request: Request, origin: string) {
if (new URL(request.url).origin !== origin) throw new HttpError(403, "Host is not allowed.");
}
async function oauth(request: Request, env: Env, ctx: ExecutionContext) {
const config = settings(env);
if (!env.OAUTH_KV) throw new HttpError(503, "OAuth storage is not configured.");
// This proof lives for one request; only the Access verifier can populate it.
let verifiedOwner: OwnerIdentity | null = null;
const core = createCoreClient({
async authenticate(token, signal) {
signal.throwIfAborted();
const identity = await env.SUPERVISOR.get(
env.SUPERVISOR.idFromName(config.project),
).authenticateAgent(token);
signal.throwIfAborted();
return identity;
},
fetch: (coreRequest) => dispatchApi(coreRequest, env),
async enrollAgent(owner, project, agent, signal) {
signal.throwIfAborted();
if (!verifiedOwner || owner.subject !== verifiedOwner.subject || project !== config.project)
throw new PublicError(403, "Owner authorization is required.");
const response = await dispatchApi(
new Request(`https://core.invalid/projects/${project}/agents`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ agent }),
signal,
}),
env,
true,
);
if (response.status !== 201) throw new PublicError(502, "Agent enrollment failed.");
const enrolled = await response.json<{ agent: string; apiToken: string }>();
return { project, ...enrolled };
},
});
const response = await createOAuthHandler({
core,
publicOrigin: config.origin,
path: config.path,
project: config.project,
async authorizeOwner(ownerRequest, signal) {
signal.throwIfAborted();
verifiedOwner = await verifyOwner(ownerRequest, env);
signal.throwIfAborted();
return verifiedOwner;
},
})(request, { OAUTH_KV: env.OAUTH_KV } as unknown as OAuthEnv, ctx);
// Add identity only to successful consent HTML; preserve redirects, cookies and auth checks.
if (new URL(request.url).pathname === "/authorize" && request.method === "GET" &&
response.status === 200 && response.headers.get("content-type")?.includes("text/html")) {
return new HTMLRewriter().on("head", {
element(element) {
element.append(``, { html: true });
},
}).transform(response);
}
return response;
}
async function dashboard(request: Request, env: Env): Promise {
const config = settings(env);
requireHost(request, config.origin);
if (!(await verifyOwner(request, env)))
throw new HttpError(401, "Owner Access authentication is required.");
if (!["GET", "HEAD"].includes(request.method)) return json({ error: "Method not allowed." }, 405);
if (!env.ASSETS) throw new HttpError(503, "Dashboard assets are not configured.");
const url = new URL(request.url);
url.pathname = url.pathname.replace(/^\/app\/?/, "/");
if (url.pathname === "/") url.pathname = "/index.html";
// The binding is reached only after verification, even for JS/CSS. Do not forward credentials.
const asset = await env.ASSETS.fetch(new Request(url, { method: request.method }));
const headers = new Headers(asset.headers);
headers.set("cache-control", "private, no-store");
headers.set("x-content-type-options", "nosniff");
headers.set("referrer-policy", "same-origin");
headers.set(
"content-security-policy",
"default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
);
headers.delete("etag");
let response = new Response(asset.body, { status: asset.status, headers });
if (request.method === "GET" && asset.ok && headers.get("content-type")?.includes("text/html")) {
const data = JSON.stringify({
projects: [{ id: config.project, label: config.project }],
}).replaceAll("<", "\\u003c");
response = new HTMLRewriter()
.on("head", {
element(element) {
element.append(``, {
html: true,
});
},
})
.transform(response);
}
return response;
}
export async function publicFetch(
request: Request,
env: Env,
ctx?: ExecutionContext,
): Promise {
try {
const url = new URL(request.url);
if (url.pathname === "/app" || url.pathname.startsWith("/app/"))
return await dashboard(request, env);
if (
url.pathname === (env.MCP_PATH ?? "/mcp") ||
url.pathname === "/authorize" ||
url.pathname.startsWith("/oauth/") ||
url.pathname.startsWith("/.well-known/")
) {
if (!ctx) throw new HttpError(503, "OAuth execution context is unavailable.");
return await oauth(request, env, ctx);
}
if (url.pathname === "/projects" || url.pathname.startsWith("/projects/")) {
// Browser owner authority comes from Access, never a client-selected identity/header.
if (
request.headers.has("origin") ||
!request.headers.has("authorization") ||
request.headers.has("cf-access-jwt-assertion")
) {
const { origin } = settings(env);
requireHost(request, origin);
if (!(await verifyOwner(request, env)))
throw new HttpError(401, "Owner Access authentication is required.");
const suppliedOrigin = request.headers.get("origin");
if (
(suppliedOrigin && suppliedOrigin !== origin) ||
(!["GET", "HEAD"].includes(request.method) &&
(suppliedOrigin !== origin ||
request.headers.get("content-type")?.split(";")[0]?.trim() !== "application/json"))
) {
throw new HttpError(403, "Owner request origin or content type is not allowed.");
}
return await dispatchApi(request, env, true);
}
return await dispatchApi(request, env);
}
return json({ error: "Not found." }, 404);
} catch (error) {
return errorResponse(error, { op: operation(request) });
}
}