add virtual api key validation in verifySession

This commit is contained in:
miloschwartz
2026-08-12 10:38:08 -04:00
parent ac3402a8b3
commit 49020fa6ea
29 changed files with 852 additions and 197 deletions
+12
View File
@@ -0,0 +1,12 @@
export const AI_CAPABILITIES = [
"openai_chat",
"openai_responses",
"anthropic_messages",
"gemini_generate_content",
"bedrock_model_invoke",
"google_generate_content",
"google_raw_predict",
"bedrock_converse"
] as const;
export type AiCapability = (typeof AI_CAPABILITIES)[number];
+107
View File
@@ -0,0 +1,107 @@
import { AI_CAPABILITIES, type AiCapability } from "@app/lib/aiCapabilities";
export type AiProviderType =
| "openai"
| "anthropic"
| "googleGemini"
| "vertexAi"
| "bedrock"
| "microsoftFoundry"
| "openRouter"
| "vercelAiGateway"
| "custom";
export const AI_PROVIDER_AUTH_TYPES = [
"bearer",
"x-api-key",
"x-goog-api-key",
"hec",
"cf-aig-authorization",
"none",
"passthrough"
] as const;
export type AiProviderAuthType = (typeof AI_PROVIDER_AUTH_TYPES)[number];
export type AiBudgetUnit = "usd" | "tokens";
export type AiProviderRoutingMode = "url" | "target";
type AiProviderDefaults = {
upstreamUrl: string | null;
authType: AiProviderAuthType;
capabilities: readonly AiCapability[];
};
export const AI_PROVIDER_DEFAULTS: Record<
Exclude<AiProviderType, "custom">,
AiProviderDefaults
> = {
openai: {
upstreamUrl: "https://api.openai.com/v1",
authType: "bearer",
capabilities: ["openai_chat", "openai_responses"]
},
anthropic: {
upstreamUrl: "https://api.anthropic.com",
authType: "x-api-key",
capabilities: ["anthropic_messages"]
},
googleGemini: {
upstreamUrl: "https://generativelanguage.googleapis.com",
authType: "x-goog-api-key",
capabilities: ["gemini_generate_content"]
},
vertexAi: {
upstreamUrl: null,
authType: "bearer",
capabilities: ["google_generate_content", "google_raw_predict"]
},
bedrock: {
upstreamUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
authType: "bearer",
capabilities: ["bedrock_converse"]
},
microsoftFoundry: {
upstreamUrl: null,
authType: "bearer",
capabilities: ["openai_chat", "openai_responses", "anthropic_messages"]
},
openRouter: {
upstreamUrl: "https://openrouter.ai/api/v1",
authType: "bearer",
capabilities: ["openai_chat"]
},
vercelAiGateway: {
upstreamUrl: "https://ai-gateway.vercel.sh/v1",
authType: "bearer",
capabilities: ["openai_chat", "openai_responses"]
}
};
export function authTypeRequiresApiKey(authType: AiProviderAuthType): boolean {
return authType !== "none" && authType !== "passthrough";
}
export function providerRequiresUpstreamUrl(
type: AiProviderType,
routingMode: AiProviderRoutingMode = "url"
): boolean {
if (routingMode === "target") {
return false;
}
if (type === "custom") {
return true;
}
return AI_PROVIDER_DEFAULTS[type].upstreamUrl === null;
}
export function defaultsForProviderType(
type: AiProviderType
): readonly AiCapability[] {
if (type === "custom") {
return [];
}
return AI_PROVIDER_DEFAULTS[type].capabilities;
}
export { AI_CAPABILITIES };
export type { AiCapability };
+3 -2
View File
@@ -1,14 +1,15 @@
import { z } from "zod";
import {
AI_CAPABILITIES,
AI_PROVIDER_AUTH_TYPES,
AI_PROVIDER_DEFAULTS,
authTypeRequiresApiKey,
defaultsForProviderType,
providerRequiresUpstreamUrl,
type AiCapability,
type AiProviderAuthType,
type AiProviderType
} from "@server/lib/aiProviderDefaults";
import { AI_CAPABILITIES, type AiCapability } from "@server/lib/aiCapabilities";
} from "@app/lib/aiProviderDefaults";
type TranslateFn = (key: string) => string;
+63
View File
@@ -0,0 +1,63 @@
export const VIRTUAL_API_KEY_PREFIX = "vk-";
const VIRTUAL_API_KEY_AUTH_HEADER_NAMES = [
"authorization",
"x-api-key",
"x-goog-api-key",
"cf-aig-authorization"
] as const;
export function formatVirtualApiKeyCredential(
virtualApiKeyId: string,
secret: string
): string {
return `${VIRTUAL_API_KEY_PREFIX}${virtualApiKeyId}.${secret}`;
}
export function formatVirtualApiKeyPreview(
virtualApiKeyId: string,
lastChars: string
): string {
return `${VIRTUAL_API_KEY_PREFIX}${virtualApiKeyId}••••${lastChars}`;
}
export function looksLikeVirtualApiKeyCredential(value: string): boolean {
const trimmed = value.trim();
if (!trimmed.startsWith(VIRTUAL_API_KEY_PREFIX)) {
return false;
}
const withoutPrefix = trimmed.slice(VIRTUAL_API_KEY_PREFIX.length);
const dot = withoutPrefix.indexOf(".");
return dot > 0 && dot < withoutPrefix.length - 1;
}
function headerValueCarriesVirtualApiKey(raw: string): boolean {
const trimmed = raw.trim();
const bearerMatch = trimmed.match(/^(?:Bearer|Splunk)\s+(.+)$/i);
if (bearerMatch) {
return looksLikeVirtualApiKeyCredential(bearerMatch[1]);
}
return looksLikeVirtualApiKeyCredential(trimmed);
}
/**
* Remove client headers that carry a Pangolin virtual API key so they are
* never forwarded to upstream providers (including passthrough auth).
*/
export function stripVirtualApiKeyAuthHeaders(
headers: Record<string, string>
): void {
for (const key of Object.keys(headers)) {
const lower = key.toLowerCase();
if (
!(VIRTUAL_API_KEY_AUTH_HEADER_NAMES as readonly string[]).includes(
lower
)
) {
continue;
}
if (headerValueCarriesVirtualApiKey(headers[key])) {
delete headers[key];
}
}
}