chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:47 +08:00
commit 7653f56fed
1422 changed files with 359026 additions and 0 deletions
+105
View File
@@ -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"'
}
}
);
}
+40
View File
@@ -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;
}
+170
View File
@@ -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;
}
+141
View File
@@ -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;
}