Spaces:
Runtime error
Runtime error
| import { | |
| getProviderConnections, | |
| createProviderConnection, | |
| updateProviderConnection, | |
| } from "@/lib/localDb"; | |
| import { ClaudeAuthFileError } from "@/lib/oauth/utils/claudeAuthFile"; | |
| type JsonRecord = Record<string, unknown>; | |
| function toRecord(value: unknown): JsonRecord { | |
| return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; | |
| } | |
| function toNonEmptyString(value: unknown): string | null { | |
| if (typeof value !== "string") return null; | |
| const trimmed = value.trim(); | |
| return trimmed ? trimmed : null; | |
| } | |
| // ββββ Public types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface ParsedClaudeAuth { | |
| accessToken: string; | |
| refreshToken: string; | |
| expiresAt: string | null; // ISO (converted from ms) | |
| scopes: string[]; | |
| subscriptionType: string | null; | |
| rateLimitTier: string | null; | |
| email: string | null; // from bootstrap enrichment | |
| } | |
| export interface EnrichedClaudeAuth extends ParsedClaudeAuth { | |
| accountUUID: string | null; | |
| organizationUUID: string | null; | |
| organizationName: string | null; | |
| organizationType: string | null; | |
| rateLimitTier: string | null; | |
| } | |
| export interface CreateConnectionOptions { | |
| name?: string; | |
| email?: string; | |
| overwriteExisting?: boolean; | |
| } | |
| // ββββ Parse & validate ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function parseAndValidateClaudeAuth(raw: unknown): ParsedClaudeAuth { | |
| const doc = toRecord(raw); | |
| const oauthBlock = toRecord(doc.claudeAiOauth); | |
| const accessToken = toNonEmptyString(oauthBlock.accessToken); | |
| const refreshToken = toNonEmptyString(oauthBlock.refreshToken); | |
| if (!accessToken) { | |
| throw new ClaudeAuthFileError( | |
| "accessToken is missing or empty in claudeAiOauth", | |
| 400, | |
| "missing_access_token" | |
| ); | |
| } | |
| if (!refreshToken) { | |
| throw new ClaudeAuthFileError( | |
| "refreshToken is missing or empty in claudeAiOauth", | |
| 400, | |
| "missing_refresh_token" | |
| ); | |
| } | |
| // expiresAt in the file is ms epoch; store as ISO in DB | |
| let expiresAt: string | null = null; | |
| const rawExpiresAt = oauthBlock.expiresAt; | |
| if (typeof rawExpiresAt === "number" && Number.isFinite(rawExpiresAt)) { | |
| expiresAt = new Date(rawExpiresAt).toISOString(); | |
| } else if (typeof rawExpiresAt === "string" && rawExpiresAt.trim()) { | |
| expiresAt = rawExpiresAt.trim(); | |
| } | |
| const rawScopes = oauthBlock.scopes; | |
| const scopes: string[] = Array.isArray(rawScopes) | |
| ? rawScopes.filter((s): s is string => typeof s === "string") | |
| : []; | |
| return { | |
| accessToken, | |
| refreshToken, | |
| expiresAt, | |
| scopes, | |
| subscriptionType: toNonEmptyString(oauthBlock.subscriptionType), | |
| rateLimitTier: toNonEmptyString(oauthBlock.rateLimitTier), | |
| email: null, | |
| }; | |
| } | |
| // ββββ Bootstrap enrichment ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export async function enrichWithBootstrap( | |
| parsed: ParsedClaudeAuth, | |
| // proxyConfig reserved for future authenticated-proxy support | |
| proxyConfig?: null | |
| ): Promise<EnrichedClaudeAuth> { | |
| const base: EnrichedClaudeAuth = { | |
| ...parsed, | |
| accountUUID: null, | |
| organizationUUID: null, | |
| organizationName: null, | |
| organizationType: null, | |
| rateLimitTier: parsed.rateLimitTier, | |
| }; | |
| const controller = new AbortController(); | |
| const timer = setTimeout(() => controller.abort(), 8000); | |
| try { | |
| const res = await fetch("https://api.anthropic.com/api/claude_cli/bootstrap", { | |
| method: "GET", | |
| headers: { | |
| Authorization: `Bearer ${parsed.accessToken}`, | |
| "anthropic-version": "2023-06-01", | |
| "Content-Type": "application/json", | |
| }, | |
| signal: controller.signal, | |
| }); | |
| if (!res.ok) { | |
| return base; | |
| } | |
| const body = toRecord(await res.json()); | |
| const accountUUID = toNonEmptyString(body.account_uuid); | |
| const organizationUUID = toNonEmptyString(body.organization_uuid); | |
| const organizationName = toNonEmptyString(body.organization_name); | |
| const organizationType = toNonEmptyString(body.organization_type); | |
| const rateLimitTier = toNonEmptyString(body.rate_limit_tier) || parsed.rateLimitTier; | |
| const bootstrapEmail = toNonEmptyString(body.account_email); | |
| return { | |
| ...base, | |
| accountUUID, | |
| organizationUUID, | |
| organizationName, | |
| organizationType, | |
| rateLimitTier, | |
| email: parsed.email || bootstrapEmail, | |
| }; | |
| } catch { | |
| // Network error, timeout, or parse failure β best-effort; callers handle null fields | |
| return base; | |
| } finally { | |
| clearTimeout(timer); | |
| } | |
| } | |
| // ββββ Lookup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export async function findExistingClaudeConnection( | |
| accountUUID: string | |
| ): Promise<JsonRecord | null> { | |
| const connections = await getProviderConnections({ provider: "claude" }); | |
| const lower = accountUUID.toLowerCase(); | |
| return ( | |
| (connections.find((c) => { | |
| const psd = toRecord((c as JsonRecord).providerSpecificData); | |
| const stored = toNonEmptyString(psd.accountUUID); | |
| return stored !== null && stored.toLowerCase() === lower; | |
| }) as JsonRecord | undefined) ?? null | |
| ); | |
| } | |
| // ββββ Create / update connection ββββββββββββββββββββββββββββββββββββββββββββββ | |
| export async function createConnectionFromAuthFile( | |
| enriched: EnrichedClaudeAuth, | |
| options: CreateConnectionOptions | |
| ): Promise<{ connection: JsonRecord; created: boolean }> { | |
| // Duplicate detection by accountUUID (skipped when bootstrap failed) | |
| if (enriched.accountUUID) { | |
| const existing = await findExistingClaudeConnection(enriched.accountUUID); | |
| if (existing) { | |
| if (!options.overwriteExisting) { | |
| throw new ClaudeAuthFileError( | |
| "A Claude connection for this account already exists. Pass overwriteExisting: true to replace it.", | |
| 409, | |
| "duplicate_account" | |
| ); | |
| } | |
| const updated = await updateProviderConnection(existing.id as string, { | |
| accessToken: enriched.accessToken, | |
| refreshToken: enriched.refreshToken, | |
| expiresAt: enriched.expiresAt, | |
| email: | |
| options.email || enriched.email || (existing.email as string | undefined) || undefined, | |
| name: | |
| options.name || | |
| (existing.name as string | undefined) || | |
| options.email || | |
| enriched.email || | |
| "Claude (imported)", | |
| testStatus: "active", | |
| providerSpecificData: { | |
| ...toRecord(existing.providerSpecificData), | |
| accountUUID: enriched.accountUUID, | |
| organizationUUID: enriched.organizationUUID, | |
| organizationName: enriched.organizationName, | |
| organizationType: enriched.organizationType, | |
| rateLimitTier: enriched.rateLimitTier, | |
| scopes: enriched.scopes, | |
| subscriptionType: enriched.subscriptionType, | |
| bootstrapEmail: enriched.email, | |
| importedAt: new Date().toISOString(), | |
| }, | |
| }); | |
| return { connection: updated || existing, created: false }; | |
| } | |
| } | |
| // Identity check: when bootstrap failed and we have no email, refuse unless | |
| // the caller has explicitly opted into overwrite mode (they know what they're doing). | |
| if (!enriched.email && !enriched.accountUUID && !options.overwriteExisting) { | |
| throw new ClaudeAuthFileError( | |
| "Could not verify the account identity (bootstrap failed and no email/accountUUID available). Pass overwriteExisting: true to import anyway.", | |
| 409, | |
| "identity_unverified" | |
| ); | |
| } | |
| const email = options.email || enriched.email || undefined; | |
| const name = options.name || options.email || enriched.email || "Claude (imported)"; | |
| const connection = await createProviderConnection({ | |
| provider: "claude", | |
| authType: "oauth", | |
| name, | |
| email, | |
| accessToken: enriched.accessToken, | |
| refreshToken: enriched.refreshToken, | |
| expiresAt: enriched.expiresAt, | |
| isActive: true, | |
| testStatus: "active", | |
| providerSpecificData: { | |
| accountUUID: enriched.accountUUID, | |
| organizationUUID: enriched.organizationUUID, | |
| organizationName: enriched.organizationName, | |
| organizationType: enriched.organizationType, | |
| rateLimitTier: enriched.rateLimitTier, | |
| scopes: enriched.scopes, | |
| subscriptionType: enriched.subscriptionType, | |
| bootstrapEmail: enriched.email, | |
| importedAt: new Date().toISOString(), | |
| }, | |
| }); | |
| return { connection, created: true }; | |
| } | |