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]>
125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import {
|
|
shouldRefreshToken,
|
|
extractRequestUrl,
|
|
rewriteUrlForCodex,
|
|
createCodexHeaders,
|
|
} from '../lib/fetch-helpers.js';
|
|
import type { Auth } from '../lib/types.js';
|
|
import { URL_PATHS, OPENAI_HEADERS, OPENAI_HEADER_VALUES } from '../lib/constants.js';
|
|
|
|
describe('Fetch Helpers Module', () => {
|
|
describe('shouldRefreshToken', () => {
|
|
it('should return true for non-oauth auth', () => {
|
|
const auth: Auth = { type: 'api', key: 'test-key' };
|
|
expect(shouldRefreshToken(auth)).toBe(true);
|
|
});
|
|
|
|
it('should return true when access token is missing', () => {
|
|
const auth: Auth = { type: 'oauth', access: '', refresh: 'refresh-token', expires: Date.now() + 1000 };
|
|
expect(shouldRefreshToken(auth)).toBe(true);
|
|
});
|
|
|
|
it('should return true when token is expired', () => {
|
|
const auth: Auth = {
|
|
type: 'oauth',
|
|
access: 'access-token',
|
|
refresh: 'refresh-token',
|
|
expires: Date.now() - 1000 // expired
|
|
};
|
|
expect(shouldRefreshToken(auth)).toBe(true);
|
|
});
|
|
|
|
it('should return false for valid oauth token', () => {
|
|
const auth: Auth = {
|
|
type: 'oauth',
|
|
access: 'access-token',
|
|
refresh: 'refresh-token',
|
|
expires: Date.now() + 10000 // valid for 10 seconds
|
|
};
|
|
expect(shouldRefreshToken(auth)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('extractRequestUrl', () => {
|
|
it('should extract URL from string', () => {
|
|
const url = 'https://example.com/test';
|
|
expect(extractRequestUrl(url)).toBe(url);
|
|
});
|
|
|
|
it('should extract URL from URL object', () => {
|
|
const url = new URL('https://example.com/test');
|
|
expect(extractRequestUrl(url)).toBe('https://example.com/test');
|
|
});
|
|
|
|
it('should extract URL from Request object', () => {
|
|
const request = new Request('https://example.com/test');
|
|
expect(extractRequestUrl(request)).toBe('https://example.com/test');
|
|
});
|
|
});
|
|
|
|
describe('rewriteUrlForCodex', () => {
|
|
it('should rewrite /responses to /codex/responses', () => {
|
|
const url = 'https://chatgpt.com/backend-api/responses';
|
|
expect(rewriteUrlForCodex(url)).toBe('https://chatgpt.com/backend-api/codex/responses');
|
|
});
|
|
|
|
it('should not modify URL without /responses', () => {
|
|
const url = 'https://chatgpt.com/backend-api/other';
|
|
expect(rewriteUrlForCodex(url)).toBe(url);
|
|
});
|
|
|
|
it('should only replace first occurrence', () => {
|
|
const url = 'https://example.com/responses/responses';
|
|
const result = rewriteUrlForCodex(url);
|
|
expect(result).toBe('https://example.com/codex/responses/responses');
|
|
});
|
|
});
|
|
|
|
describe('createCodexHeaders', () => {
|
|
const accountId = 'test-account-123';
|
|
const accessToken = 'test-access-token';
|
|
|
|
it('should create headers with all required fields', () => {
|
|
const headers = createCodexHeaders(undefined, accountId, accessToken);
|
|
|
|
expect(headers.get('Authorization')).toBe(`Bearer ${accessToken}`);
|
|
expect(headers.get(OPENAI_HEADERS.ACCOUNT_ID)).toBe(accountId);
|
|
expect(headers.get(OPENAI_HEADERS.BETA)).toBe(OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
expect(headers.get(OPENAI_HEADERS.ORIGINATOR)).toBe(OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
expect(headers.has(OPENAI_HEADERS.SESSION_ID)).toBe(true);
|
|
});
|
|
|
|
it('should remove x-api-key header', () => {
|
|
const init = { headers: { 'x-api-key': 'should-be-removed' } };
|
|
const headers = createCodexHeaders(init, accountId, accessToken);
|
|
|
|
expect(headers.has('x-api-key')).toBe(false);
|
|
});
|
|
|
|
it('should preserve other existing headers', () => {
|
|
const init = { headers: { 'Content-Type': 'application/json' } };
|
|
const headers = createCodexHeaders(init, accountId, accessToken);
|
|
|
|
expect(headers.get('Content-Type')).toBe('application/json');
|
|
});
|
|
|
|
it('should generate unique session IDs', () => {
|
|
const headers1 = createCodexHeaders(undefined, accountId, accessToken);
|
|
const headers2 = createCodexHeaders(undefined, accountId, accessToken);
|
|
|
|
expect(headers1.get(OPENAI_HEADERS.SESSION_ID)).not.toBe(
|
|
headers2.get(OPENAI_HEADERS.SESSION_ID)
|
|
);
|
|
});
|
|
|
|
it('should validate session ID format (UUID)', () => {
|
|
const headers = createCodexHeaders(undefined, accountId, accessToken);
|
|
const sessionId = headers.get(OPENAI_HEADERS.SESSION_ID);
|
|
|
|
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
});
|
|
});
|
|
});
|