mirror of
https://github.com/hanzoai/opencode-openai-codex-auth.git
synced 2026-09-24 17:01:02 +00:00
## 🎯 Overview Complete TypeScript rewrite with comprehensive testing, modular architecture, and enhanced user experience. Adds 9 pre-configured model variants matching Codex CLI presets and a beautiful OAuth success page. ## ✨ New Features ### Configuration System - **9 Model Variants**: Pre-configured reasoning levels (Low/Medium/High) for both gpt-5-codex and gpt-5, plus Minimal variants - **Full Config Example**: `config/full-opencode.json` with all variants ready to use - **Minimal Config Example**: `config/minimal-opencode.json` for basic setup - **Config Documentation**: Comprehensive guide in `config/README.md` ### OAuth Enhancement - **Animated Success Page**: Beautiful terminal-style UI with matrix rain effect, animated checkmark, and glowing effects - **Interactive Elements**: Mouse-responsive matrix rain and parallax effects - **Professional Branding**: OpenCode logo with pulse animations ### Developer Experience - **TypeScript Rewrite**: Complete migration from .mjs to .ts with strict typing - **93 Comprehensive Tests**: Full test coverage using Vitest - **Modular Architecture**: 10 focused helper functions, each < 40 lines - **Centralized Constants**: All magic values in `lib/constants.ts` - **CI/CD Pipeline**: GitHub Actions workflow for automated testing ### Documentation - **AGENTS.md**: Complete guide for AI assistants maintaining the codebase - **Enhanced README**: Updated with new configuration options and examples - **Inline JSDoc**: Comprehensive documentation for all public functions ## 🏗️ Technical Changes ### Architecture Improvements - Extracted 10 focused helper functions from monolithic fetch() - Centralized all constants and error messages - Separated concerns: auth, browser, codex, server, etc. - Type-safe discriminated unions for result types ### Build System - TypeScript compilation with declarations and source maps - Automatic HTML asset copying in build process - Test files excluded from distribution bundle - Clean dist/ output with only production code ### Testing Infrastructure - Vitest test runner with UI mode - 93 tests across 7 test files - Mock-based testing for external dependencies - Coverage reporting capability ## 📁 New Structure ``` opencode-openai-codex-auth/ ├── index.ts # Main plugin (TypeScript) ├── lib/ │ ├── auth.ts # OAuth logic │ ├── browser.ts # Platform-specific browser opening │ ├── codex.ts # Codex instructions │ ├── server.ts # OAuth callback server │ ├── fetch-helpers.ts # 10 focused helpers │ ├── constants.ts # All magic values │ ├── types.ts # TypeScript types │ ├── oauth-success.html # Animated success page │ └── ... ├── config/ │ ├── full-opencode.json # Complete config with variants │ ├── minimal-opencode.json # Basic config │ └── README.md # Config docs ├── test/ # 93 comprehensive tests ├── AGENTS.md # AI maintenance guide └── tsconfig.json # TypeScript config ``` ## 🔄 Migration Notes ### Breaking Changes - Plugin now requires TypeScript compilation (dist/ folder) - Configuration structure enhanced (backward compatible) - OAuth success page replaces plain HTML response ### Upgrade Path 1. Use new config from `config/full-opencode.json` for best experience 2. Or continue with minimal config (uses plugin defaults) 3. Rebuild plugin: `npm run build` ## 📊 Metrics - **Lines of Code**: Modular design with functions < 70 lines - **Test Coverage**: 93 tests covering all functionality - **Type Safety**: Strict TypeScript with no `any` types - **Dependencies**: Still minimal (only @openauthjs/openauth) - **Model Variants**: 9 pre-configured options for users ## 🎨 User Experience - **Codex CLI Parity**: Match official CLI reasoning presets - **Beautiful OAuth**: Professional animated success page - **Clear Documentation**: Step-by-step guides and examples - **Flexible Config**: From minimal to comprehensive setups Co-authored-by: Hanzo Dev <[email protected]>
144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
createState,
|
|
parseAuthorizationInput,
|
|
decodeJWT,
|
|
createAuthorizationFlow,
|
|
CLIENT_ID,
|
|
AUTHORIZE_URL,
|
|
REDIRECT_URI,
|
|
SCOPE,
|
|
} from '../lib/auth.js';
|
|
|
|
describe('Auth Module', () => {
|
|
describe('createState', () => {
|
|
it('should generate a random 32-character hex string', () => {
|
|
const state = createState();
|
|
expect(state).toMatch(/^[a-f0-9]{32}$/);
|
|
});
|
|
|
|
it('should generate unique states', () => {
|
|
const state1 = createState();
|
|
const state2 = createState();
|
|
expect(state1).not.toBe(state2);
|
|
});
|
|
});
|
|
|
|
describe('parseAuthorizationInput', () => {
|
|
it('should parse full OAuth callback URL', () => {
|
|
const input = 'http://localhost:1455/auth/callback?code=abc123&state=xyz789';
|
|
const result = parseAuthorizationInput(input);
|
|
expect(result).toEqual({ code: 'abc123', state: 'xyz789' });
|
|
});
|
|
|
|
it('should parse code#state format', () => {
|
|
const input = 'abc123#xyz789';
|
|
const result = parseAuthorizationInput(input);
|
|
expect(result).toEqual({ code: 'abc123', state: 'xyz789' });
|
|
});
|
|
|
|
it('should parse query string format', () => {
|
|
const input = 'code=abc123&state=xyz789';
|
|
const result = parseAuthorizationInput(input);
|
|
expect(result).toEqual({ code: 'abc123', state: 'xyz789' });
|
|
});
|
|
|
|
it('should parse code only', () => {
|
|
const input = 'abc123';
|
|
const result = parseAuthorizationInput(input);
|
|
expect(result).toEqual({ code: 'abc123' });
|
|
});
|
|
|
|
it('should return empty object for empty input', () => {
|
|
const result = parseAuthorizationInput('');
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it('should handle whitespace', () => {
|
|
const result = parseAuthorizationInput(' ');
|
|
expect(result).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('decodeJWT', () => {
|
|
it('should decode valid JWT token', () => {
|
|
// Create a simple JWT token: header.payload.signature
|
|
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64');
|
|
const payload = Buffer.from(JSON.stringify({ sub: '1234567890', name: 'Test User' })).toString('base64');
|
|
const signature = 'fake-signature';
|
|
const token = `${header}.${payload}.${signature}`;
|
|
|
|
const decoded = decodeJWT(token);
|
|
expect(decoded).toEqual({ sub: '1234567890', name: 'Test User' });
|
|
});
|
|
|
|
it('should decode JWT with ChatGPT account info', () => {
|
|
const payload = Buffer.from(JSON.stringify({
|
|
'https://api.openai.com/auth': {
|
|
chatgpt_account_id: 'account-123',
|
|
},
|
|
})).toString('base64');
|
|
const token = `header.${payload}.signature`;
|
|
|
|
const decoded = decodeJWT(token);
|
|
expect(decoded?.['https://api.openai.com/auth']?.chatgpt_account_id).toBe('account-123');
|
|
});
|
|
|
|
it('should return null for invalid JWT', () => {
|
|
const result = decodeJWT('invalid-token');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return null for malformed JWT', () => {
|
|
const result = decodeJWT('header.payload');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return null for non-JSON payload', () => {
|
|
const token = 'header.not-json.signature';
|
|
const result = decodeJWT(token);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('createAuthorizationFlow', () => {
|
|
it('should create authorization flow with PKCE', async () => {
|
|
const flow = await createAuthorizationFlow();
|
|
|
|
expect(flow).toHaveProperty('pkce');
|
|
expect(flow).toHaveProperty('state');
|
|
expect(flow).toHaveProperty('url');
|
|
|
|
expect(flow.pkce).toHaveProperty('challenge');
|
|
expect(flow.pkce).toHaveProperty('verifier');
|
|
expect(flow.state).toMatch(/^[a-f0-9]{32}$/);
|
|
});
|
|
|
|
it('should generate URL with correct parameters', async () => {
|
|
const flow = await createAuthorizationFlow();
|
|
const url = new URL(flow.url);
|
|
|
|
expect(url.origin + url.pathname).toBe(AUTHORIZE_URL);
|
|
expect(url.searchParams.get('response_type')).toBe('code');
|
|
expect(url.searchParams.get('client_id')).toBe(CLIENT_ID);
|
|
expect(url.searchParams.get('redirect_uri')).toBe(REDIRECT_URI);
|
|
expect(url.searchParams.get('scope')).toBe(SCOPE);
|
|
expect(url.searchParams.get('code_challenge_method')).toBe('S256');
|
|
expect(url.searchParams.get('code_challenge')).toBe(flow.pkce.challenge);
|
|
expect(url.searchParams.get('state')).toBe(flow.state);
|
|
expect(url.searchParams.get('id_token_add_organizations')).toBe('true');
|
|
expect(url.searchParams.get('codex_cli_simplified_flow')).toBe('true');
|
|
expect(url.searchParams.get('originator')).toBe('codex_cli_rs');
|
|
});
|
|
|
|
it('should generate unique flows', async () => {
|
|
const flow1 = await createAuthorizationFlow();
|
|
const flow2 = await createAuthorizationFlow();
|
|
|
|
expect(flow1.state).not.toBe(flow2.state);
|
|
expect(flow1.pkce.verifier).not.toBe(flow2.pkce.verifier);
|
|
expect(flow1.url).not.toBe(flow2.url);
|
|
});
|
|
});
|
|
});
|