mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-13 16:00:02 +02:00
add virtual api key validation in verifySession
This commit is contained in:
@@ -1,17 +1,7 @@
|
||||
import type { Request } from "express";
|
||||
import { AI_CAPABILITIES, type AiCapability } from "@app/lib/aiCapabilities";
|
||||
|
||||
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];
|
||||
export { AI_CAPABILITIES, type AiCapability };
|
||||
|
||||
export type AiCapabilityRoute = {
|
||||
method: "POST";
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createHash } from "crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export const AI_GATEWAY_TRUST_HEADER = "X-Pangolin-Ai-Gateway-Auth";
|
||||
|
||||
/**
|
||||
* Derive a Traefik-injected trust token from the server secret.
|
||||
* Traefik overwrites this header on inference routes so the AI gateway can
|
||||
* trust Badger-injected Remote-* identity without re-validating credentials.
|
||||
*/
|
||||
export function deriveAiGatewayTrustToken(secret: string): string {
|
||||
return createHash("sha256")
|
||||
.update(`ai-gateway-trust:${secret}`)
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
export function getAiGatewayTrustToken(): string {
|
||||
const secret = config.getRawConfig().server.secret;
|
||||
if (!secret) {
|
||||
throw new Error("Server secret is required for AI gateway trust token");
|
||||
}
|
||||
return deriveAiGatewayTrustToken(secret);
|
||||
}
|
||||
|
||||
export function isAiGatewayTrustHeaderValid(
|
||||
headers: Record<string, string | string[] | undefined> | undefined,
|
||||
expectedToken?: string
|
||||
): boolean {
|
||||
if (!headers) {
|
||||
return false;
|
||||
}
|
||||
const expected = expectedToken ?? getAiGatewayTrustToken();
|
||||
const raw =
|
||||
headers[AI_GATEWAY_TRUST_HEADER] ??
|
||||
headers[AI_GATEWAY_TRUST_HEADER.toLowerCase()];
|
||||
const value = Array.isArray(raw) ? raw[0] : raw;
|
||||
return typeof value === "string" && value === expected;
|
||||
}
|
||||
@@ -3,82 +3,29 @@ import {
|
||||
parseCapabilities,
|
||||
type AiCapability
|
||||
} from "@server/lib/aiCapabilities";
|
||||
import { stripVirtualApiKeyAuthHeaders } from "@app/lib/virtualApiKeyFormat";
|
||||
import {
|
||||
AI_PROVIDER_AUTH_TYPES,
|
||||
AI_PROVIDER_DEFAULTS,
|
||||
authTypeRequiresApiKey,
|
||||
defaultsForProviderType,
|
||||
providerRequiresUpstreamUrl,
|
||||
type AiBudgetUnit,
|
||||
type AiProviderAuthType,
|
||||
type AiProviderRoutingMode,
|
||||
type AiProviderType
|
||||
} from "@app/lib/aiProviderDefaults";
|
||||
|
||||
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 {
|
||||
AI_PROVIDER_AUTH_TYPES,
|
||||
AI_PROVIDER_DEFAULTS,
|
||||
authTypeRequiresApiKey,
|
||||
defaultsForProviderType,
|
||||
providerRequiresUpstreamUrl,
|
||||
type AiBudgetUnit,
|
||||
type AiProviderAuthType,
|
||||
type AiProviderRoutingMode,
|
||||
type AiProviderType
|
||||
};
|
||||
|
||||
const CONFLICTING_AUTH_HEADERS = [
|
||||
@@ -88,23 +35,6 @@ const CONFLICTING_AUTH_HEADERS = [
|
||||
"cf-aig-authorization"
|
||||
] as const;
|
||||
|
||||
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 resolveAiProviderCreateFields(input: {
|
||||
type: AiProviderType;
|
||||
upstreamUrl?: string | null;
|
||||
@@ -191,15 +121,18 @@ export function applyAiProviderCustomHeaders(
|
||||
|
||||
/**
|
||||
* Apply provider auth to upstream headers.
|
||||
* - Injected modes: strip client auth headers, then set the provider key.
|
||||
* - none: strip client auth headers, send no auth.
|
||||
* - passthrough: leave client auth headers as-is.
|
||||
* - Always strips Pangolin virtual API key credentials from client auth headers.
|
||||
* - Injected modes: strip conflicting client auth headers, then set the provider key.
|
||||
* - none: strip conflicting client auth headers, send no auth.
|
||||
* - passthrough: leave remaining client auth headers as-is (after VAK strip).
|
||||
*/
|
||||
export function applyAiProviderAuthHeaders(
|
||||
headers: Record<string, string>,
|
||||
authType: AiProviderAuthType,
|
||||
apiKey: string | null
|
||||
): void {
|
||||
stripVirtualApiKeyAuthHeaders(headers);
|
||||
|
||||
if (authType === "passthrough") {
|
||||
return;
|
||||
}
|
||||
@@ -251,12 +184,3 @@ export function resolveCapabilitiesForCreate(input: {
|
||||
}
|
||||
return [...AI_PROVIDER_DEFAULTS[input.type].capabilities];
|
||||
}
|
||||
|
||||
export function defaultsForProviderType(
|
||||
type: AiProviderType
|
||||
): readonly AiCapability[] {
|
||||
if (type === "custom") {
|
||||
return [];
|
||||
}
|
||||
return AI_PROVIDER_DEFAULTS[type].capabilities;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ import { resources, sites, Target, targets } from "@server/db";
|
||||
import createPathRewriteMiddleware from "./middleware";
|
||||
import { sanitize, encodePath, validatePathRewriteConfig } from "./utils";
|
||||
import regionalCache from "@server/lib/cache";
|
||||
import {
|
||||
AI_GATEWAY_TRUST_HEADER,
|
||||
getAiGatewayTrustToken
|
||||
} from "@server/lib/aiGatewayTrust";
|
||||
|
||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||
const badgerMiddlewareName = "badger";
|
||||
@@ -808,7 +812,8 @@ export async function getTraefikConfig(
|
||||
headers: {
|
||||
customRequestHeaders: {
|
||||
...(aiGatewayHost ? { Host: aiGatewayHost } : {}),
|
||||
"p-host": fullDomain
|
||||
"p-host": fullDomain,
|
||||
[AI_GATEWAY_TRUST_HEADER]: getAiGatewayTrustToken()
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -911,7 +916,8 @@ export async function getTraefikConfig(
|
||||
headers: {
|
||||
customRequestHeaders: {
|
||||
...(aiGatewayHost ? { Host: aiGatewayHost } : {}),
|
||||
"p-host": fullDomain
|
||||
"p-host": fullDomain,
|
||||
[AI_GATEWAY_TRUST_HEADER]: getAiGatewayTrustToken()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,14 @@ import config from "@server/lib/config";
|
||||
import { decrypt, encrypt } from "@server/lib/crypto";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
|
||||
export {
|
||||
VIRTUAL_API_KEY_PREFIX,
|
||||
formatVirtualApiKeyCredential,
|
||||
formatVirtualApiKeyPreview,
|
||||
looksLikeVirtualApiKeyCredential,
|
||||
stripVirtualApiKeyAuthHeaders
|
||||
} from "@app/lib/virtualApiKeyFormat";
|
||||
|
||||
export type MintedVirtualApiKeySecret = {
|
||||
virtualApiKeyId: string;
|
||||
secret: string;
|
||||
|
||||
Reference in New Issue
Block a user