refactor: replace HTTP credential checks with Claude Agent SDK query

Replaces validateApiKey and validateOAuthToken (direct fetch calls) with
a single SDK-based query using claude-haiku-4-5-20251001. Uses
SDKAssistantMessageError types for structured error classification and
returns human-readable error messages for each failure case.
This commit is contained in:
ezl-keygraph
2026-02-20 17:00:31 +05:30
parent 7ecf5abb35
commit e8e830c9f8
+78 -205
View File
@@ -14,21 +14,17 @@
* Checks run sequentially, cheapest first: * Checks run sequentially, cheapest first:
* 1. Repository path exists and contains .git * 1. Repository path exists and contains .git
* 2. Config file parses and validates (if provided) * 2. Config file parses and validates (if provided)
* 3. Credentials validate (API key, OAuth token, or router mode) * 3. Credentials validate via Claude Agent SDK query (API key, OAuth, or router mode)
*/ */
import fs from 'fs/promises'; import fs from 'fs/promises';
import { PentestError } from './error-handling.js'; import { query, type SDKAssistantMessageError } from '@anthropic-ai/claude-agent-sdk';
import { PentestError, isRetryableError } from './error-handling.js';
import { ErrorCode } from '../types/errors.js'; import { ErrorCode } from '../types/errors.js';
import { type Result, ok, err } from '../types/result.js'; import { type Result, ok, err } from '../types/result.js';
import { parseConfig } from '../config-parser.js'; import { parseConfig } from '../config-parser.js';
import type { ActivityLogger } from '../types/activity-logger.js'; import type { ActivityLogger } from '../types/activity-logger.js';
const VALIDATION_MODEL = 'claude-haiku-3-5-20241022';
const ANTHROPIC_MESSAGES_URL = 'https://api.anthropic.com/v1/messages';
const ANTHROPIC_OAUTH_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
const FETCH_TIMEOUT_MS = 30_000;
// === Repository Validation === // === Repository Validation ===
async function validateRepo( async function validateRepo(
@@ -124,187 +120,41 @@ async function validateConfig(
// === Credential Validation === // === Credential Validation ===
/** /** Map SDK error type to a human-readable preflight PentestError. */
* Validate a direct Anthropic API key via minimal Messages API call. function classifySdkError(
* Costs ~$0.000025 (1 input token + 1 output token on Haiku). sdkError: SDKAssistantMessageError,
*/ authType: string
async function validateApiKey( ): Result<void, PentestError> {
apiKey: string, switch (sdkError) {
logger: ActivityLogger case 'authentication_failed':
): Promise<Result<void, PentestError>> { return err(new PentestError(
logger.info('Validating Anthropic API key...'); `Invalid ${authType}. Check your credentials in .env and try again.`,
'config', false, { authType, sdkError }, ErrorCode.AUTH_FAILED
try { ));
const response = await fetch(ANTHROPIC_MESSAGES_URL, { case 'billing_error':
method: 'POST', return err(new PentestError(
headers: { `Anthropic account has a billing issue. Add credits or check your billing dashboard.`,
'Content-Type': 'application/json', 'billing', true, { authType, sdkError }, ErrorCode.BILLING_ERROR
'x-api-key': apiKey, ));
'anthropic-version': '2023-06-01', case 'rate_limit':
}, return err(new PentestError(
body: JSON.stringify({ `Anthropic rate limit or spending cap reached. Wait a few minutes and try again.`,
model: VALIDATION_MODEL, 'billing', true, { authType, sdkError }, ErrorCode.BILLING_ERROR
max_tokens: 1, ));
messages: [{ role: 'user', content: 'hi' }], case 'server_error':
}), return err(new PentestError(
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), `Anthropic API is temporarily unavailable. Try again shortly.`,
}); 'network', true, { authType, sdkError }
));
if (response.ok) { default:
logger.info('API key OK'); return err(new PentestError(
return ok(undefined); `${authType} validation failed unexpectedly. Check your credentials in .env.`,
} 'config', false, { authType, sdkError }, ErrorCode.AUTH_FAILED
));
let errorBody: string;
try {
errorBody = await response.text();
} catch {
errorBody = '';
}
if (response.status === 401) {
return err(
new PentestError(
`API authentication failed: invalid x-api-key`,
'config',
false,
{ status: response.status },
ErrorCode.AUTH_FAILED
)
);
}
if (response.status === 402 || response.status === 403) {
return err(
new PentestError(
`Anthropic billing error (HTTP ${response.status}): ${errorBody.slice(0, 200)}`,
'billing',
true,
{ status: response.status },
ErrorCode.BILLING_ERROR
)
);
}
if (response.status === 429) {
return err(
new PentestError(
`Spending cap or rate limit reached (HTTP 429)`,
'billing',
true,
{ status: response.status },
ErrorCode.BILLING_ERROR
)
);
}
// Other status codes (5xx, etc) - transient
return err(
new PentestError(
`Anthropic API error (HTTP ${response.status}): ${errorBody.slice(0, 200)}`,
'network',
true,
{ status: response.status }
)
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return err(
new PentestError(
`Failed to reach Anthropic API: ${message}`,
'network',
true,
{ originalError: message }
)
);
} }
} }
/** /** Validate credentials via a minimal Claude Agent SDK query. */
* Validate an OAuth token via the Anthropic usage endpoint.
* Confirms the token is valid and checks quota availability.
*/
async function validateOAuthToken(
token: string,
logger: ActivityLogger
): Promise<Result<void, PentestError>> {
logger.info('Validating OAuth token...');
try {
const response = await fetch(ANTHROPIC_OAUTH_USAGE_URL, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});
if (response.ok) {
logger.info('OAuth token OK');
return ok(undefined);
}
let errorBody: string;
try {
errorBody = await response.text();
} catch {
errorBody = '';
}
if (response.status === 401) {
return err(
new PentestError(
`OAuth token is invalid or expired`,
'config',
false,
{ status: response.status },
ErrorCode.AUTH_FAILED
)
);
}
if (response.status === 403 || response.status === 429) {
return err(
new PentestError(
`OAuth billing/quota error (HTTP ${response.status}): ${errorBody.slice(0, 200)}`,
'billing',
true,
{ status: response.status },
ErrorCode.BILLING_ERROR
)
);
}
return err(
new PentestError(
`OAuth validation error (HTTP ${response.status}): ${errorBody.slice(0, 200)}`,
'network',
true,
{ status: response.status }
)
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return err(
new PentestError(
`Failed to reach Anthropic OAuth endpoint: ${message}`,
'network',
true,
{ originalError: message }
)
);
}
}
/**
* Validate credentials based on detected auth mode.
*
* Auth modes (mutually exclusive):
* - Router mode (ANTHROPIC_BASE_URL set): skip validation, log warning
* - OAuth (CLAUDE_CODE_OAUTH_TOKEN set): validate via /api/oauth/usage
* - API key (ANTHROPIC_API_KEY set): validate via Messages API
* - None: error
*/
async function validateCredentials( async function validateCredentials(
logger: ActivityLogger logger: ActivityLogger
): Promise<Result<void, PentestError>> { ): Promise<Result<void, PentestError>> {
@@ -314,28 +164,51 @@ async function validateCredentials(
return ok(undefined); return ok(undefined);
} }
// 2. OAuth token // 2. Check that at least one credential is present
const oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; if (!process.env.ANTHROPIC_API_KEY && !process.env.CLAUDE_CODE_OAUTH_TOKEN) {
if (oauthToken) { return err(
return validateOAuthToken(oauthToken, logger); new PentestError(
'No API credentials found. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env',
'config',
false,
{},
ErrorCode.AUTH_FAILED
)
);
} }
// 3. Direct API key // 3. Validate via SDK query
const apiKey = process.env.ANTHROPIC_API_KEY; const authType = process.env.CLAUDE_CODE_OAUTH_TOKEN ? 'OAuth token' : 'API key';
if (apiKey) { logger.info(`Validating ${authType} via SDK...`);
return validateApiKey(apiKey, logger);
}
// 4. No credentials try {
return err( for await (const message of query({ prompt: 'hi', options: { model: 'claude-haiku-4-5-20251001', maxTurns: 1 } })) {
new PentestError( if (message.type === 'assistant' && message.error) {
'No API credentials found. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env', return classifySdkError(message.error, authType);
'config', }
false, if (message.type === 'result') {
{}, break;
ErrorCode.AUTH_FAILED }
) }
);
logger.info(`${authType} OK`);
return ok(undefined);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const retryable = isRetryableError(error instanceof Error ? error : new Error(message));
return err(
new PentestError(
retryable
? `Failed to reach Anthropic API. Check your network connection.`
: `${authType} validation failed: ${message}`,
retryable ? 'network' : 'config',
retryable,
{ authType },
retryable ? undefined : ErrorCode.AUTH_FAILED
)
);
}
} }
// === Preflight Orchestrator === // === Preflight Orchestrator ===
@@ -368,7 +241,7 @@ export async function runPreflightChecks(
} }
} }
// 3. Credential check (cheap — 1 token or single GET) // 3. Credential check (cheap — 1 SDK round-trip)
const credResult = await validateCredentials(logger); const credResult = await validateCredentials(logger);
if (!credResult.ok) { if (!credResult.ok) {
return credResult; return credResult;