fix: accept SDK provider capability headers

This commit is contained in:
2026-07-11 14:55:29 +08:00
parent 19d942e812
commit 6228460877
4 changed files with 45 additions and 7 deletions
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@paradigm/hub",
"version": "0.0.14",
"version": "0.0.15",
"private": true,
"type": "module",
"engines": {
+15 -4
View File
@@ -102,7 +102,7 @@ async function forwardProviderRequest(
upstream: ProviderUpstreamCredential,
options: ProviderProxyOptions,
): Promise<void> {
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)) {
+27
View File
@@ -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[] = [];