mirror of
https://github.com/hanzoai/opencode-openai-codex-auth.git
synced 2026-09-24 17:01:02 +00:00
## CODEX_MODE Implementation
Added configurable CODEX_MODE that replaces OpenCode system prompts with
a Codex-OpenCode bridge prompt for better Codex CLI parity.
**Key Features:**
- Enabled by default for optimal Codex CLI compatibility
- Configurable via ~/.opencode/openai-codex-auth-config.json
- Environment variable override support (CODEX_MODE=0 or CODEX_MODE=1)
- Priority: env var > config file > default (true)
**Bridge Prompt:**
- ~450 tokens (~90% reduction vs full OpenCode prompt)
- Critical tool replacements (apply_patch → edit, update_plan → todowrite)
- Available tools list and verification checklist
- OpenCode working style guidelines
**Configuration:**
```json
{
"codexMode": true // default
}
```
## Library Reorganization
Restructured lib/ into semantic subfolders:
```
lib/
├── auth/ # OAuth authentication
│ ├── auth.ts
│ ├── browser.ts
│ └── server.ts
├── prompts/ # System prompts
│ ├── codex.ts
│ └── codex-opencode-bridge.ts
├── request/ # Request handling
│ ├── fetch-helpers.ts
│ ├── request-transformer.ts
│ └── response-handler.ts
├── config.ts # Plugin configuration
├── constants.ts
├── logger.ts
├── types.ts
└── oauth-success.html
```
## Files Changed
**New Files:**
- lib/config.ts - Plugin configuration loading
- lib/prompts/codex-opencode-bridge.ts - Bridge prompt
- test/plugin-config.test.ts - 12 new tests
**Modified:**
- lib/request/request-transformer.ts - CODEX_MODE parameter
- lib/request/fetch-helpers.ts - Pass codexMode through pipeline
- index.ts - Load config and determine codexMode
- lib/types.ts - Add PluginConfig type
- README.md - Document CODEX_MODE configuration
**Reorganized:**
- lib/*.ts → lib/{auth,prompts,request}/*.ts
- Updated all imports across codebase and tests
## Testing
- 123 total tests (added 12 new tests for config loading)
- All tests passing
- Comprehensive CODEX_MODE coverage:
- Config file loading (exists, missing, invalid)
- Priority order testing
- Environment variable override
- Bridge prompt vs tool remap behavior
## Documentation
Updated README.md:
- Added "Plugin Configuration" section
- Documented CODEX_MODE setting and priority
- Updated features list (123 tests, CODEX_MODE)
- Updated "How It Works" section
Co-authored-by: Hanzo Dev <[email protected]>
196 lines
5.4 KiB
TypeScript
196 lines
5.4 KiB
TypeScript
import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
import { randomBytes } from "node:crypto";
|
|
import type { PKCEPair, AuthorizationFlow, TokenResult, ParsedAuthInput, JWTPayload } from "../types.js";
|
|
|
|
// OAuth constants (from openai/codex)
|
|
export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
export const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
export const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
export const REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
export const SCOPE = "openid profile email offline_access";
|
|
|
|
/**
|
|
* Generate a random state value for OAuth flow
|
|
* @returns Random hex string
|
|
*/
|
|
export function createState(): string {
|
|
return randomBytes(16).toString("hex");
|
|
}
|
|
|
|
/**
|
|
* Parse authorization code and state from user input
|
|
* @param input - User input (URL, code#state, or just code)
|
|
* @returns Parsed authorization data
|
|
*/
|
|
export function parseAuthorizationInput(input: string): ParsedAuthInput {
|
|
const value = (input || "").trim();
|
|
if (!value) return {};
|
|
|
|
try {
|
|
const url = new URL(value);
|
|
return {
|
|
code: url.searchParams.get("code") ?? undefined,
|
|
state: url.searchParams.get("state") ?? undefined,
|
|
};
|
|
} catch {}
|
|
|
|
if (value.includes("#")) {
|
|
const [code, state] = value.split("#", 2);
|
|
return { code, state };
|
|
}
|
|
if (value.includes("code=")) {
|
|
const params = new URLSearchParams(value);
|
|
return {
|
|
code: params.get("code") ?? undefined,
|
|
state: params.get("state") ?? undefined,
|
|
};
|
|
}
|
|
return { code: value };
|
|
}
|
|
|
|
/**
|
|
* Exchange authorization code for access and refresh tokens
|
|
* @param code - Authorization code from OAuth flow
|
|
* @param verifier - PKCE verifier
|
|
* @param redirectUri - OAuth redirect URI
|
|
* @returns Token result
|
|
*/
|
|
export async function exchangeAuthorizationCode(
|
|
code: string,
|
|
verifier: string,
|
|
redirectUri: string = REDIRECT_URI,
|
|
): Promise<TokenResult> {
|
|
const res = await fetch(TOKEN_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams({
|
|
grant_type: "authorization_code",
|
|
client_id: CLIENT_ID,
|
|
code,
|
|
code_verifier: verifier,
|
|
redirect_uri: redirectUri,
|
|
}),
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => "");
|
|
console.error("[openai-codex-plugin] code->token failed:", res.status, text);
|
|
return { type: "failed" };
|
|
}
|
|
const json = (await res.json()) as {
|
|
access_token?: string;
|
|
refresh_token?: string;
|
|
expires_in?: number;
|
|
};
|
|
if (
|
|
!json?.access_token ||
|
|
!json?.refresh_token ||
|
|
typeof json?.expires_in !== "number"
|
|
) {
|
|
console.error("[openai-codex-plugin] token response missing fields:", json);
|
|
return { type: "failed" };
|
|
}
|
|
return {
|
|
type: "success",
|
|
access: json.access_token,
|
|
refresh: json.refresh_token,
|
|
expires: Date.now() + json.expires_in * 1000,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decode a JWT token to extract payload
|
|
* @param token - JWT token to decode
|
|
* @returns Decoded payload or null if invalid
|
|
*/
|
|
export function decodeJWT(token: string): JWTPayload | null {
|
|
try {
|
|
const parts = token.split(".");
|
|
if (parts.length !== 3) return null;
|
|
const payload = parts[1];
|
|
const decoded = Buffer.from(payload, "base64").toString("utf-8");
|
|
return JSON.parse(decoded) as JWTPayload;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh access token using refresh token
|
|
* @param refreshToken - Refresh token
|
|
* @returns Token result
|
|
*/
|
|
export async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
|
try {
|
|
const response = await fetch(TOKEN_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams({
|
|
grant_type: "refresh_token",
|
|
refresh_token: refreshToken,
|
|
client_id: CLIENT_ID,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => "");
|
|
console.error(
|
|
"[openai-codex-plugin] Token refresh failed:",
|
|
response.status,
|
|
text,
|
|
);
|
|
return { type: "failed" };
|
|
}
|
|
|
|
const json = (await response.json()) as {
|
|
access_token?: string;
|
|
refresh_token?: string;
|
|
expires_in?: number;
|
|
};
|
|
if (
|
|
!json?.access_token ||
|
|
!json?.refresh_token ||
|
|
typeof json?.expires_in !== "number"
|
|
) {
|
|
console.error(
|
|
"[openai-codex-plugin] Token refresh response missing fields:",
|
|
json,
|
|
);
|
|
return { type: "failed" };
|
|
}
|
|
|
|
return {
|
|
type: "success",
|
|
access: json.access_token,
|
|
refresh: json.refresh_token,
|
|
expires: Date.now() + json.expires_in * 1000,
|
|
};
|
|
} catch (error) {
|
|
const err = error as Error;
|
|
console.error("[openai-codex-plugin] Token refresh error:", err);
|
|
return { type: "failed" };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create OAuth authorization flow
|
|
* @returns Authorization flow details
|
|
*/
|
|
export async function createAuthorizationFlow(): Promise<AuthorizationFlow> {
|
|
const pkce = (await generatePKCE()) as PKCEPair;
|
|
const state = createState();
|
|
|
|
const url = new URL(AUTHORIZE_URL);
|
|
url.searchParams.set("response_type", "code");
|
|
url.searchParams.set("client_id", CLIENT_ID);
|
|
url.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
url.searchParams.set("scope", SCOPE);
|
|
url.searchParams.set("code_challenge", pkce.challenge);
|
|
url.searchParams.set("code_challenge_method", "S256");
|
|
url.searchParams.set("state", state);
|
|
url.searchParams.set("id_token_add_organizations", "true");
|
|
url.searchParams.set("codex_cli_simplified_flow", "true");
|
|
url.searchParams.set("originator", "codex_cli_rs");
|
|
|
|
return { pkce, state, url: url.toString() };
|
|
}
|