feat(http): allow config.apiKey fallback on server for single-tenant deployments

This commit is contained in:
Lucas Winther
2026-09-07 01:23:31 +02:00
parent 914883957e
commit 2c1ff8f7db
2 changed files with 31 additions and 2 deletions
+10 -2
View File
@@ -37,7 +37,11 @@ export interface HttpServerInstance {
* 2. X-OpenProject-Api-Key: <key> * 2. X-OpenProject-Api-Key: <key>
* 3. ?apiKey=<key> query parameter * 3. ?apiKey=<key> query parameter
*/ */
export function extractApiKey(req: Request, url: URL): string | undefined { export function extractApiKey(
req: Request,
url: URL,
defaultApiKey?: string
): string | undefined {
const authHeader = req.headers.get("authorization"); const authHeader = req.headers.get("authorization");
if (authHeader) { if (authHeader) {
const match = authHeader.match(/^Bearer\s+(.+)$/i); const match = authHeader.match(/^Bearer\s+(.+)$/i);
@@ -56,6 +60,10 @@ export function extractApiKey(req: Request, url: URL): string | undefined {
return queryKey.trim(); return queryKey.trim();
} }
if (defaultApiKey && defaultApiKey.trim().length > 0) {
return defaultApiKey.trim();
}
return undefined; return undefined;
} }
@@ -192,7 +200,7 @@ export async function startHttpServer(config: AppConfig): Promise<HttpServerInst
}); });
} }
const apiKey = extractApiKey(req, url); const apiKey = extractApiKey(req, url, config.apiKey);
if (!apiKey) { if (!apiKey) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
+21
View File
@@ -243,6 +243,27 @@ describe("Hosted Remote MCP Server (HTTP/SSE)", () => {
expect(res.headers.get("access-control-allow-methods")).toContain("GET"); expect(res.headers.get("access-control-allow-methods")).toContain("GET");
}); });
test("GET /sse falls back to config.apiKey when configured on server", async () => {
const singleTenantServer = await startHttpServer({
baseUrl: "https://mock.openproject.example.com",
readOnly: true,
port: 0,
host: "127.0.0.1",
apiKey: "server-configured-key",
});
const res = await fetch(`http://127.0.0.1:${singleTenantServer.port}/sse`, {
headers: { Accept: "text/event-stream" },
});
expect(res.status).toBe(200);
const reader = res.body?.getReader();
const { value } = await reader!.read();
const text = new TextDecoder().decode(value);
expect(text).toContain("event: endpoint");
await reader!.cancel();
await singleTenantServer.stop();
});
test("concurrent clients with different API keys maintain isolated sessions", async () => { test("concurrent clients with different API keys maintain isolated sessions", async () => {
// Client A // Client A
const resA = await fetch(`${serverUrl}/sse?apiKey=user-a-secret`, { const resA = await fetch(`${serverUrl}/sse?apiKey=user-a-secret`, {