feat(ai): adapt OAuth flows to OAuthAuth (phase 4)
anthropic, openai-codex, and github-copilot flow modules gain OAuthAuth exports (login/refresh/toAuth) wired to the prompt()/notify() login callbacks, making the lazyOAuth attachments on the provider factories functional. Copilot's modifyModels baseUrl rewriting becomes toAuth() returning ModelAuth.baseUrl derived from the token proxy endpoint. Callback-server flows race a manual_code prompt and abort it through AuthPrompt.signal once the flow settles; OAuthAuth has no usesCallbackServer flag. The old OAuthProviderInterface exports stay unchanged until the coding-agent migration.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { Server } from "node:http";
|
||||
import type { OAuthAuth } from "../../auth/types.ts";
|
||||
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts";
|
||||
import { generatePKCE } from "./pkce.ts";
|
||||
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.ts";
|
||||
@@ -378,6 +379,42 @@ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuth
|
||||
};
|
||||
}
|
||||
|
||||
export const anthropicOAuth: OAuthAuth = {
|
||||
name: "Anthropic (Claude Pro/Max)",
|
||||
|
||||
async login(callbacks) {
|
||||
// The manual_code prompt races the local callback server; abort it once
|
||||
// the flow settles so the UI can dismiss the pending input.
|
||||
const manualAbort = new AbortController();
|
||||
try {
|
||||
const credentials = await loginAnthropic({
|
||||
onAuth: (info) => callbacks.notify({ type: "auth_url", url: info.url, instructions: info.instructions }),
|
||||
onProgress: (message) => callbacks.notify({ type: "progress", message }),
|
||||
onPrompt: (prompt) =>
|
||||
callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }),
|
||||
onManualCodeInput: () =>
|
||||
callbacks.prompt({
|
||||
type: "manual_code",
|
||||
message: "Complete login in your browser, or paste the authorization code / redirect URL here:",
|
||||
placeholder: REDIRECT_URI,
|
||||
signal: manualAbort.signal,
|
||||
}),
|
||||
});
|
||||
return { ...credentials, type: "oauth" };
|
||||
} finally {
|
||||
manualAbort.abort();
|
||||
}
|
||||
},
|
||||
|
||||
async refresh(credential) {
|
||||
return { ...(await refreshAnthropicToken(credential.refresh)), type: "oauth" };
|
||||
},
|
||||
|
||||
async toAuth(credential) {
|
||||
return { apiKey: credential.access };
|
||||
},
|
||||
};
|
||||
|
||||
export const anthropicOAuthProvider: OAuthProviderInterface = {
|
||||
id: "anthropic",
|
||||
name: "Anthropic (Claude Pro/Max)",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* GitHub Copilot OAuth flow
|
||||
*/
|
||||
|
||||
import type { OAuthAuth, OAuthCredential } from "../../auth/types.ts";
|
||||
import { getModels } from "../../models.ts";
|
||||
import type { Api, Model } from "../../types.ts";
|
||||
import { pollOAuthDeviceCodeFlow } from "./device-code.ts";
|
||||
@@ -330,6 +331,42 @@ export async function loginGitHubCopilot(options: {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function copilotEnterpriseDomain(credential: OAuthCredential): string | undefined {
|
||||
const enterpriseUrl = credential.enterpriseUrl;
|
||||
if (typeof enterpriseUrl !== "string" || !enterpriseUrl) return undefined;
|
||||
return normalizeDomain(enterpriseUrl) ?? undefined;
|
||||
}
|
||||
|
||||
export const githubCopilotOAuth: OAuthAuth = {
|
||||
name: "GitHub Copilot",
|
||||
|
||||
async login(callbacks) {
|
||||
const credentials = await loginGitHubCopilot({
|
||||
onDeviceCode: (info) => callbacks.notify({ type: "device_code", ...info }),
|
||||
onPrompt: (prompt) =>
|
||||
callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }),
|
||||
onProgress: (message) => callbacks.notify({ type: "progress", message }),
|
||||
signal: callbacks.signal,
|
||||
});
|
||||
return { ...credentials, type: "oauth" };
|
||||
},
|
||||
|
||||
async refresh(credential) {
|
||||
return {
|
||||
...(await refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential))),
|
||||
type: "oauth",
|
||||
};
|
||||
},
|
||||
|
||||
/** Per-credential baseUrl from the token's proxy endpoint replaces the old `modifyModels` rewriting. */
|
||||
async toAuth(credential) {
|
||||
return {
|
||||
apiKey: credential.access,
|
||||
baseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const githubCopilotOAuthProvider: OAuthProviderInterface = {
|
||||
id: "github-copilot",
|
||||
name: "GitHub Copilot",
|
||||
|
||||
@@ -17,6 +17,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
|
||||
});
|
||||
}
|
||||
|
||||
import type { OAuthAuth } from "../../auth/types.ts";
|
||||
import { pollOAuthDeviceCodeFlow } from "./device-code.ts";
|
||||
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts";
|
||||
import { generatePKCE } from "./pkce.ts";
|
||||
@@ -560,6 +561,62 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAu
|
||||
return credentialsFromToken(await refreshAccessToken(refreshToken));
|
||||
}
|
||||
|
||||
export const openaiCodexOAuth: OAuthAuth = {
|
||||
name: "OpenAI (ChatGPT Plus/Pro)",
|
||||
|
||||
async login(callbacks) {
|
||||
const method = await callbacks.prompt({
|
||||
type: "select",
|
||||
message: "Select OpenAI Codex login method:",
|
||||
options: [
|
||||
{ id: OPENAI_CODEX_BROWSER_LOGIN_METHOD, label: "Browser login (default)" },
|
||||
{ id: OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD, label: "Device code login (headless)" },
|
||||
],
|
||||
});
|
||||
|
||||
if (method === OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD) {
|
||||
const credentials = await loginOpenAICodexDeviceCode({
|
||||
onDeviceCode: (info) => callbacks.notify({ type: "device_code", ...info }),
|
||||
signal: callbacks.signal,
|
||||
});
|
||||
return { ...credentials, type: "oauth" };
|
||||
}
|
||||
if (method !== OPENAI_CODEX_BROWSER_LOGIN_METHOD) {
|
||||
throw new Error(`Unknown OpenAI Codex login method: ${method}`);
|
||||
}
|
||||
|
||||
// The manual_code prompt races the local callback server; abort it once
|
||||
// the flow settles so the UI can dismiss the pending input.
|
||||
const manualAbort = new AbortController();
|
||||
try {
|
||||
const credentials = await loginOpenAICodex({
|
||||
onAuth: (info) => callbacks.notify({ type: "auth_url", url: info.url, instructions: info.instructions }),
|
||||
onProgress: (message) => callbacks.notify({ type: "progress", message }),
|
||||
onPrompt: (prompt) =>
|
||||
callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }),
|
||||
onManualCodeInput: () =>
|
||||
callbacks.prompt({
|
||||
type: "manual_code",
|
||||
message: "Complete login in your browser, or paste the authorization code / redirect URL here:",
|
||||
placeholder: REDIRECT_URI,
|
||||
signal: manualAbort.signal,
|
||||
}),
|
||||
});
|
||||
return { ...credentials, type: "oauth" };
|
||||
} finally {
|
||||
manualAbort.abort();
|
||||
}
|
||||
},
|
||||
|
||||
async refresh(credential) {
|
||||
return { ...(await refreshOpenAICodexToken(credential.refresh)), type: "oauth" };
|
||||
},
|
||||
|
||||
async toAuth(credential) {
|
||||
return { apiKey: credential.access };
|
||||
},
|
||||
};
|
||||
|
||||
export const openaiCodexOAuthProvider: OAuthProviderInterface = {
|
||||
id: "openai-codex",
|
||||
name: "ChatGPT Plus/Pro (Codex Subscription)",
|
||||
|
||||
Reference in New Issue
Block a user