redesign our approach for api keys to not use claude setup-token

This commit is contained in:
Cody Seibert
2025-12-15 14:24:18 -05:00
parent 07ca7fccb8
commit 54b977ee1b
27 changed files with 1564 additions and 1230 deletions

View File

@@ -151,7 +151,7 @@ export async function getClaudeStatus() {
if (auth.hasEnvOAuthToken) {
auth.authenticated = true;
auth.oauthTokenValid = true;
auth.method = "oauth_token_env"; // OAuth token from CLAUDE_CODE_OAUTH_TOKEN env var
auth.method = "oauth_token_env";
} else if (auth.hasEnvApiKey) {
auth.authenticated = true;
auth.apiKeyValid = true;

View File

@@ -7,8 +7,10 @@ import { createClaudeStatusHandler } from "./routes/claude-status.js";
import { createInstallClaudeHandler } from "./routes/install-claude.js";
import { createAuthClaudeHandler } from "./routes/auth-claude.js";
import { createStoreApiKeyHandler } from "./routes/store-api-key.js";
import { createDeleteApiKeyHandler } from "./routes/delete-api-key.js";
import { createApiKeysHandler } from "./routes/api-keys.js";
import { createPlatformHandler } from "./routes/platform.js";
import { createVerifyClaudeAuthHandler } from "./routes/verify-claude-auth.js";
export function createSetupRoutes(): Router {
const router = Router();
@@ -17,8 +19,10 @@ export function createSetupRoutes(): Router {
router.post("/install-claude", createInstallClaudeHandler());
router.post("/auth-claude", createAuthClaudeHandler());
router.post("/store-api-key", createStoreApiKeyHandler());
router.post("/delete-api-key", createDeleteApiKeyHandler());
router.get("/api-keys", createApiKeysHandler());
router.get("/platform", createPlatformHandler());
router.post("/verify-claude-auth", createVerifyClaudeAuthHandler());
return router;
}

View File

@@ -0,0 +1,104 @@
/**
* POST /delete-api-key endpoint - Delete a stored API key
*/
import type { Request, Response } from "express";
import { createLogger } from "../../../lib/logger.js";
import path from "path";
import fs from "fs/promises";
const logger = createLogger("Setup");
// In-memory storage reference (imported from common.ts pattern)
// We need to modify common.ts to export a deleteApiKey function
import { setApiKey } from "../common.js";
/**
* Remove an API key from the .env file
*/
async function removeApiKeyFromEnv(key: string): Promise<void> {
const envPath = path.join(process.cwd(), ".env");
try {
let envContent = "";
try {
envContent = await fs.readFile(envPath, "utf-8");
} catch {
// .env file doesn't exist, nothing to delete
return;
}
// Parse existing env content and remove the key
const lines = envContent.split("\n");
const keyRegex = new RegExp(`^${key}=`);
const newLines = lines.filter((line) => !keyRegex.test(line));
// Remove empty lines at the end
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === "") {
newLines.pop();
}
await fs.writeFile(envPath, newLines.join("\n") + (newLines.length > 0 ? "\n" : ""));
logger.info(`[Setup] Removed ${key} from .env file`);
} catch (error) {
logger.error(`[Setup] Failed to remove ${key} from .env:`, error);
throw error;
}
}
export function createDeleteApiKeyHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { provider } = req.body as { provider: string };
if (!provider) {
res.status(400).json({
success: false,
error: "Provider is required",
});
return;
}
logger.info(`[Setup] Deleting API key for provider: ${provider}`);
// Map provider to env key name
const envKeyMap: Record<string, string> = {
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_GENERATIVE_AI_API_KEY",
openai: "OPENAI_API_KEY",
};
const envKey = envKeyMap[provider];
if (!envKey) {
res.status(400).json({
success: false,
error: `Unknown provider: ${provider}`,
});
return;
}
// Clear from in-memory storage
setApiKey(provider, "");
// Remove from environment
delete process.env[envKey];
// Remove from .env file
await removeApiKeyFromEnv(envKey);
logger.info(`[Setup] Successfully deleted API key for ${provider}`);
res.json({
success: true,
message: `API key for ${provider} has been deleted`,
});
} catch (error) {
logger.error("[Setup] Delete API key error:", error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : "Failed to delete API key",
});
}
};
}

