a83175c94c
- Consolidated @oh-my-pi/pi-utils subpath imports into single package root import across 100+ files. - Moved tryParseJson utility from local web scrapers module to @oh-my-pi/pi-utils package for centralized JSON parsing. - Renamed loadSkillsFromDir to scanSkillsFromDir and refactored skill discovery to use fs.promises.readdir instead of glob-based approach. - Replaced custom parseJSON with tryParseJson across discovery modules for consistent error handling. - Removed emitCustomToolSessionEvent method and cleanupSshResources function, consolidating shutdown logic into dispose method. - Updated glob pattern construction to use GlobBuilder with literal_separator(true) for improved path handling.
156 lines
4.8 KiB
TypeScript
156 lines
4.8 KiB
TypeScript
/**
|
|
* Anthropic Authentication
|
|
*
|
|
* 3-tier auth resolution:
|
|
* 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL env vars
|
|
* 2. OAuth credentials in ~/.omp/agent/agent.db (with expiry check)
|
|
* 3. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
|
|
*/
|
|
import { $env, getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
import { type AuthCredential, AuthCredentialStore } from "../auth-storage";
|
|
import { buildAnthropicHeaders as buildProviderAnthropicHeaders } from "../providers/anthropic";
|
|
import { getEnvApiKey } from "../stream";
|
|
|
|
/** Auth configuration for Anthropic */
|
|
export interface AnthropicAuthConfig {
|
|
apiKey: string;
|
|
baseUrl: string;
|
|
isOAuth: boolean;
|
|
}
|
|
|
|
/** OAuth credential for Anthropic API access */
|
|
export interface AnthropicOAuthCredential {
|
|
type: "oauth";
|
|
access: string;
|
|
refresh?: string;
|
|
/** Expiry timestamp in milliseconds */
|
|
expires: number;
|
|
}
|
|
|
|
const DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
|
|
/**
|
|
* Checks if a token is an OAuth token by looking for sk-ant-oat prefix.
|
|
* @param apiKey - The API key to check
|
|
* @returns True if the token is an OAuth token
|
|
*/
|
|
export function isOAuthToken(apiKey: string): boolean {
|
|
return apiKey.includes("sk-ant-oat");
|
|
}
|
|
|
|
/**
|
|
* Converts a generic AuthCredential to AnthropicOAuthCredential if it's a valid OAuth entry.
|
|
* @param credential - The credential to convert
|
|
* @returns The converted OAuth credential, or null if not a valid OAuth type
|
|
*/
|
|
function toAnthropicOAuthCredential(credential: AuthCredential): AnthropicOAuthCredential | null {
|
|
if (credential.type !== "oauth") return null;
|
|
if (typeof credential.access !== "string" || typeof credential.expires !== "number") return null;
|
|
return {
|
|
type: "oauth",
|
|
access: credential.access,
|
|
refresh: credential.refresh,
|
|
expires: credential.expires,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads Anthropic OAuth credentials from an AuthCredentialStore.
|
|
* @param store - Credential store to read from (creates AuthCredentialStore if not provided)
|
|
* @returns Array of valid Anthropic OAuth credentials
|
|
*/
|
|
async function readAnthropicOAuthCredentials(store?: AuthCredentialStore): Promise<AnthropicOAuthCredential[]> {
|
|
const ownsStore = !store;
|
|
const effectiveStore = store ?? (await AuthCredentialStore.open(getAgentDbPath()));
|
|
try {
|
|
const records = effectiveStore.listAuthCredentials("anthropic");
|
|
const credentials: AnthropicOAuthCredential[] = [];
|
|
for (const record of records) {
|
|
const mapped = toAnthropicOAuthCredential(record.credential);
|
|
if (mapped) {
|
|
credentials.push(mapped);
|
|
}
|
|
}
|
|
|
|
return credentials;
|
|
} finally {
|
|
if (ownsStore) {
|
|
effectiveStore.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds Anthropic auth config using 3-tier priority:
|
|
* 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
|
|
* 2. OAuth in agent.db (with 5-minute expiry buffer)
|
|
* 3. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
|
|
* @param store - Optional credential store (creates one from default db path if not provided)
|
|
* @returns The first valid auth configuration found, or null if none available
|
|
*/
|
|
export async function findAnthropicAuth(store?: AuthCredentialStore): Promise<AnthropicAuthConfig | null> {
|
|
// 1. Explicit search-specific env vars
|
|
const searchApiKey = $env.ANTHROPIC_SEARCH_API_KEY;
|
|
const searchBaseUrl = $env.ANTHROPIC_SEARCH_BASE_URL;
|
|
if (searchApiKey) {
|
|
return {
|
|
apiKey: searchApiKey,
|
|
baseUrl: searchBaseUrl ?? DEFAULT_BASE_URL,
|
|
isOAuth: isOAuthToken(searchApiKey),
|
|
};
|
|
}
|
|
|
|
// 2. OAuth credentials in agent.db (with 5-minute expiry buffer)
|
|
const expiryBuffer = 5 * 60 * 1000; // 5 minutes
|
|
const now = Date.now();
|
|
const credentials = await readAnthropicOAuthCredentials(store);
|
|
for (const credential of credentials) {
|
|
if (!credential.access) continue;
|
|
if (credential.expires > now + expiryBuffer) {
|
|
return {
|
|
apiKey: credential.access,
|
|
baseUrl: DEFAULT_BASE_URL,
|
|
isOAuth: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
// 3. Generic ANTHROPIC_API_KEY fallback
|
|
const apiKey = getEnvApiKey("anthropic");
|
|
const baseUrl = $env.ANTHROPIC_BASE_URL;
|
|
if (apiKey) {
|
|
return {
|
|
apiKey,
|
|
baseUrl: baseUrl ?? DEFAULT_BASE_URL,
|
|
isOAuth: isOAuthToken(apiKey),
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Builds HTTP headers for Anthropic API requests (search variant).
|
|
* @param auth - The authentication configuration
|
|
* @returns Headers object ready for use in fetch requests
|
|
*/
|
|
export function buildAnthropicSearchHeaders(auth: AnthropicAuthConfig): Record<string, string> {
|
|
return buildProviderAnthropicHeaders({
|
|
apiKey: auth.apiKey,
|
|
baseUrl: auth.baseUrl,
|
|
isOAuth: auth.isOAuth,
|
|
extraBetas: ["web-search-2025-03-05"],
|
|
stream: false,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Builds the full API URL for Anthropic messages endpoint.
|
|
* @param auth - The authentication configuration
|
|
* @returns The complete API URL with beta query parameter
|
|
*/
|
|
export function buildAnthropicUrl(auth: AnthropicAuthConfig): string {
|
|
const base = `${auth.baseUrl}/v1/messages`;
|
|
return `${base}?beta=true`;
|
|
}
|