import { randomUUID } from "node:crypto"; import type { AuthInteraction, Credential, OAuthAuth, OAuthCredential, } from "@earendil-works/pi-ai"; import { builtinModels } from "@earendil-works/pi-ai/providers/all"; import { MAX_MODEL_CONTEXT_WINDOW, MAX_MODEL_MAX_TOKENS, type ModelOAuthBegin, type ModelOAuthSignInMode, type ThinkingLevel, ThinkingLevelSchema, } from "@rakazo/contracts"; import { createManualAnthropicOAuthLogin } from "./pi-anthropic-oauth.js"; export const CHATGPT_OAUTH_PROVIDER = "openai-codex"; export const COPILOT_OAUTH_PROVIDER = "github-copilot"; export const XAI_OAUTH_PROVIDER = "xai"; export const ANTHROPIC_OAUTH_PROVIDER = "anthropic"; export const SUBSCRIPTION_SIGN_IN_PROVIDERS: Record< string, { mode: ModelOAuthSignInMode; loginLabel: string; hint: string; billing: string } > = { [CHATGPT_OAUTH_PROVIDER]: { mode: "device-code", loginLabel: "Sign in with ChatGPT Plus/Pro", hint: "ChatGPT Plus/Pro", billing: "Sign in with ChatGPT Plus or Pro. Uses your OpenAI subscription. Rakazo does not pay.", }, [COPILOT_OAUTH_PROVIDER]: { mode: "device-code", loginLabel: "Sign in with GitHub Copilot", hint: "Copilot", billing: "Sign in with GitHub Copilot. Uses your Copilot subscription. Rakazo does not pay.", }, [XAI_OAUTH_PROVIDER]: { mode: "device-code", loginLabel: "Sign in with SuperGrok or X Premium", hint: "SuperGrok / key", billing: "Sign in with SuperGrok or X Premium, or paste an xAI API key. Rakazo does not pay.", }, [ANTHROPIC_OAUTH_PROVIDER]: { mode: "auth-url", loginLabel: "Sign in with Claude Pro/Max", hint: "Claude Pro/Max / key", // Button + "Or paste an API key" already explain the choices; no extra paragraph. billing: "", }, }; const MIN_OAUTH_VALIDITY_MS = 5 * 60 * 1000; const SIGN_IN_START_WAIT_MS = 30_000; export type StoredModelSecret = | { kind: "api_key"; key: string } | { kind: "oauth"; credential: OAuthCredential } | { kind: "openai_compatible"; baseUrl: string; apiKey?: string; reasoning?: boolean; thinkingLevel?: ThinkingLevel | null; maxTokens?: number; contextWindow?: number; visionModelIds?: string[]; maxImagesPerPrompt?: number; }; export type PiOAuthConnected = { status: "connected"; credential: OAuthCredential; provider: string; modelId?: string; label?: string; signal: AbortSignal; }; export type PiOAuthComplete = | { status: "pending" } | PiOAuthConnected | { status: "error"; error: string }; export type PiOAuthFinish = | { status: "pending" } | { status: "connected"; value: T } | { status: "error"; error: string }; export type PiOAuthBegin = ModelOAuthBegin; type WithoutLogin = T extends unknown ? Omit : never; type PiOAuthStarted = WithoutLogin; type LoginFn = ( providerId: string, type: "oauth", interaction: AuthInteraction, ) => Promise; type SessionState = "pending" | "ready" | "finalizing" | "consumed"; type Session = { id: string; scope: string; userId: string; spaceId: string; provider: string; modelId?: string; label?: string; abort: AbortController; state: SessionState; credential?: OAuthCredential; error?: string; finishing?: Promise; // auth-url flows: resolves the runtime's "paste the redirect URL" prompt. submitCode?: (input: string) => void; codeSubmitted?: boolean; expiresTimer?: ReturnType; }; function isOAuthCredential(value: Credential): value is OAuthCredential { return value.type === "oauth"; } export function parseModelSecret(plaintext: string): StoredModelSecret { const trimmed = plaintext.trim(); if (trimmed.startsWith("{")) { try { const parsed = JSON.parse(trimmed) as Record; if ( parsed.kind === "openai_compatible" && typeof parsed.baseUrl === "string" && parsed.baseUrl.trim() ) { const apiKey = typeof parsed.apiKey === "string" ? parsed.apiKey : undefined; const parsedThinkingLevel = ThinkingLevelSchema.nullable().safeParse(parsed.thinkingLevel); const thinkingLevel = parsedThinkingLevel.success ? parsedThinkingLevel.data : undefined; const maxTokens = typeof parsed.maxTokens === "number" && Number.isInteger(parsed.maxTokens) && parsed.maxTokens >= 1 && parsed.maxTokens <= MAX_MODEL_MAX_TOKENS ? parsed.maxTokens : undefined; const contextWindow = typeof parsed.contextWindow === "number" && Number.isInteger(parsed.contextWindow) && parsed.contextWindow >= 1 && parsed.contextWindow <= MAX_MODEL_CONTEXT_WINDOW ? parsed.contextWindow : undefined; const visionModelIds = Array.isArray(parsed.visionModelIds) ? parsed.visionModelIds.filter( (modelId): modelId is string => typeof modelId === "string" && modelId.trim().length > 0, ) : undefined; const maxImagesPerPrompt = typeof parsed.maxImagesPerPrompt === "number" && Number.isInteger(parsed.maxImagesPerPrompt) && parsed.maxImagesPerPrompt >= 1 && parsed.maxImagesPerPrompt <= 1000 ? parsed.maxImagesPerPrompt : undefined; return { kind: "openai_compatible", baseUrl: parsed.baseUrl.trim(), ...(apiKey ? { apiKey } : {}), ...(typeof parsed.reasoning === "boolean" ? { reasoning: parsed.reasoning } : {}), ...(thinkingLevel !== undefined ? { thinkingLevel } : {}), ...(maxTokens !== undefined ? { maxTokens } : {}), ...(contextWindow !== undefined ? { contextWindow } : {}), ...(visionModelIds ? { visionModelIds } : {}), ...(maxImagesPerPrompt !== undefined ? { maxImagesPerPrompt } : {}), }; } if ( parsed.type === "oauth" && typeof parsed.access === "string" && typeof parsed.refresh === "string" && typeof parsed.expires === "number" ) { return { kind: "oauth", credential: parsed as OAuthCredential }; } } catch { // Treat malformed JSON as a literal API key. } } return { kind: "api_key", key: plaintext }; } export function serializeModelSecret(secret: StoredModelSecret): string { if (secret.kind === "oauth") return JSON.stringify(secret.credential); if (secret.kind === "openai_compatible") { return JSON.stringify({ kind: "openai_compatible", baseUrl: secret.baseUrl, ...(secret.apiKey ? { apiKey: secret.apiKey } : {}), ...(secret.reasoning !== undefined ? { reasoning: secret.reasoning } : {}), ...(secret.thinkingLevel !== undefined ? { thinkingLevel: secret.thinkingLevel } : {}), ...(secret.maxTokens !== undefined ? { maxTokens: secret.maxTokens } : {}), ...(secret.contextWindow !== undefined ? { contextWindow: secret.contextWindow } : {}), ...(secret.visionModelIds !== undefined ? { visionModelIds: secret.visionModelIds } : {}), ...(secret.maxImagesPerPrompt !== undefined ? { maxImagesPerPrompt: secret.maxImagesPerPrompt } : {}), }); } return secret.key; } export function secretValuesToRedact(secret: StoredModelSecret): string[] { if (secret.kind === "api_key") return secret.key ? [secret.key] : []; if (secret.kind === "openai_compatible") return secret.apiKey ? [secret.apiKey] : []; return [secret.credential.access, secret.credential.refresh].filter(Boolean); } export function loadProviderOAuth(providerId: string): OAuthAuth | undefined { return providerCatalog().getProvider(providerId)?.auth.oauth; } let cachedProviderCatalog: ReturnType | undefined; function providerCatalog() { cachedProviderCatalog ??= builtinModels(); return cachedProviderCatalog; } type ResolveModelOpts = { persist?: (next: string) => Promise; now?: number; oauth?: Pick; signal?: AbortSignal; }; export async function resolveModelAuth( plaintext: string, provider: string, opts?: ResolveModelOpts, ): Promise<{ secret: StoredModelSecret; apiKey: string }> { const parsed = parseModelSecret(plaintext); if (parsed.kind === "api_key") return { secret: parsed, apiKey: parsed.key }; if (parsed.kind === "openai_compatible") { return { secret: parsed, apiKey: parsed.apiKey ?? "" }; } const oauth = opts?.oauth ?? loadProviderOAuth(provider); if (!oauth) { throw new Error(`No OAuth handler for ${provider}. Sign in again from onboarding.`); } const now = opts?.now ?? Date.now(); let credential = parsed.credential; if (credential.expires - now < MIN_OAUTH_VALIDITY_MS) { credential = await oauth.refresh(credential, opts?.signal ?? new AbortController().signal); await opts?.persist?.(serializeModelSecret({ kind: "oauth", credential })); } const auth = await oauth.toAuth(credential); if (!auth.apiKey) { throw new Error("Subscription sign-in did not produce a usable token. Sign in again."); } return { secret: { kind: "oauth", credential }, apiKey: auth.apiKey }; } export async function resolveModelApiKey( plaintext: string, provider: string, opts?: ResolveModelOpts, ): Promise { const resolved = await resolveModelAuth(plaintext, provider, opts); return resolved.apiKey; } export class PiOAuthLogins { private readonly pending = new Map(); private readonly activeByScope = new Map(); private readonly replacementTails = new Map>(); constructor(private readonly loginFn: LoginFn = defaultLogin) {} async begin(input: { userId: string; spaceId: string; provider: string; modelId?: string; label?: string; signal?: AbortSignal; }): Promise { if (!SUBSCRIPTION_SIGN_IN_PROVIDERS[input.provider]) { throw new Error( "In-app subscription sign-in is only available for ChatGPT Plus/Pro, Claude Pro/Max, GitHub Copilot, and SuperGrok.", ); } if (input.signal?.aborted) { throw input.signal.reason ?? new Error("Sign-in cancelled."); } const scope = oauthScopeKey(input.userId, input.spaceId, input.provider); const prepared = await this.withReplacementLock(scope, input.signal, async () => { await this.retireActiveSession(scope, input.signal); throwIfAborted(input.signal); const abort = new AbortController(); const abortFromRequest = () => abort.abort(input.signal?.reason); const loginId = randomUUID(); const session: Session = { id: loginId, scope, userId: input.userId, spaceId: input.spaceId, provider: input.provider, modelId: input.modelId, label: input.label, abort, state: "pending", }; const signInStarted = deferred(); const done = this.loginFn(input.provider, "oauth", { signal: abort.signal, async prompt(prompt) { if (prompt.type === "select") { const option = prompt.options.find((entry) => entry.id === "device_code"); if (!option) { throw new Error("Device-code sign-in is not available for this provider."); } return option.id; } if (prompt.type === "manual_code") { return new Promise((resolve, reject) => { const signals = [abort.signal, prompt.signal].filter( (signal): signal is AbortSignal => signal !== undefined, ); const removeAbortListeners = () => { for (const signal of signals) signal.removeEventListener("abort", onAbort); }; const onAbort = (event: Event) => { removeAbortListeners(); session.submitCode = undefined; const signal = event.currentTarget as AbortSignal; reject(signal.reason ?? new Error("Sign-in cancelled.")); }; session.submitCode = (code) => { removeAbortListeners(); session.submitCode = undefined; resolve(code); }; const aborted = signals.find((signal) => signal.aborted); if (aborted) { removeAbortListeners(); session.submitCode = undefined; reject(aborted.reason ?? new Error("Sign-in cancelled.")); } else { for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true }); } }); } // Copilot asks for a GitHub Enterprise host first. Blank is github.com. if (prompt.type === "text") return ""; throw new Error("Unexpected subscription login prompt."); }, notify(event) { if (event.type === "device_code") { signInStarted.resolve({ mode: "device-code", userCode: event.userCode, verificationUri: httpsAuthorizationUrl(event.verificationUri), expiresInSeconds: event.expiresInSeconds ?? 15 * 60, }); } if (event.type === "auth_url") { signInStarted.resolve({ mode: "auth-url", verificationUri: httpsAuthorizationUrl(event.url), expiresInSeconds: 15 * 60, }); } }, }) .then((credential) => { if (!isOAuthCredential(credential)) { throw new Error("Subscription sign-in did not return an OAuth credential."); } session.credential = credential; session.state = "ready"; return credential; }) .catch((error) => { session.error = error instanceof Error ? error.message : "Subscription sign-in failed."; signInStarted.reject(error instanceof Error ? error : new Error(session.error)); throw error; }); if (input.signal?.aborted) abortFromRequest(); else input.signal?.addEventListener("abort", abortFromRequest, { once: true }); void done.catch(() => undefined); this.pending.set(loginId, session); this.activeByScope.set(scope, session); return { abort, abortFromRequest, signInStarted: signInStarted.promise, loginId, session }; }); const { abort, abortFromRequest, signInStarted, loginId, session } = prepared; try { const started = await Promise.race([ signInStarted, sleep(SIGN_IN_START_WAIT_MS).then(() => { throw new Error("Subscription sign-in did not start. Try again."); }), ]); input.signal?.removeEventListener("abort", abortFromRequest); if (abort.signal.aborted) throw abort.signal.reason ?? new Error("Sign-in cancelled."); session.expiresTimer = setTimeout(() => { if (session.state === "finalizing") return; session.abort.abort(new Error("Sign-in expired.")); this.removeSession(session); }, started.expiresInSeconds * 1000); session.expiresTimer.unref?.(); return { loginId, provider: input.provider, ...started, }; } catch (error) { input.signal?.removeEventListener("abort", abortFromRequest); abort.abort(); this.removeSession(session); throw error; } } submit(loginId: string, actor: { userId: string; spaceId: string }, code: string): { ok: true } { const session = this.pending.get(loginId); if (!session || session.userId !== actor.userId || session.spaceId !== actor.spaceId) { throw new Error("Sign-in session not found. Start sign-in again."); } if (session.error) throw new Error(session.error); const normalizedCode = code.trim(); if (!normalizedCode) throw new Error("Paste an authorization code or callback URL."); const resolve = session.submitCode; if (session.codeSubmitted) return { ok: true }; if (!resolve) { throw new Error("This sign-in is not waiting for a pasted code."); } session.codeSubmitted = true; resolve(normalizedCode); return { ok: true }; } complete(loginId: string, actor: { userId: string; spaceId: string }): PiOAuthComplete { const session = this.pending.get(loginId); if (!session || session.userId !== actor.userId || session.spaceId !== actor.spaceId) { return { status: "error", error: "Sign-in session not found. Start sign-in again." }; } if (session.error) { this.removeSession(session); return { status: "error", error: session.error }; } if (session.state === "finalizing") return { status: "pending" }; if (session.credential) { return { status: "connected", credential: session.credential, provider: session.provider, modelId: session.modelId, label: session.label, signal: session.abort.signal, }; } return { status: "pending" }; } async finish( loginId: string, actor: { userId: string; spaceId: string }, persist: (result: PiOAuthConnected) => Promise, ): Promise> { const session = this.pending.get(loginId); if (!session || session.userId !== actor.userId || session.spaceId !== actor.spaceId) { return { status: "error", error: "Sign-in session not found. Start sign-in again." }; } if (session.state === "finalizing") return { status: "pending" }; const result = this.complete(loginId, actor); if (result.status !== "connected") return result; // The state transition is synchronous, so cancel either wins before this claim or waits for // the finalization to settle. The finalization signal is intentionally detached from cancel. const finishing = deferred(); session.finishing = finishing.promise; session.state = "finalizing"; try { const value = await persist({ ...result, signal: new AbortController().signal }); session.state = "consumed"; this.removeSession(session); session.abort.abort(); return { status: "connected", value }; } catch (error) { if (this.pending.get(loginId) === session) session.state = "ready"; throw error; } finally { if (this.pending.get(loginId) === session) session.finishing = undefined; finishing.resolve(undefined); } } async cancel(loginId: string, actor: { userId: string; spaceId: string }): Promise { while (true) { const session = this.pending.get(loginId); if (!session || session.userId !== actor.userId || session.spaceId !== actor.spaceId) { return; } if (session.state === "finalizing") { if (session.finishing) await session.finishing; else return; continue; } if (session.state === "consumed") return; session.abort.abort(new Error("Sign-in cancelled.")); this.removeSession(session); return; } } private async withReplacementLock( scope: string, signal: AbortSignal | undefined, operation: () => Promise, ): Promise { const previous = this.replacementTails.get(scope) ?? Promise.resolve(); const release = deferred(); const current = previous.then(() => release.promise); this.replacementTails.set(scope, current); try { await previous; throwIfAborted(signal); return await operation(); } finally { release.resolve(undefined); if (this.replacementTails.get(scope) === current) this.replacementTails.delete(scope); } } private async retireActiveSession(scope: string, signal: AbortSignal | undefined): Promise { while (true) { throwIfAborted(signal); const session = this.activeByScope.get(scope); if (!session) return; if (session.state === "finalizing") { if (!session.finishing) { throw new Error("OAuth session is finalizing without a completion promise."); } await session.finishing; throwIfAborted(signal); continue; } if (session.state === "consumed") { this.removeSession(session); continue; } session.abort.abort(new Error("Sign-in replaced.")); this.removeSession(session); return; } } private removeSession(session: Session): void { if (session.expiresTimer) clearTimeout(session.expiresTimer); session.expiresTimer = undefined; if (this.pending.get(session.id) === session) this.pending.delete(session.id); if (this.activeByScope.get(session.scope) === session) { this.activeByScope.delete(session.scope); } } abortAll(): void { for (const session of this.pending.values()) session.abort.abort(); this.pending.clear(); this.activeByScope.clear(); } } function defaultLogin( providerId: string, type: "oauth", interaction: AuthInteraction, ): Promise { if (providerId === ANTHROPIC_OAUTH_PROVIDER) { return createManualAnthropicOAuthLogin()(interaction); } return builtinModels().login(providerId, type, interaction); } function deferred() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function oauthScopeKey(userId: string, spaceId: string, provider: string): string { return JSON.stringify([userId, spaceId, provider]); } function httpsAuthorizationUrl(input: string): string { const url = new URL(input); if (url.protocol !== "https:") throw new Error("Authorization URL must use HTTPS."); return url.toString(); } function throwIfAborted(signal?: AbortSignal): void { if (signal?.aborted) throw signal.reason ?? new Error("Sign-in cancelled."); }