mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
Support native Anthropic API keys (sk-ant-...) via `ccs api create --preset anthropic`. Profiles use ANTHROPIC_API_KEY env var without BASE_URL, letting Claude CLI authenticate natively with x-api-key header instead of proxy Bearer token. - Add anthropic preset to provider catalog - Branch profile-writer env structure (native vs proxy) - Add validateAnthropicKey() preflight with x-api-key header - Auto-detect api.anthropic.com URL in interactive flow - Fix hasApiKey/isConfigured to recognize ANTHROPIC_API_KEY - Update delegation-validator for native mode profiles - Add Droid target API key fallback (ANTHROPIC_API_KEY) Closes #688 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
5.9 KiB
TypeScript
242 lines
5.9 KiB
TypeScript
/**
|
|
* API Key Pre-flight Validator
|
|
*
|
|
* Quick validation of API keys before Claude CLI launch.
|
|
* Catches expired keys early with actionable error messages.
|
|
*/
|
|
|
|
import * as http from 'http';
|
|
import * as https from 'https';
|
|
import { URL } from 'url';
|
|
|
|
export interface ValidationResult {
|
|
valid: boolean;
|
|
error?: string;
|
|
suggestion?: string;
|
|
}
|
|
|
|
/** Default placeholders that indicate unconfigured keys */
|
|
const DEFAULT_PLACEHOLDERS = [
|
|
'YOUR_GLM_API_KEY_HERE',
|
|
'YOUR_KIMI_API_KEY_HERE',
|
|
'YOUR_MINIMAX_API_KEY_HERE',
|
|
'YOUR_API_KEY_HERE',
|
|
'YOUR-API-KEY-HERE',
|
|
'PLACEHOLDER',
|
|
'',
|
|
];
|
|
|
|
interface ProviderConfig {
|
|
name: string;
|
|
profile: string;
|
|
defaultBaseUrl: string;
|
|
path: string;
|
|
displayName: string;
|
|
dashboardUrl: string;
|
|
}
|
|
|
|
async function validateProviderKey(
|
|
apiKey: string,
|
|
config: ProviderConfig,
|
|
baseUrl?: string,
|
|
timeoutMs = 2000
|
|
): Promise<ValidationResult> {
|
|
if (process.env.CCS_SKIP_PREFLIGHT === '1') {
|
|
return { valid: true };
|
|
}
|
|
|
|
if (!apiKey || DEFAULT_PLACEHOLDERS.includes(apiKey.toUpperCase())) {
|
|
return {
|
|
valid: false,
|
|
error: 'API key not configured',
|
|
suggestion:
|
|
`Set ANTHROPIC_AUTH_TOKEN in ~/.ccs/${config.profile}.settings.json\n` +
|
|
`Or run: ccs config -> API Profiles -> ${config.name}`,
|
|
};
|
|
}
|
|
|
|
const targetBase = baseUrl || config.defaultBaseUrl;
|
|
let url: URL;
|
|
try {
|
|
url = new URL(config.path, targetBase);
|
|
} catch {
|
|
return { valid: true };
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
let resolved = false;
|
|
const safeResolve = (result: ValidationResult) => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
resolve(result);
|
|
};
|
|
|
|
const isHttps = url.protocol === 'https:';
|
|
const httpModule = isHttps ? https : http;
|
|
const defaultPort = isHttps ? 443 : 80;
|
|
|
|
const options: https.RequestOptions = {
|
|
hostname: url.hostname,
|
|
port: url.port || defaultPort,
|
|
path: url.pathname,
|
|
method: 'GET',
|
|
headers: {
|
|
Authorization: `Bearer ${apiKey}`,
|
|
'User-Agent': 'CCS-Preflight/1.0',
|
|
},
|
|
};
|
|
|
|
const req = httpModule.request(options, (res) => {
|
|
clearTimeout(timeoutId);
|
|
|
|
if (res.statusCode === 200) {
|
|
safeResolve({ valid: true });
|
|
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
|
safeResolve({
|
|
valid: false,
|
|
error: `API key rejected by ${config.displayName}`,
|
|
suggestion:
|
|
`Your key may have expired. To fix:\n` +
|
|
` 1. Go to ${config.dashboardUrl} and regenerate your API key\n` +
|
|
` 2. Update ~/.ccs/${config.profile}.settings.json with new key\n` +
|
|
` 3. Or run: ccs config -> API Profiles -> ${config.name}`,
|
|
});
|
|
} else {
|
|
if (process.env.CCS_DEBUG === '1') {
|
|
console.error(
|
|
`[CCS-Preflight] Unexpected status ${res.statusCode} from ${url.href} - fail-open`
|
|
);
|
|
}
|
|
safeResolve({ valid: true });
|
|
}
|
|
|
|
res.resume();
|
|
});
|
|
|
|
req.on('error', () => {
|
|
clearTimeout(timeoutId);
|
|
safeResolve({ valid: true });
|
|
});
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
req.destroy();
|
|
safeResolve({ valid: true });
|
|
}, timeoutMs);
|
|
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
export async function validateGlmKey(
|
|
apiKey: string,
|
|
baseUrl?: string,
|
|
timeoutMs?: number
|
|
): Promise<ValidationResult> {
|
|
return validateProviderKey(
|
|
apiKey,
|
|
{
|
|
name: 'GLM',
|
|
profile: 'glm',
|
|
defaultBaseUrl: 'https://api.z.ai',
|
|
path: '/api/anthropic/v1/models',
|
|
displayName: 'Z.AI',
|
|
dashboardUrl: 'Z.AI dashboard',
|
|
},
|
|
baseUrl,
|
|
timeoutMs
|
|
);
|
|
}
|
|
|
|
export async function validateMiniMaxKey(
|
|
apiKey: string,
|
|
baseUrl?: string,
|
|
timeoutMs?: number
|
|
): Promise<ValidationResult> {
|
|
return validateProviderKey(
|
|
apiKey,
|
|
{
|
|
name: 'MiniMax',
|
|
profile: 'mm',
|
|
defaultBaseUrl: 'https://api.minimax.io',
|
|
path: '/anthropic/v1/models',
|
|
displayName: 'MiniMax',
|
|
dashboardUrl: 'platform.minimax.io',
|
|
},
|
|
baseUrl,
|
|
timeoutMs
|
|
);
|
|
}
|
|
|
|
/** Validate Anthropic direct API key using x-api-key header (not Bearer) */
|
|
export async function validateAnthropicKey(
|
|
apiKey: string,
|
|
timeoutMs = 2000
|
|
): Promise<ValidationResult> {
|
|
if (process.env.CCS_SKIP_PREFLIGHT === '1') {
|
|
return { valid: true };
|
|
}
|
|
|
|
if (!apiKey || DEFAULT_PLACEHOLDERS.includes(apiKey.toUpperCase())) {
|
|
return {
|
|
valid: false,
|
|
error: 'Anthropic API key not configured',
|
|
suggestion: 'Set ANTHROPIC_API_KEY in your profile settings.json',
|
|
};
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
let resolved = false;
|
|
const safeResolve = (result: ValidationResult) => {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
resolve(result);
|
|
};
|
|
|
|
const options: https.RequestOptions = {
|
|
hostname: 'api.anthropic.com',
|
|
port: 443,
|
|
path: '/v1/models',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-api-key': apiKey,
|
|
'anthropic-version': '2023-06-01',
|
|
'User-Agent': 'CCS-Preflight/1.0',
|
|
},
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
clearTimeout(timeoutId);
|
|
|
|
if (res.statusCode === 200) {
|
|
safeResolve({ valid: true });
|
|
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
|
safeResolve({
|
|
valid: false,
|
|
error: 'Anthropic API key rejected',
|
|
suggestion:
|
|
'Your key may have expired. To fix:\n' +
|
|
' 1. Go to console.anthropic.com/settings/keys and regenerate your API key\n' +
|
|
' 2. Update ANTHROPIC_API_KEY in your profile settings.json\n' +
|
|
' 3. Or run: ccs api create --preset anthropic',
|
|
});
|
|
} else {
|
|
safeResolve({ valid: true });
|
|
}
|
|
|
|
res.resume();
|
|
});
|
|
|
|
req.on('error', () => {
|
|
clearTimeout(timeoutId);
|
|
safeResolve({ valid: true });
|
|
});
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
req.destroy();
|
|
safeResolve({ valid: true });
|
|
}, timeoutMs);
|
|
|
|
req.end();
|
|
});
|
|
}
|