@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
341 lines • 14.4 kB
JavaScript
/**
* Shared OAuth fetch wrapper for Anthropic API requests.
*
* Extracted from `providers/anthropic.ts` so that any provider or proxy layer
* that needs OAuth-authenticated Anthropic requests can reuse the same logic.
*
* Applies full "cloaking" to make requests indistinguishable from the
* official Claude CLI / CLIProxyAPI, which is required for OAuth + tools
* to work correctly.
*
* @module proxy/oauthFetch
*/
import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, MCP_TOOL_PREFIX, } from "../auth/anthropicOAuth.js";
import { logger } from "../utils/logger.js";
import { createProxyFetch } from "./proxyFetch.js";
// Re-export constants for consumers that previously imported them alongside
// the function from `providers/anthropic.ts`.
export { CLAUDE_CLI_USER_AGENT, MCP_TOOL_PREFIX };
function resolveOAuthRequestUrl(input) {
try {
if (typeof input === "string" || input instanceof URL) {
return new URL(input.toString());
}
if (input instanceof Request) {
return new URL(input.url);
}
}
catch {
return null;
}
return null;
}
function mergeRequestHeaders(input, init) {
const requestHeaders = new Headers();
if (input instanceof Request) {
input.headers.forEach((value, key) => {
requestHeaders.set(key, value);
});
}
if (!init?.headers) {
return requestHeaders;
}
if (init.headers instanceof Headers) {
init.headers.forEach((value, key) => {
requestHeaders.set(key, value);
});
return requestHeaders;
}
if (Array.isArray(init.headers)) {
for (const [key, value] of init.headers) {
if (typeof value !== "undefined") {
requestHeaders.set(key, String(value));
}
}
return requestHeaders;
}
for (const [key, value] of Object.entries(init.headers)) {
if (typeof value !== "undefined") {
requestHeaders.set(key, String(value));
}
}
return requestHeaders;
}
function applyOAuthHeaders(requestHeaders, getToken, includeOptionalBetas, skipBodyTransform) {
const existingBetas = (requestHeaders.get("anthropic-beta") ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const requiredBetas = ["oauth-2025-04-20"];
if (!skipBodyTransform) {
requiredBetas.push(...(includeOptionalBetas
? CLAUDE_CODE_OAUTH_BETAS.filter((beta) => beta !== "oauth-2025-04-20")
: []));
}
requestHeaders.set("authorization", `Bearer ${getToken()}`);
requestHeaders.set("anthropic-beta", [...new Set([...existingBetas, ...requiredBetas])].join(","));
requestHeaders.delete("x-api-key");
if (skipBodyTransform) {
return;
}
requestHeaders.set("user-agent", CLAUDE_CLI_USER_AGENT);
requestHeaders.set("anthropic-version", "2023-06-01");
requestHeaders.set("accept", "application/json");
requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
requestHeaders.set("x-app", "cli");
requestHeaders.set("connection", "keep-alive");
requestHeaders.set("x-stainless-retry-count", "0");
requestHeaders.set("x-stainless-runtime-version", "v24.3.0");
requestHeaders.set("x-stainless-package-version", "0.74.0");
requestHeaders.set("x-stainless-runtime", "node");
requestHeaders.set("x-stainless-lang", "js");
requestHeaders.set("x-stainless-arch", process.arch === "x64" ? "x64" : process.arch);
requestHeaders.set("x-stainless-os", process.platform === "darwin"
? "MacOS"
: process.platform === "win32"
? "Windows"
: "Linux");
requestHeaders.set("x-stainless-timeout", "600");
}
async function resolveOAuthRequestBody(input, init) {
const sourceRequest = input instanceof Request ? input : undefined;
const method = init?.method ?? sourceRequest?.method;
let body = init?.body;
if (body === undefined &&
sourceRequest &&
method !== "GET" &&
method !== "HEAD") {
const contentType = sourceRequest.headers.get("content-type") ?? "";
if (contentType.includes("application/json")) {
body = (await sourceRequest.clone().text()) || undefined;
}
else {
body = sourceRequest.clone().body ?? undefined;
}
}
return { sourceRequest, method, body: body ?? undefined };
}
function transformOAuthJsonBody(body, requestHeaders, getToken, enableMcpPrefix) {
const parsed = JSON.parse(body);
if (enableMcpPrefix) {
if (parsed.tools && Array.isArray(parsed.tools)) {
parsed.tools = parsed.tools.map((tool) => ({
...tool,
name: tool.name ? `${MCP_TOOL_PREFIX}${tool.name}` : tool.name,
}));
}
if (parsed.messages && Array.isArray(parsed.messages)) {
parsed.messages = parsed.messages.map((msg) => {
if (msg.content && Array.isArray(msg.content)) {
msg.content = msg.content.map((block) => {
const b = block;
if (b.type === "tool_use" && b.name) {
return {
...b,
name: `${MCP_TOOL_PREFIX}${b.name}`,
};
}
return block;
});
}
return msg;
});
}
}
if (parsed.tool_choice?.type === "any" ||
parsed.tool_choice?.type === "tool") {
delete parsed.thinking;
}
const agentBlock = {
type: "text",
text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
};
// Normalise `system` to an array and APPEND billing + agent blocks.
// IMPORTANT: We append (not prepend) to preserve the client's cache
// prefix chain. Anthropic's prompt caching uses prefix matching — if
// we insert anything before the client's system blocks, we invalidate
// all cached content (tools, system prompt, message history).
//
// Claude Code sends a billing block with a `cch=<hash>` value that
// changes on every request. We remove any existing billing/agent
// blocks from their positions and always append our stable
// Claude-Code-shaped versions at the end.
if (parsed.system) {
if (typeof parsed.system === "string") {
parsed.system = [{ type: "text", text: parsed.system }];
}
if (Array.isArray(parsed.system)) {
// Find and remove existing billing/agent blocks from wherever
// the client placed them (typically at system[0])
const billingIdx = parsed.system.findIndex((b) => typeof b.text === "string" &&
b.text.includes("x-anthropic-billing-header"));
const agentIdx = parsed.system.findIndex((b) => typeof b.text === "string" && b.text.includes("Claude Agent SDK"));
const billingBlock = {
type: "text",
text: buildStableClaudeCodeBillingHeader(parsed.system[billingIdx]?.text),
};
// Remove in reverse index order so indices stay valid
const indicesToRemove = [billingIdx, agentIdx]
.filter((i) => i >= 0)
.sort((a, b) => b - a);
for (const idx of indicesToRemove) {
parsed.system.splice(idx, 1);
}
// Always append deterministic billing + agent blocks at the end
parsed.system = [...parsed.system, billingBlock, agentBlock];
}
}
else {
const billingBlock = {
type: "text",
text: buildStableClaudeCodeBillingHeader(),
};
parsed.system = [billingBlock, agentBlock];
}
const token = getToken();
const stableId = parsed.metadata?.user_id ?? token.substring(0, Math.min(20, token.length));
const identity = getOrCreateClaudeCodeIdentity(stableId, {
existingUserId: parsed.metadata?.user_id,
preferredSessionId: requestHeaders.get("x-claude-code-session-id") ?? undefined,
});
parsed.metadata = {
...parsed.metadata,
user_id: identity.metadataUserId,
};
requestHeaders.set("x-claude-code-session-id", identity.sessionId);
return JSON.stringify(parsed);
}
async function injectOtelHeaders(requestHeaders) {
try {
const { propagation: otelPropagation, context: otelContext } = await import("@opentelemetry/api");
const carrier = {};
otelPropagation.inject(otelContext.active(), carrier);
for (const [key, value] of Object.entries(carrier)) {
if (!requestHeaders.has(key)) {
requestHeaders.set(key, value);
}
}
}
catch {
// OTel not available — skip silently
}
}
function rewriteMcpPrefixedStreamingResponse(response) {
if (!response.body) {
return response;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const responseHeaders = new Headers(response.headers);
responseHeaders.delete("content-length");
let carry = "";
const stream = new ReadableStream({
async pull(controller) {
const { done, value } = await reader.read();
if (done) {
if (carry) {
controller.enqueue(encoder.encode(carry.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"')));
carry = "";
}
controller.close();
return;
}
const chunkText = decoder.decode(value, { stream: true });
const combined = carry + chunkText;
const partialMatch = combined.match(/"name"\s*:\s*"mcp_[^"]*$/);
let safeText;
if (partialMatch && partialMatch.index !== undefined) {
safeText = combined.slice(0, partialMatch.index);
carry = combined.slice(partialMatch.index);
}
else {
const lastQuote = combined.lastIndexOf('"');
const safeLen = lastQuote >= 0 ? lastQuote + 1 : combined.length;
safeText = combined.slice(0, safeLen);
carry = combined.slice(safeLen);
}
const replaced = safeText.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
if (replaced) {
controller.enqueue(encoder.encode(replaced));
}
},
async cancel(reason) {
await reader.cancel(reason);
},
});
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
}
async function executeOAuthFetch(input, init, getToken, includeOptionalBetas, enableMcpPrefix, skipBodyTransform) {
const requestUrl = resolveOAuthRequestUrl(input);
if (requestUrl &&
requestUrl.pathname === "/v1/messages" &&
!requestUrl.searchParams.has("beta")) {
requestUrl.searchParams.set("beta", "true");
}
const requestHeaders = mergeRequestHeaders(input, init);
applyOAuthHeaders(requestHeaders, getToken, includeOptionalBetas, skipBodyTransform);
logger.debug("[createOAuthFetch] Making OAuth request:", {
url: requestUrl?.toString() || input.toString(),
hasAuthorization: requestHeaders.has("authorization"),
authType: "Bearer",
anthropicBeta: requestHeaders.get("anthropic-beta"),
userAgent: requestHeaders.get("user-agent"),
});
const { sourceRequest, method, body: initialBody, } = await resolveOAuthRequestBody(input, init);
let body = initialBody;
if (body && typeof body === "string" && !skipBodyTransform) {
try {
body = transformOAuthJsonBody(body, requestHeaders, getToken, enableMcpPrefix);
}
catch {
// Ignore JSON parse errors — pass body through unchanged
}
}
requestHeaders.delete("content-length");
await injectOtelHeaders(requestHeaders);
const proxyFetch = createProxyFetch();
const response = await proxyFetch(requestUrl?.toString() ||
(input instanceof Request ? input.url : input.toString()), {
...init,
method,
body,
signal: init?.signal ?? sourceRequest?.signal,
headers: requestHeaders,
});
return enableMcpPrefix
? rewriteMcpPrefixedStreamingResponse(response)
: response;
}
// ---------------------------------------------------------------------------
// Main factory
// ---------------------------------------------------------------------------
/**
* Creates a custom fetch function for OAuth-authenticated requests.
* This wrapper applies all required transformations for OAuth mode:
* - Uses Authorization: Bearer header (NOT x-api-key)
* - Adds OAuth-required beta headers
* - Sets User-Agent to Claude CLI
* - Adds ?beta=true query parameter to /v1/messages
* - Injects billing header & agent block into system prompt
* - Injects Claude-Code-shaped user ID into metadata
* - Adds Stainless SDK headers for fingerprint matching
* - Disables thinking when tool_choice is forced
*
* Accepts a getter function instead of a static token so that refreshed
* tokens are picked up automatically on each request.
*
* @param getToken - Function returning the current OAuth access token
* @param includeOptionalBetas - Whether to include optional beta headers (default true)
* @param enableMcpPrefix - Whether to apply mcp_ prefix/strip logic to tool names (default false)
* @param skipBodyTransform - When true, skip ALL body modifications (billing header, user ID, tool prefix).
* Used for proxy passthrough where the request body must be forwarded as-is.
*/
export function createOAuthFetch(getToken, includeOptionalBetas = true, enableMcpPrefix = false, skipBodyTransform = false) {
return async (input, init) => executeOAuthFetch(input, init, getToken, includeOptionalBetas, enableMcpPrefix, skipBodyTransform);
}
//# sourceMappingURL=oauthFetch.js.map