View File

@@ -0,0 +1,330 @@
/**
* POST /verify-claude-auth endpoint - Verify Claude authentication by running a test query
* Supports verifying either CLI auth or API key auth independently
*/
import type { Request, Response } from "express";
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createLogger } from "../../../lib/logger.js";
import { getApiKey } from "../common.js";
const logger = createLogger("Setup");
// Known error patterns that indicate auth failure
const AUTH_ERROR_PATTERNS = [
"OAuth token revoked",
"Please run /login",
"please run /login",
"token revoked",
"invalid_api_key",
"authentication_error",
"unauthorized",
"not authenticated",
"authentication failed",
"invalid api key",
"api key is invalid",
];
// Patterns that indicate billing/credit issues - should FAIL verification
const BILLING_ERROR_PATTERNS = [
"credit balance is too low",
"credit balance too low",
"insufficient credits",
"insufficient balance",
"no credits",
"out of credits",
"billing",
"payment required",
"add credits",
];
// Patterns that indicate rate/usage limits - should FAIL verification
// Users need to wait or upgrade their plan
const RATE_LIMIT_PATTERNS = [
"limit reached",
"rate limit",
"rate_limit",
"resets", // Only valid if it's a temporary reset, not a billing issue
"/upgrade",
"extra-usage",
];
function isRateLimitError(text: string): boolean {
const lowerText = text.toLowerCase();
// First check if it's a billing error - billing errors are NOT rate limits
if (isBillingError(text)) {
return false;
}
return RATE_LIMIT_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
}
function isBillingError(text: string): boolean {
const lowerText = text.toLowerCase();
return BILLING_ERROR_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
}
function containsAuthError(text: string): boolean {
const lowerText = text.toLowerCase();
return AUTH_ERROR_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
}
export function createVerifyClaudeAuthHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
// Get the auth method from the request body
const { authMethod } = req.body as { authMethod?: "cli" | "api_key" };
logger.info(
`[Setup] Verifying Claude authentication using method: ${
authMethod || "auto"
}`
);
// Create an AbortController with a 30-second timeout
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), 30000);
let authenticated = false;
let errorMessage = "";
let receivedAnyContent = false;
// Save original env values
const originalAnthropicKey = process.env.ANTHROPIC_API_KEY;
try {
// Configure environment based on auth method
if (authMethod === "cli") {
// For CLI verification, remove any API key so it uses CLI credentials only
delete process.env.ANTHROPIC_API_KEY;
logger.info(
"[Setup] Cleared API key environment for CLI verification"
);
} else if (authMethod === "api_key") {
// For API key verification, ensure we're using the stored API key
const storedApiKey = getApiKey("anthropic");
if (storedApiKey) {
process.env.ANTHROPIC_API_KEY = storedApiKey;
logger.info("[Setup] Using stored API key for verification");
} else {
// Check env var
if (!process.env.ANTHROPIC_API_KEY) {
res.json({
success: true,
authenticated: false,
error: "No API key configured. Please enter an API key first.",
});
return;
}
}
}
// Run a minimal query to verify authentication
const stream = query({
prompt: "Reply with only the word 'ok'",
options: {
model: "claude-sonnet-4-20250514",
maxTurns: 1,
allowedTools: [],
abortController,
},
});
// Collect all messages and check for errors
const allMessages: string[] = [];
for await (const msg of stream) {
const msgStr = JSON.stringify(msg);
allMessages.push(msgStr);
logger.info("[Setup] Stream message:", msgStr.substring(0, 500));
// Check for billing errors FIRST - these should fail verification
if (isBillingError(msgStr)) {
logger.error("[Setup] Found billing error in message");
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
authenticated = false;
break;
}
// Check if any part of the message contains auth errors
if (containsAuthError(msgStr)) {
logger.error("[Setup] Found auth error in message");
if (authMethod === "cli") {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
}
break;
}
// Check specifically for assistant messages with text content
if (msg.type === "assistant" && (msg as any).message?.content) {
const content = (msg as any).message.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === "text" && block.text) {
const text = block.text;
logger.info("[Setup] Assistant text:", text);
if (containsAuthError(text)) {
if (authMethod === "cli") {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
}
break;
}
// Valid text response that's not an error
if (text.toLowerCase().includes("ok") || text.length > 0) {
receivedAnyContent = true;
}
}
}
}
}
// Check for result messages
if (msg.type === "result") {
const resultStr = JSON.stringify(msg);
// First check for billing errors - these should FAIL verification
if (isBillingError(resultStr)) {
logger.error(
"[Setup] Billing error detected - insufficient credits"
);
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
authenticated = false;
break;
}
// Check if it's a rate limit error - should FAIL verification
else if (isRateLimitError(resultStr)) {
logger.warn(
"[Setup] Rate limit detected - treating as unverified"
);
errorMessage =
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
authenticated = false;
break;
} else if (containsAuthError(resultStr)) {
if (authMethod === "cli") {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
}
} else {
// Got a result without errors
receivedAnyContent = true;
}
}
}
// Determine authentication status
if (errorMessage) {
authenticated = false;
} else if (receivedAnyContent) {
authenticated = true;
} else {
// No content received - might be an issue
logger.warn("[Setup] No content received from stream");
logger.warn("[Setup] All messages:", allMessages.join("\n"));
errorMessage =
"No response received from Claude. Please check your authentication.";
}
} catch (error: unknown) {
const errMessage =
error instanceof Error ? error.message : String(error);
logger.error("[Setup] Claude auth verification exception:", errMessage);
// Check for billing errors FIRST - these always fail
if (isBillingError(errMessage)) {
authenticated = false;
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
}
// Check for rate limit in exception - should FAIL verification
else if (isRateLimitError(errMessage)) {
authenticated = false;
errorMessage =
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
logger.warn(
"[Setup] Rate limit in exception - treating as unverified"
);
}
// If we already determined auth was successful, keep it
else if (authenticated) {
logger.info("[Setup] Auth already confirmed, ignoring exception");
}
// Check for auth-related errors in exception
else if (containsAuthError(errMessage)) {
if (authMethod === "cli") {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
}
} else if (
errMessage.includes("abort") ||
errMessage.includes("timeout")
) {
errorMessage = "Verification timed out. Please try again.";
} else if (
errMessage.includes("exit") &&
errMessage.includes("code 1")
) {
// Process exited with code 1 but we might have gotten rate limit info in the stream
// Check if we received any content that indicated auth worked
if (receivedAnyContent && !errorMessage) {
authenticated = true;
logger.info(
"[Setup] Process exit 1 but content received - auth valid"
);
} else if (!errorMessage) {
errorMessage = errMessage;
}
} else if (!errorMessage) {
errorMessage = errMessage;
}
} finally {
clearTimeout(timeoutId);
// Restore original environment
if (originalAnthropicKey !== undefined) {
process.env.ANTHROPIC_API_KEY = originalAnthropicKey;
} else if (authMethod === "cli") {
// If we cleared it and there was no original, keep it cleared
delete process.env.ANTHROPIC_API_KEY;
}
}
logger.info("[Setup] Verification result:", {
authenticated,
errorMessage,
authMethod,
});
res.json({
success: true,
authenticated,
error: errorMessage || undefined,
});
} catch (error) {
logger.error("[Setup] Verify Claude auth endpoint error:", error);
res.status(500).json({
success: false,
authenticated: false,
error: error instanceof Error ? error.message : "Verification failed",
});
}
};
}