File size: 4,211 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | import open from "open";
import { OAuthService } from "./oauth.js";
import { CODEX_CONFIG } from "../constants/oauth.js";
import { getServerCredentials } from "../config/index.js";
import { startLocalServer } from "../utils/server.js";
import { generatePKCE } from "../utils/pkce.js";
import { spinner as createSpinner } from "../utils/ui.js";
/**
* Codex (OpenAI) OAuth Service
*/
export class CodexService extends OAuthService {
constructor() {
super(CODEX_CONFIG);
}
/**
* Build Codex authorization URL
*/
buildCodexAuthUrl(redirectUri, state, codeChallenge) {
// Build URL manually to ensure space encoding as %20 instead of +
const params = {
response_type: "code",
client_id: CODEX_CONFIG.clientId,
redirect_uri: redirectUri,
scope: CODEX_CONFIG.scope,
code_challenge: codeChallenge,
code_challenge_method: CODEX_CONFIG.codeChallengeMethod,
...CODEX_CONFIG.extraParams,
state: state,
};
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&");
return `${CODEX_CONFIG.authorizeUrl}?${queryString}`;
}
/**
* Save Codex tokens to server
*/
async saveTokens(tokens) {
const { server, token, userId } = getServerCredentials();
const response = await fetch(`${server}/api/cli/providers/codex`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"X-User-Id": userId,
},
body: JSON.stringify({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
lastRefreshAt: new Date().toISOString(),
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to save tokens");
}
return await response.json();
}
/**
* Complete Codex OAuth flow
*/
async connect() {
const spinner = createSpinner("Starting Codex OAuth...").start();
try {
spinner.text = "Starting local server...";
// Start local server for callback (use fixed port 1455 like real Codex CLI)
const fixedPort = 1455;
let callbackParams = null;
const { port, close } = await startLocalServer((params) => {
callbackParams = params;
}, fixedPort);
const redirectUri = `http://localhost:${port}/auth/callback`;
spinner.succeed(`Local server started on port ${port}`);
// Generate PKCE
const { codeVerifier, codeChallenge, state } = generatePKCE();
// Build authorization URL
const authUrl = this.buildCodexAuthUrl(redirectUri, state, codeChallenge);
console.log("\nOpening browser for OpenAI authentication...");
console.log(`If browser doesn't open, visit:\n${authUrl}\n`);
// Open browser
await open(authUrl);
// Wait for callback
spinner.start("Waiting for OpenAI authorization...");
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Authentication timeout (5 minutes)"));
}, 300000);
const checkInterval = setInterval(() => {
if (callbackParams) {
clearInterval(checkInterval);
clearTimeout(timeout);
resolve();
}
}, 100);
});
close();
if (callbackParams.error) {
throw new Error(callbackParams.error_description || callbackParams.error);
}
if (!callbackParams.code) {
throw new Error("No authorization code received");
}
spinner.start("Exchanging code for tokens...");
// Exchange code for tokens (Codex uses form-urlencoded)
const tokens = await this.exchangeCode(callbackParams.code, redirectUri, codeVerifier, "application/x-www-form-urlencoded");
spinner.text = "Saving tokens to server...";
// Save tokens to server
await this.saveTokens(tokens);
spinner.succeed("Codex connected successfully!");
return true;
} catch (error) {
spinner.fail(`Failed: ${error.message}`);
throw error;
}
}
}
|