From e7ad5580ec8cba75bde1d0963d447da7af25dbb5 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Sat, 11 Jul 2026 14:55:29 +0800 Subject: [PATCH] fix: accept SDK provider capability headers --- hub/package-lock.json | 4 ++-- hub/package.json | 2 +- hub/src/connections/providerProxy.ts | 19 +++++++++++++++---- hub/test/unit/provider-proxy.test.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/hub/package-lock.json b/hub/package-lock.json index 781cc2c..e944447 100644 --- a/hub/package-lock.json +++ b/hub/package-lock.json @@ -1,12 +1,12 @@ { "name": "@paradigm/hub", - "version": "0.0.14", + "version": "0.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@paradigm/hub", - "version": "0.0.14", + "version": "0.0.15", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", diff --git a/hub/package.json b/hub/package.json index f3073fa..4e77d3c 100644 --- a/hub/package.json +++ b/hub/package.json @@ -1,6 +1,6 @@ { "name": "@paradigm/hub", - "version": "0.0.14", + "version": "0.0.15", "private": true, "type": "module", "engines": { diff --git a/hub/src/connections/providerProxy.ts b/hub/src/connections/providerProxy.ts index 271fdb3..8850fcf 100644 --- a/hub/src/connections/providerProxy.ts +++ b/hub/src/connections/providerProxy.ts @@ -102,7 +102,7 @@ async function forwardProviderRequest( upstream: ProviderUpstreamCredential, options: ProviderProxyOptions, ): Promise { - if (!authorized(request.headers.authorization, capability)) { + if (!authorized(request.headers.authorization, header(request.headers["x-api-key"]), capability)) { options.onDiagnostic?.({ code: "provider_proxy_unauthorized", category: "authorization" }); response.writeHead(401, { "content-type": "text/plain; charset=utf-8" }); response.end("unauthorized"); @@ -165,13 +165,24 @@ async function forwardProviderRequest( } } -function authorized(value: string | undefined, capability: string): boolean { - if (value === undefined || !value.startsWith("Bearer ")) return false; - const supplied = Buffer.from(value.slice("Bearer ".length)); +function authorized( + authorization: string | undefined, + apiKey: string | undefined, + capability: string, +): boolean { + const value = authorization?.startsWith("Bearer ") + ? authorization.slice("Bearer ".length) + : apiKey; + if (value === undefined) return false; + const supplied = Buffer.from(value); const expected = Buffer.from(capability); return supplied.byteLength === expected.byteLength && timingSafeEqual(supplied, expected); } +function header(value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value; +} + function forwardedRequestHeaders(source: IncomingHttpHeaders): Headers { const result = new Headers(); for (const [name, value] of Object.entries(source)) { diff --git a/hub/test/unit/provider-proxy.test.ts b/hub/test/unit/provider-proxy.test.ts index 30875b3..cd7791c 100644 --- a/hub/test/unit/provider-proxy.test.ts +++ b/hub/test/unit/provider-proxy.test.ts @@ -107,6 +107,33 @@ describe("run-scoped provider proxy", () => { expect(diagnostics).toEqual([{ code: "provider_proxy_unauthorized", category: "authorization" }]); }); + it("accepts the run capability in the Anthropic x-api-key header", async () => { + let upstreamRequests = 0; + const upstream = createServer((_request, response) => { + upstreamRequests += 1; + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ ok: true })); + }); + upstreamServers.push(upstream); + upstream.listen(0, "127.0.0.1"); + await once(upstream, "listening"); + const address = upstream.address(); + if (address === null || typeof address === "string") throw new Error("expected upstream TCP address"); + const lease = await openProviderProxyLease({ + baseUrl: `http://127.0.0.1:${address.port}`, + authToken: "customer-secret", + anthropicApiKey: "", + }); + leases.push(lease); + + const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, { + headers: { "x-api-key": lease.sdkEnv.ANTHROPIC_AUTH_TOKEN ?? "" }, + }); + + expect(response.status).toBe(200); + expect(upstreamRequests).toBe(1); + }); + it("does not forward provider credentials across an upstream redirect", async () => { let redirectedRequests = 0; const diagnostics: unknown[] = [];