chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* API Authentication Utilities
|
||||
* Validates API keys for workflow execution endpoints
|
||||
* Supports both Clerk (UI) and API Key (external) authentication
|
||||
*/
|
||||
|
||||
import { NextRequest } from 'next/server';
|
||||
import { getConvexClient, api } from '@/lib/convex/client';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
export interface ApiAuthResult {
|
||||
authenticated: boolean;
|
||||
userId?: string;
|
||||
error?: string;
|
||||
authType?: 'clerk' | 'api-key';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates authentication - checks Clerk first (for UI), then API key (for external)
|
||||
* This allows both authenticated UI users and API key users to access endpoints
|
||||
*/
|
||||
export async function validateApiKey(request: NextRequest): Promise<ApiAuthResult> {
|
||||
try {
|
||||
// First, check for Clerk authentication (UI users)
|
||||
const { userId } = await auth();
|
||||
if (userId) {
|
||||
return {
|
||||
authenticated: true,
|
||||
userId,
|
||||
authType: 'clerk'
|
||||
};
|
||||
}
|
||||
|
||||
// If no Clerk auth, check for API key (external API users)
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
|
||||
if (!authHeader) {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: 'Authentication required. Please sign in or provide an API key with format: "Bearer YOUR_API_KEY"'
|
||||
};
|
||||
}
|
||||
|
||||
// Parse Bearer token
|
||||
const parts = authHeader.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: 'Invalid Authorization header format. Expected: "Bearer YOUR_API_KEY"'
|
||||
};
|
||||
}
|
||||
|
||||
const apiKey = parts[1];
|
||||
|
||||
if (!apiKey || apiKey.length < 10) {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: 'Invalid API key format'
|
||||
};
|
||||
}
|
||||
|
||||
// Verify key with Convex
|
||||
const convex = getConvexClient();
|
||||
const result = await convex.mutation(api.apiKeys.verify, { key: apiKey });
|
||||
|
||||
if (!result.valid) {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: result.error || 'Invalid API key'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
userId: result.userId,
|
||||
authType: 'api-key'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('API key validation error:', error);
|
||||
return {
|
||||
authenticated: false,
|
||||
error: 'API key validation failed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized unauthorized response
|
||||
*/
|
||||
export function createUnauthorizedResponse(error: string) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Unauthorized',
|
||||
message: error,
|
||||
hint: 'Generate an API key from Settings and include it in the Authorization header'
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'WWW-Authenticate': 'Bearer realm="API Key Required"'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Server-side API configuration utilities
|
||||
* Use this for getting API keys in API routes and server components
|
||||
*/
|
||||
|
||||
export interface APIKeys {
|
||||
anthropic?: string;
|
||||
groq?: string;
|
||||
openai?: string;
|
||||
arcade?: string;
|
||||
e2b?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API keys from environment variables (server-side only)
|
||||
* Returns available keys even if some are missing
|
||||
*/
|
||||
export function getServerAPIKeys(): APIKeys {
|
||||
const anthropic = process.env.ANTHROPIC_API_KEY;
|
||||
const groq = process.env.GROQ_API_KEY;
|
||||
const openai = process.env.OPENAI_API_KEY;
|
||||
const arcade = process.env.ARCADE_API_KEY;
|
||||
const e2b = process.env.E2B_API_KEY;
|
||||
|
||||
return {
|
||||
anthropic,
|
||||
groq,
|
||||
openai,
|
||||
arcade,
|
||||
e2b,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if required API keys are configured
|
||||
*/
|
||||
export function hasServerAPIKeys(): boolean {
|
||||
const hasLLMKey = !!(process.env.ANTHROPIC_API_KEY || process.env.GROQ_API_KEY || process.env.OPENAI_API_KEY);
|
||||
return hasLLMKey;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* LLM API Key Management
|
||||
*
|
||||
* Provides API keys for LLM providers with fallback logic:
|
||||
* 1. Check user-specific keys from database
|
||||
* 2. Fall back to environment variables if no user key exists
|
||||
*/
|
||||
|
||||
import { ConvexClient } from "convex/browser";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
|
||||
// Initialize Convex client for server-side use
|
||||
const getConvexClient = () => {
|
||||
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
|
||||
if (!convexUrl) {
|
||||
throw new Error("NEXT_PUBLIC_CONVEX_URL is not configured");
|
||||
}
|
||||
return new ConvexClient(convexUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the API key for a specific LLM provider
|
||||
* Checks user keys first, then falls back to environment variables
|
||||
*
|
||||
* @param provider - The LLM provider ('anthropic', 'openai', 'groq')
|
||||
* @param userId - Optional user ID to check for user-specific keys
|
||||
* @returns The API key or null if not found
|
||||
*/
|
||||
export async function getLLMApiKey(
|
||||
provider: 'anthropic' | 'openai' | 'groq',
|
||||
userId?: string
|
||||
): Promise<string | null> {
|
||||
// First, try to get user-specific key if userId is provided
|
||||
if (userId) {
|
||||
try {
|
||||
const client = getConvexClient();
|
||||
const userKey = await client.query(api.userLLMKeys.getActiveKey, {
|
||||
userId,
|
||||
provider,
|
||||
});
|
||||
|
||||
if (userKey?.apiKey) {
|
||||
// Update usage stats
|
||||
await client.mutation(api.userLLMKeys.updateKeyUsage, {
|
||||
userId,
|
||||
provider,
|
||||
}).catch(console.error); // Don't fail if usage update fails
|
||||
|
||||
return userKey.apiKey;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get user key for ${provider}:`, error);
|
||||
// Continue to environment variable fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to environment variables
|
||||
const envKeyMap = {
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
openai: 'OPENAI_API_KEY',
|
||||
groq: 'GROQ_API_KEY',
|
||||
};
|
||||
|
||||
const envKey = envKeyMap[provider];
|
||||
const apiKey = process.env[envKey];
|
||||
|
||||
if (apiKey) {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider has an API key configured (either user or env)
|
||||
*/
|
||||
export async function isProviderConfigured(
|
||||
provider: 'anthropic' | 'openai' | 'groq',
|
||||
userId?: string
|
||||
): Promise<boolean> {
|
||||
const apiKey = await getLLMApiKey(provider, userId);
|
||||
return !!apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configured providers for a user
|
||||
*/
|
||||
export async function getConfiguredProviders(userId?: string): Promise<string[]> {
|
||||
const providers: ('anthropic' | 'openai' | 'groq')[] = ['anthropic', 'openai', 'groq'];
|
||||
const configured: string[] = [];
|
||||
|
||||
for (const provider of providers) {
|
||||
if (await isProviderConfigured(provider, userId)) {
|
||||
configured.push(provider);
|
||||
}
|
||||
}
|
||||
|
||||
return configured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize LLM client with appropriate API key
|
||||
* This is a helper function that can be used by the execute routes
|
||||
*/
|
||||
export async function initializeLLMClient(
|
||||
provider: 'anthropic' | 'openai' | 'groq',
|
||||
userId?: string
|
||||
): Promise<{ apiKey: string; provider: string }> {
|
||||
const apiKey = await getLLMApiKey(provider, userId);
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
`No API key found for ${provider}. Please configure your API key in Settings or set the ${
|
||||
provider === 'anthropic' ? 'ANTHROPIC_API_KEY' :
|
||||
provider === 'openai' ? 'OPENAI_API_KEY' :
|
||||
'GROQ_API_KEY'
|
||||
} environment variable.`
|
||||
);
|
||||
}
|
||||
|
||||
return { apiKey, provider };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration status for all providers
|
||||
* Useful for the settings panel to show which providers are configured
|
||||
*/
|
||||
export async function getProvidersStatus(userId?: string): Promise<{
|
||||
anthropic: { configured: boolean; source: 'user' | 'env' | null };
|
||||
openai: { configured: boolean; source: 'user' | 'env' | null };
|
||||
groq: { configured: boolean; source: 'user' | 'env' | null };
|
||||
}> {
|
||||
const status: any = {};
|
||||
|
||||
for (const provider of ['anthropic', 'openai', 'groq'] as const) {
|
||||
// Check user key first
|
||||
if (userId) {
|
||||
try {
|
||||
const client = getConvexClient();
|
||||
const userKey = await client.query(api.userLLMKeys.getActiveKey, {
|
||||
userId,
|
||||
provider,
|
||||
});
|
||||
|
||||
if (userKey) {
|
||||
status[provider] = { configured: true, source: 'user' };
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue to env check
|
||||
}
|
||||
}
|
||||
|
||||
// Check environment variable
|
||||
const envKeyMap = {
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
openai: 'OPENAI_API_KEY',
|
||||
groq: 'GROQ_API_KEY',
|
||||
};
|
||||
|
||||
const envKey = envKeyMap[provider];
|
||||
if (process.env[envKey]) {
|
||||
status[provider] = { configured: true, source: 'env' };
|
||||
} else {
|
||||
status[provider] = { configured: false, source: null };
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Model validation and configuration for LLM providers
|
||||
*/
|
||||
|
||||
export type Provider = 'openai' | 'anthropic' | 'groq';
|
||||
|
||||
export interface ModelConfig {
|
||||
provider: Provider;
|
||||
modelName: string;
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported models by provider (MCP-compatible only)
|
||||
* These models support MCP through their respective APIs:
|
||||
* - OpenAI: Responses API
|
||||
* - Anthropic: Messages API with MCP beta
|
||||
* - Groq: Responses API (OpenAI-compatible)
|
||||
*/
|
||||
export const SUPPORTED_MODELS = {
|
||||
openai: [
|
||||
// OpenAI models that support Responses API
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
],
|
||||
anthropic: [
|
||||
// Claude 4 models that support Messages API with MCP
|
||||
'claude-sonnet-4-5-20250929',
|
||||
'claude-haiku-4-5', // Latest Haiku 4.5
|
||||
],
|
||||
groq: [
|
||||
// Only Groq models that support Responses API (per Groq docs)
|
||||
'gpt-oss-120b',
|
||||
],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Default models for each provider
|
||||
*/
|
||||
export const DEFAULT_MODELS = {
|
||||
openai: 'gpt-4o',
|
||||
anthropic: 'claude-sonnet-4-5-20250929', // Claude 4.5 Sonnet
|
||||
groq: 'gpt-oss-120b', // Using Responses API model for better MCP support
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Parse a model string into provider and model name
|
||||
* Supports formats:
|
||||
* - "provider/model-name" (e.g., "openai/gpt-4o")
|
||||
* - "model-name" (defaults to openai provider)
|
||||
*/
|
||||
export function parseModelString(modelString?: string): { provider: Provider; modelName: string } {
|
||||
if (!modelString) {
|
||||
return { provider: 'openai', modelName: DEFAULT_MODELS.openai };
|
||||
}
|
||||
|
||||
if (modelString.includes('/')) {
|
||||
const [provider, modelName] = modelString.split('/', 2) as [string, string];
|
||||
|
||||
// Validate provider
|
||||
if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'groq') {
|
||||
// Default to openai if provider is unknown
|
||||
return { provider: 'openai', modelName: DEFAULT_MODELS.openai };
|
||||
}
|
||||
|
||||
return { provider, modelName };
|
||||
}
|
||||
|
||||
// No provider prefix, default to openai
|
||||
return { provider: 'openai', modelName: modelString };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a model configuration
|
||||
*/
|
||||
export function validateModel(modelString?: string): ModelConfig {
|
||||
const { provider, modelName } = parseModelString(modelString);
|
||||
|
||||
// Check if model is in supported list
|
||||
const supportedModels = SUPPORTED_MODELS[provider];
|
||||
const isValid = (supportedModels as readonly string[]).includes(modelName);
|
||||
|
||||
if (!isValid) {
|
||||
return {
|
||||
provider,
|
||||
modelName,
|
||||
isValid: false,
|
||||
error: `Model '${modelName}' is not supported for provider '${provider}'. Supported models: ${supportedModels.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
provider,
|
||||
modelName,
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default model for a provider
|
||||
*/
|
||||
export function getDefaultModel(provider: Provider): string {
|
||||
return DEFAULT_MODELS[provider];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is supported
|
||||
*/
|
||||
export function isSupportedProvider(provider: string): provider is Provider {
|
||||
return provider === 'openai' || provider === 'anthropic' || provider === 'groq';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model string with provider prefix
|
||||
*/
|
||||
export function getModelString(provider: Provider, modelName: string): string {
|
||||
return `${provider}/${modelName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure model string is compatible with OpenAI Responses API
|
||||
* (only OpenAI models work with the Responses API)
|
||||
*/
|
||||
export function ensureOpenAIModel(modelString?: string): string {
|
||||
const { provider, modelName } = parseModelString(modelString);
|
||||
|
||||
// If it's already an OpenAI model, return it
|
||||
if (provider === 'openai') {
|
||||
// Validate it's a supported OpenAI model
|
||||
const validation = validateModel(modelString);
|
||||
if (validation.isValid) {
|
||||
return modelName;
|
||||
}
|
||||
// Fall back to default if invalid
|
||||
return DEFAULT_MODELS.openai;
|
||||
}
|
||||
|
||||
// For non-OpenAI providers, return default OpenAI model
|
||||
return DEFAULT_MODELS.openai;
|
||||
}
|
||||
Reference in New Issue
Block a user