Files
opencode-openai-codex-auth/lib/oauth-success.html
T
Numman AliandHanzo Dev 7dae285752 feat: Major TypeScript rewrite with enhanced configuration and OAuth UI
## 🎯 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]>
2025-10-03 21:51:59 +01:00

713 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenCode - Authentication Successful</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "IBM Plex Mono", monospace;
font-weight: 400;
background: #0a0a0a;
color: #B7B1B1;
overflow: hidden;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
/* Matrix rain background */
.matrix {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
opacity: 0.35;
}
/* Scan lines effect */
.scanline {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
transparent 50%,
rgba(0, 0, 0, 0.1) 50%
);
background-size: 100% 4px;
z-index: 2;
pointer-events: none;
animation: scan 8s linear infinite;
}
@keyframes scan {
0% { transform: translateY(0); }
100% { transform: translateY(4px); }
}
/* Vignette effect */
.vignette {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: inset 0 0 200px rgba(0, 0, 0, 0.8);
z-index: 2;
pointer-events: none;
}
.container {
width: 90%;
max-width: 650px;
text-align: center;
z-index: 10;
position: relative;
animation: fadeIn 0.8s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.logo {
margin-bottom: 60px;
opacity: 0;
animation: logoEntrance 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) 0.3s forwards;
position: relative;
}
@keyframes logoEntrance {
0% {
opacity: 0;
transform: translateY(-50px) rotateX(90deg);
}
100% {
opacity: 1;
transform: translateY(0) rotateX(0);
}
}
.logo svg {
width: 300px;
height: auto;
filter: drop-shadow(0 0 30px rgba(241, 236, 236, 0.3));
animation: pulse 3s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
filter: drop-shadow(0 0 30px rgba(241, 236, 236, 0.3));
}
50% {
filter: drop-shadow(0 0 50px rgba(241, 236, 236, 0.5));
}
}
/* Glowing orb behind logo */
.logo::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(241, 236, 236, 0.1) 0%, transparent 70%);
border-radius: 50%;
animation: orbPulse 4s ease-in-out infinite;
z-index: -1;
}
@keyframes orbPulse {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.3;
}
50% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 0.6;
}
}
.terminal-window {
background: rgba(26, 26, 26, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(75, 70, 70, 0.3);
border-radius: 12px;
margin: 30px 0;
opacity: 0;
animation: boxEntrance 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 0.6s forwards;
position: relative;
overflow: hidden;
box-shadow:
0 0 60px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.terminal-header {
background: rgba(15, 15, 15, 0.9);
padding: 14px 18px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid rgba(75, 70, 70, 0.3);
position: relative;
}
.terminal-button {
width: 12px;
height: 12px;
border-radius: 50%;
transition: transform 0.2s;
}
.terminal-button:hover {
transform: scale(1.2);
}
.terminal-button.red {
background: #ff5f56;
box-shadow: 0 0 10px rgba(255, 95, 86, 0.3);
animation: pulse 2s infinite;
}
.terminal-button.yellow {
background: #ffbd2e;
box-shadow: 0 0 10px rgba(255, 189, 46, 0.3);
animation: pulse 2s infinite 0.3s;
}
.terminal-button.green {
background: #27c93f;
box-shadow: 0 0 10px rgba(39, 201, 63, 0.3);
animation: pulse 2s infinite 0.6s;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.terminal-title {
flex: 1;
text-align: center;
font-size: 12px;
color: #6a6565;
opacity: 0.8;
letter-spacing: 0.5px;
}
.status-box {
padding: 50px 40px;
position: relative;
}
@keyframes boxEntrance {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Animated border gradient */
.terminal-window::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(
45deg,
transparent,
rgba(241, 236, 236, 0.1),
transparent,
rgba(241, 236, 236, 0.1)
);
background-size: 400% 400%;
border-radius: 12px;
z-index: -1;
animation: gradientShift 8s ease infinite;
}
@keyframes gradientShift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.checkmark-container {
width: 100px;
height: 100px;
margin: 0 auto 40px;
position: relative;
}
.checkmark-circle {
width: 100px;
height: 100px;
border-radius: 50%;
border: 4px solid transparent;
display: flex;
align-items: center;
justify-content: center;
position: relative;
animation: circleAppear 0.6s ease-out 1s both;
}
@keyframes circleAppear {
from {
transform: scale(0) rotate(-180deg);
border-color: transparent;
}
to {
transform: scale(1) rotate(0);
border-color: #4B4646;
}
}
/* Spinning rings around checkmark */
.checkmark-circle::before,
.checkmark-circle::after {
content: '';
position: absolute;
width: 120%;
height: 120%;
border: 2px solid transparent;
border-top-color: rgba(241, 236, 236, 0.2);
border-radius: 50%;
animation: rotate 3s linear infinite;
}
.checkmark-circle::after {
width: 140%;
height: 140%;
border-top-color: rgba(183, 177, 177, 0.1);
animation: rotate 4s linear infinite reverse;
}
@keyframes rotate {
to { transform: rotate(360deg); }
}
.checkmark {
width: 35px;
height: 18px;
border-left: 5px solid #F1ECEC;
border-bottom: 5px solid #F1ECEC;
transform: rotate(-45deg) scale(0);
animation: checkmarkDraw 0.5s cubic-bezier(0.65, 0, 0.45, 1) 1.3s forwards;
filter: drop-shadow(0 0 10px rgba(241, 236, 236, 0.5));
}
@keyframes checkmarkDraw {
0% {
transform: rotate(-45deg) scale(0);
}
50% {
transform: rotate(-45deg) scale(1.1);
}
100% {
transform: rotate(-45deg) scale(1);
}
}
.title {
font-size: 32px;
color: #00ff41;
margin-bottom: 20px;
font-weight: 500;
letter-spacing: 1px;
text-shadow: 0 0 20px rgba(0, 255, 65, 0.5);
opacity: 0;
animation: fadeInUp 0.6s ease-out 1.8s forwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message {
font-size: 16px;
color: #9a9595;
line-height: 1.8;
margin-bottom: 30px;
opacity: 0;
animation: fadeInUp 0.6s ease-out 2s forwards;
}
.terminal-output {
background: rgba(10, 10, 10, 0.6);
border: 1px solid rgba(75, 70, 70, 0.2);
border-radius: 8px;
padding: 25px;
text-align: left;
font-size: 14px;
line-height: 2;
margin-top: 25px;
position: relative;
overflow: hidden;
}
/* Terminal typing effect */
.terminal-output::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #4B4646, transparent);
animation: scanProgress 2s ease-out 2.2s forwards;
}
@keyframes scanProgress {
from {
transform: translateX(-100%);
}
to {
transform: translateX(100%);
}
}
.terminal-line {
opacity: 0;
transform: translateX(-20px);
animation: terminalType 0.4s ease-out forwards;
display: flex;
align-items: center;
gap: 12px;
}
.terminal-line:nth-child(1) { animation-delay: 2.2s; }
.terminal-line:nth-child(2) { animation-delay: 2.4s; }
.terminal-line:nth-child(3) { animation-delay: 2.6s; }
.terminal-line:nth-child(4) { animation-delay: 2.8s; }
@keyframes terminalType {
to {
opacity: 1;
transform: translateX(0);
}
}
.prompt {
color: #6a6565;
font-weight: 500;
}
.success {
color: #B7B1B1;
}
.terminal-line::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, rgba(75, 70, 70, 0.3), transparent);
}
.footer {
margin-top: 40px;
font-size: 14px;
color: #6a6565;
opacity: 0;
animation: fadeInUp 0.6s ease-out 3.2s forwards;
text-shadow: 0 0 10px rgba(106, 101, 101, 0.5);
}
.cursor {
display: inline-block;
width: 2px;
height: 16px;
background: #8a8585;
margin-left: 6px;
animation: blink 1s infinite;
box-shadow: 0 0 8px #8a8585;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* Floating particles */
.particle {
position: fixed;
width: 2px;
height: 2px;
background: rgba(183, 177, 177, 0.3);
border-radius: 50%;
pointer-events: none;
z-index: 5;
animation: float 8s linear infinite;
}
@keyframes float {
0% {
transform: translateY(100vh) translateX(0) scale(0);
opacity: 0;
}
10% {
opacity: 1;
transform: translateY(90vh) translateX(10px) scale(1);
}
90% {
opacity: 1;
}
100% {
transform: translateY(-10vh) translateX(-10px) scale(0);
opacity: 0;
}
}
/* Success glow effect */
@keyframes successGlow {
0%, 100% {
box-shadow:
0 0 60px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
50% {
box-shadow:
0 0 80px rgba(241, 236, 236, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
}
.terminal-window {
animation:
boxEntrance 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 0.6s forwards,
successGlow 4s ease-in-out 2s infinite;
}
</style>
</head>
<body>
<canvas class="matrix" id="matrix"></canvas>
<div class="scanline"></div>
<div class="vignette"></div>
<div class="container">
<div class="logo">
<svg width="300" height="54" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 30H6V18H18V30Z" fill="#4B4646"/>
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#B7B1B1"/>
<path d="M48 30H36V18H48V30Z" fill="#4B4646"/>
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#B7B1B1"/>
<path d="M84 24V30H66V24H84Z" fill="#4B4646"/>
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#B7B1B1"/>
<path d="M108 36H96V18H108V36Z" fill="#4B4646"/>
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#B7B1B1"/>
<path d="M144 30H126V18H144V30Z" fill="#4B4646"/>
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#F1ECEC"/>
<path d="M168 30H156V18H168V30Z" fill="#4B4646"/>
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#F1ECEC"/>
<path d="M198 30H186V18H198V30Z" fill="#4B4646"/>
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#F1ECEC"/>
<path d="M234 24V30H216V24H234Z" fill="#4B4646"/>
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#F1ECEC"/>
</svg>
</div>
<div class="terminal-window">
<div class="terminal-header">
<div class="terminal-button red"></div>
<div class="terminal-button yellow"></div>
<div class="terminal-button green"></div>
<div class="terminal-title">[email protected] β€” OAuth Authentication</div>
</div>
<div class="status-box">
<div class="checkmark-container">
<div class="checkmark-circle">
<div class="checkmark"></div>
</div>
</div>
<div class="title">ACCESS GRANTED</div>
<div class="message">
OpenAI ChatGPT authentication successful<br>
Your credentials have been securely stored
</div>
<div class="terminal-output">
<div class="terminal-line">
<span class="prompt">β–Έ</span>
<span class="success">Initializing OAuth handshake...</span>
</div>
<div class="terminal-line">
<span class="prompt">β–Έ</span>
<span class="success">Token exchange verified</span>
</div>
<div class="terminal-line">
<span class="prompt">β–Έ</span>
<span class="success">Credentials encrypted & stored</span>
</div>
<div class="terminal-line">
<span class="prompt">βœ“</span>
<span class="success">OpenCode ready for deployment</span>
</div>
</div>
</div>
</div>
<div class="footer">
Return to your terminal to continue<span class="cursor"></span>
</div>
</div>
<script>
// Enhanced Matrix rain effect with mouse interaction
const canvas = document.getElementById('matrix');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Mix of characters including OpenCode themed ones
const chars = 'OPENCODEβ–Έβœ“β–Ήβ–Έβ—‚β—ƒβ—„β–Ίβ—€β–Άβ†β†’β†‘β†“βŸ¨βŸ©βˆ§βˆ¨βˆ©βˆͺβŠ‚βŠƒβŠ†βŠ‡βˆˆβˆ‰βˆ€βˆƒβˆ„βˆ…βˆžβ‰‘β‰ β‰€β‰₯Β±βˆ“βŠ•βŠ—βŠ™01';
const fontSize = 16;
const columns = Math.floor(canvas.width / fontSize);
const drops = Array(columns).fill(1);
const speeds = Array(columns).fill(0).map(() => Math.random() * 0.5 + 0.5);
// Track mouse position
let mouseX = -1000;
let mouseY = -1000;
canvas.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
canvas.addEventListener('mouseleave', () => {
mouseX = -1000;
mouseY = -1000;
});
function drawMatrix() {
ctx.fillStyle = 'rgba(10, 10, 10, 0.08)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = fontSize + 'px "IBM Plex Mono"';
for (let i = 0; i < drops.length; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
const x = i * fontSize;
const y = drops[i] * fontSize;
// Check distance from mouse
const distanceFromMouse = Math.sqrt(
Math.pow(x - mouseX, 2) + Math.pow(y - mouseY, 2)
);
const hoverRadius = 80;
const isNearMouse = distanceFromMouse < hoverRadius;
if (isNearMouse) {
// Matrix green color when near mouse
const intensity = 1 - (distanceFromMouse / hoverRadius);
ctx.fillStyle = `rgba(0, 255, 65, ${intensity * 0.95})`;
ctx.shadowBlur = 20 * intensity;
ctx.shadowColor = '#00ff41';
} else {
// Bright white gradient by default
const gradient = ctx.createLinearGradient(0, y - fontSize * 10, 0, y);
gradient.addColorStop(0, 'rgba(200, 200, 200, 0)');
gradient.addColorStop(0.5, 'rgba(241, 236, 236, 0.8)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 1)');
ctx.fillStyle = gradient;
ctx.shadowBlur = 0;
}
ctx.fillText(text, x, y);
// Randomly reset drops
if (drops[i] * fontSize > canvas.height && Math.random() > 0.98) {
drops[i] = 0;
speeds[i] = Math.random() * 0.5 + 0.5;
}
drops[i] += speeds[i];
}
// Reset shadow for next frame
ctx.shadowBlur = 0;
}
setInterval(drawMatrix, 40);
// Create floating particles
function createParticle() {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = Math.random() * 100 + '%';
particle.style.animationDelay = Math.random() * 8 + 's';
particle.style.animationDuration = (Math.random() * 4 + 6) + 's';
document.body.appendChild(particle);
setTimeout(() => particle.remove(), 10000);
}
// Generate particles periodically
for (let i = 0; i < 20; i++) {
setTimeout(createParticle, i * 400);
}
setInterval(() => {
if (Math.random() > 0.7) createParticle();
}, 2000);
// Resize handler
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
// Add subtle mouse parallax effect
document.addEventListener('mousemove', (e) => {
const moveX = (e.clientX - window.innerWidth / 2) / 50;
const moveY = (e.clientY - window.innerHeight / 2) / 50;
document.querySelector('.container').style.transform =
`translate(${moveX}px, ${moveY}px)`;
});
</script>
</body>
</html>