mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
fix(proxy): never route an sk-ant bearer to the OpenAI upstream (#49)
Prevent Anthropic bearer tokens from being forwarded to OpenAI when routing /v1/models. Add tests for Anthropic, OpenAI, and x-api-key routing.
This commit is contained in:
+8
-1
@@ -564,7 +564,14 @@ function isOpenAIResponsesPath(pathname: string): boolean {
|
||||
|
||||
function isCanonicalOpenAIPath(pathname: string, headers: Headers, hasOpenAIKey: boolean): boolean {
|
||||
const isModelsPath = pathname === '/v1/models' || pathname.startsWith('/v1/models/');
|
||||
const looksOpenAIAuth = hasOpenAIKey || (headers.has('authorization') && !headers.has('x-api-key'));
|
||||
// `/v1/models` exists on BOTH APIs, so it is routed by auth style — but an
|
||||
// `sk-ant-…` bearer is Anthropic by construction (Claude Code subscription
|
||||
// auth sends `authorization: Bearer sk-ant-oat01-…` with no x-api-key).
|
||||
// Without this check that OAuth token would be forwarded to the OpenAI
|
||||
// upstream: a credential leak, and a guaranteed 401.
|
||||
const bearerIsAnthropic = /^Bearer\s+sk-ant-/i.test(headers.get('authorization') ?? '');
|
||||
const looksOpenAIAuth =
|
||||
hasOpenAIKey || (headers.has('authorization') && !headers.has('x-api-key') && !bearerIsAnthropic);
|
||||
return pathname === '/v1/chat/completions'
|
||||
|| pathname === '/v1/responses'
|
||||
|| pathname.startsWith('/v1/responses/')
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* /v1/models routing by auth style. The path exists on BOTH APIs, so the
|
||||
* proxy sniffs the auth header — but an `sk-ant-…` bearer is Anthropic by
|
||||
* construction (Claude Code subscription auth sends
|
||||
* `authorization: Bearer sk-ant-oat01-…` with no x-api-key). It must never
|
||||
* be forwarded to the OpenAI upstream. All tokens here are fake; the suite
|
||||
* never touches the network (global fetch is stubbed).
|
||||
*/
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createProxy } from '../src/core/proxy.js';
|
||||
|
||||
const realFetch = globalThis.fetch;
|
||||
afterEach(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
});
|
||||
|
||||
function stubFetch(capture: { url?: string; headers?: Headers }) {
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capture.url = String(input);
|
||||
capture.headers = new Headers(init?.headers);
|
||||
return new Response(JSON.stringify({ data: [] }), {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
}
|
||||
|
||||
describe('/v1/models auth-style routing', () => {
|
||||
const proxy = () => createProxy({});
|
||||
|
||||
it('routes an sk-ant bearer (Claude Code OAuth) to the Anthropic upstream', async () => {
|
||||
const cap: { url?: string; headers?: Headers } = {};
|
||||
stubFetch(cap);
|
||||
await proxy()(
|
||||
new Request('http://localhost/v1/models', {
|
||||
method: 'GET',
|
||||
headers: { authorization: 'Bearer sk-ant-oat01-fake-oauth-token' },
|
||||
}),
|
||||
);
|
||||
expect(cap.url).toBe('https://api.anthropic.com/v1/models');
|
||||
// The token stays on the Anthropic leg, never crosses to api.openai.com.
|
||||
expect(cap.headers?.get('authorization')).toBe('Bearer sk-ant-oat01-fake-oauth-token');
|
||||
});
|
||||
|
||||
it('still routes a non-Anthropic bearer to the OpenAI upstream', async () => {
|
||||
const cap: { url?: string; headers?: Headers } = {};
|
||||
stubFetch(cap);
|
||||
await proxy()(
|
||||
new Request('http://localhost/v1/models', {
|
||||
method: 'GET',
|
||||
headers: { authorization: 'Bearer fake-openai-key' },
|
||||
}),
|
||||
);
|
||||
expect(cap.url).toBe('https://api.openai.com/v1/models');
|
||||
});
|
||||
|
||||
it('still routes x-api-key requests to the Anthropic upstream', async () => {
|
||||
const cap: { url?: string; headers?: Headers } = {};
|
||||
stubFetch(cap);
|
||||
await proxy()(
|
||||
new Request('http://localhost/v1/models', {
|
||||
method: 'GET',
|
||||
headers: { 'x-api-key': 'fake-anthropic-key' },
|
||||
}),
|
||||
);
|
||||
expect(cap.url).toBe('https://api.anthropic.com/v1/models');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user