From 3857598de4ef88bda862314f6cbc89e37280de3e Mon Sep 17 00:00:00 2001 From: decolua Date: Mon, 5 Jan 2026 09:58:59 +0700 Subject: [PATCH] Initial commit --- .gitignore | 56 ++ .npmignore | 60 ++ README.md | 155 ++++ eslint.config.mjs | 16 + images/9router.png | Bin 0 -> 299457 bytes jsconfig.json | 12 + next.config.mjs | 44 + package.json | 38 + postcss.config.mjs | 6 + public/favicon.svg | 11 + public/file.svg | 1 + public/globe.svg | 1 + public/next.svg | 1 + public/providers/antigravity.png | Bin 0 -> 14976 bytes public/providers/claude.png | Bin 0 -> 5351 bytes public/providers/cline.png | Bin 0 -> 9538 bytes public/providers/codex.png | Bin 0 -> 3525 bytes public/providers/continue.png | Bin 0 -> 31909 bytes public/providers/copilot.png | Bin 0 -> 11578 bytes public/providers/cursor.png | Bin 0 -> 4015 bytes public/providers/gemini-cli.png | Bin 0 -> 46696 bytes public/providers/github.png | Bin 0 -> 11578 bytes public/providers/iflow.png | Bin 0 -> 2439 bytes public/providers/qwen.png | Bin 0 -> 53565 bytes public/providers/roo.png | Bin 0 -> 14079 bytes public/vercel.svg | 1 + public/window.svg | 1 + scripts/prepare-standalone.js | 65 ++ .../dashboard/cli-tools/CLIToolsPageClient.js | 193 +++++ .../cli-tools/components/ClaudeToolCard.js | 320 ++++++++ .../cli-tools/components/CodexToolCard.js | 307 +++++++ .../cli-tools/components/DefaultToolCard.js | 284 +++++++ .../dashboard/cli-tools/components/index.js | 4 + .../(dashboard)/dashboard/cli-tools/page.js | 7 + src/app/(dashboard)/dashboard/combos/page.js | 434 ++++++++++ .../dashboard/endpoint/EndpointPageClient.js | 605 ++++++++++++++ .../(dashboard)/dashboard/endpoint/page.js | 7 + src/app/(dashboard)/dashboard/page.js | 7 + src/app/(dashboard)/dashboard/profile/page.js | 95 +++ .../dashboard/providers/[id]/page.js | 764 ++++++++++++++++++ .../dashboard/providers/new/page.js | 220 +++++ .../(dashboard)/dashboard/providers/page.js | 235 ++++++ src/app/(dashboard)/layout.js | 6 + .../api/cli-tools/claude-settings/route.js | 187 +++++ src/app/api/cli-tools/codex-settings/route.js | 246 ++++++ src/app/api/cloud/auth/route.js | 50 ++ src/app/api/cloud/credentials/update/route.js | 57 ++ src/app/api/cloud/model/resolve/route.js | 50 ++ src/app/api/cloud/models/alias/route.js | 92 +++ src/app/api/combos/[id]/route.js | 94 +++ src/app/api/combos/route.js | 66 ++ src/app/api/init/route.js | 7 + src/app/api/keys/[id]/route.js | 39 + src/app/api/keys/route.js | 59 ++ src/app/api/models/alias/route.js | 83 ++ src/app/api/models/route.js | 55 ++ .../api/oauth/[provider]/[action]/route.js | 187 +++++ src/app/api/providers/[id]/models/route.js | 148 ++++ src/app/api/providers/[id]/route.js | 102 +++ src/app/api/providers/[id]/test/route.js | 95 +++ src/app/api/providers/client/route.js | 20 + src/app/api/providers/route.js | 84 ++ src/app/api/providers/validate/route.js | 96 +++ src/app/api/settings/route.js | 12 + src/app/api/shutdown/route.js | 12 + src/app/api/sync/cloud/route.js | 255 ++++++ src/app/api/sync/initialize/route.js | 36 + src/app/api/tags/route.js | 18 + src/app/api/usage/[connectionId]/route.js | 30 + src/app/api/v1/api/chat/route.js | 38 + src/app/api/v1/chat/completions/route.js | 37 + src/app/api/v1/messages/count_tokens/route.js | 52 ++ src/app/api/v1/messages/route.js | 37 + src/app/api/v1/responses/route.js | 31 + src/app/api/v1/route.js | 32 + src/app/api/v1beta/models/[...path]/route.js | 113 +++ src/app/api/v1beta/models/route.js | 44 + src/app/callback/page.js | 142 ++++ src/app/favicon.ico | Bin 0 -> 25931 bytes src/app/globals.css | 169 ++++ .../landing/components/AnimatedBackground.js | 57 ++ src/app/landing/components/Features.js | 133 +++ src/app/landing/components/FlowAnimation.js | 120 +++ src/app/landing/components/Footer.js | 61 ++ src/app/landing/components/GetStarted.js | 99 +++ src/app/landing/components/HeroSection.js | 47 ++ src/app/landing/components/HowItWorks.js | 66 ++ src/app/landing/components/Navigation.js | 67 ++ src/app/landing/page.js | 104 +++ src/app/layout.js | 33 + src/app/page.js | 7 + src/lib/initCloudSync.js | 22 + src/lib/localDb.js | 496 ++++++++++++ src/lib/oauth/constants/oauth.js | 126 +++ src/lib/oauth/providers.js | 619 ++++++++++++++ src/lib/oauth/services/antigravity.js | 239 ++++++ src/lib/oauth/services/claude.js | 136 ++++ src/lib/oauth/services/codex.js | 145 ++++ src/lib/oauth/services/gemini.js | 247 ++++++ src/lib/oauth/services/github.js | 225 ++++++ src/lib/oauth/services/iflow.js | 202 +++++ src/lib/oauth/services/index.js | 14 + src/lib/oauth/services/oauth.js | 157 ++++ src/lib/oauth/services/openai.js | 123 +++ src/lib/oauth/services/qwen.js | 170 ++++ src/lib/oauth/utils/banner.js | 63 ++ src/lib/oauth/utils/pkce.js | 38 + src/lib/oauth/utils/server.js | 116 +++ src/lib/oauth/utils/ui.js | 48 ++ src/lib/usage/fetcher.js | 202 +++++ src/models/index.js | 16 + src/server-init.js | 21 + src/shared/components/Avatar.js | 88 ++ src/shared/components/Badge.js | 55 ++ src/shared/components/Button.js | 56 ++ src/shared/components/Card.js | 92 +++ src/shared/components/Footer.js | 132 +++ src/shared/components/Header.js | 109 +++ src/shared/components/Input.js | 69 ++ src/shared/components/Loading.js | 77 ++ src/shared/components/Modal.js | 136 ++++ src/shared/components/ModelSelectModal.js | 183 +++++ src/shared/components/OAuthModal.js | 419 ++++++++++ src/shared/components/Select.js | 70 ++ src/shared/components/Sidebar.js | 171 ++++ src/shared/components/ThemeProvider.js | 15 + src/shared/components/ThemeToggle.js | 47 ++ src/shared/components/Toggle.js | 91 +++ src/shared/components/index.js | 21 + src/shared/components/layouts/AuthLayout.js | 24 + .../components/layouts/DashboardLayout.js | 43 + src/shared/components/layouts/index.js | 4 + src/shared/constants/cliTools.js | 142 ++++ src/shared/constants/colors.js | 77 ++ src/shared/constants/config.js | 53 ++ src/shared/constants/index.js | 4 + src/shared/constants/models.js | 34 + src/shared/constants/providers.js | 65 ++ src/shared/hooks/index.js | 2 + src/shared/hooks/useCopyToClipboard.js | 29 + src/shared/hooks/useTheme.js | 31 + src/shared/services/cloudSyncScheduler.js | 117 +++ src/shared/services/initializeCloudSync.js | 32 + src/shared/utils/api.js | 92 +++ src/shared/utils/apiKey.js | 98 +++ src/shared/utils/cloud.js | 40 + src/shared/utils/cn.js | 11 + src/shared/utils/index.js | 32 + src/shared/utils/machine.js | 18 + src/shared/utils/machineId.js | 58 ++ src/sse/handlers/chat.js | 134 +++ src/sse/services/auth.js | 106 +++ src/sse/services/model.js | 35 + src/sse/services/tokenRefresh.js | 173 ++++ src/sse/utils/logger.js | 75 ++ src/store/index.js | 5 + src/store/providerStore.js | 48 ++ src/store/themeStore.js | 54 ++ src/store/userStore.js | 20 + 159 files changed, 14537 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100644 eslint.config.mjs create mode 100644 images/9router.png create mode 100644 jsconfig.json create mode 100644 next.config.mjs create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/favicon.svg create mode 100644 public/file.svg create mode 100644 public/globe.svg create mode 100644 public/next.svg create mode 100644 public/providers/antigravity.png create mode 100644 public/providers/claude.png create mode 100644 public/providers/cline.png create mode 100644 public/providers/codex.png create mode 100644 public/providers/continue.png create mode 100644 public/providers/copilot.png create mode 100644 public/providers/cursor.png create mode 100644 public/providers/gemini-cli.png create mode 100644 public/providers/github.png create mode 100644 public/providers/iflow.png create mode 100644 public/providers/qwen.png create mode 100644 public/providers/roo.png create mode 100644 public/vercel.svg create mode 100644 public/window.svg create mode 100644 scripts/prepare-standalone.js create mode 100644 src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js create mode 100644 src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js create mode 100644 src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js create mode 100644 src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js create mode 100644 src/app/(dashboard)/dashboard/cli-tools/components/index.js create mode 100644 src/app/(dashboard)/dashboard/cli-tools/page.js create mode 100644 src/app/(dashboard)/dashboard/combos/page.js create mode 100644 src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js create mode 100644 src/app/(dashboard)/dashboard/endpoint/page.js create mode 100644 src/app/(dashboard)/dashboard/page.js create mode 100644 src/app/(dashboard)/dashboard/profile/page.js create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/page.js create mode 100644 src/app/(dashboard)/dashboard/providers/new/page.js create mode 100644 src/app/(dashboard)/dashboard/providers/page.js create mode 100644 src/app/(dashboard)/layout.js create mode 100644 src/app/api/cli-tools/claude-settings/route.js create mode 100644 src/app/api/cli-tools/codex-settings/route.js create mode 100644 src/app/api/cloud/auth/route.js create mode 100644 src/app/api/cloud/credentials/update/route.js create mode 100644 src/app/api/cloud/model/resolve/route.js create mode 100644 src/app/api/cloud/models/alias/route.js create mode 100644 src/app/api/combos/[id]/route.js create mode 100644 src/app/api/combos/route.js create mode 100644 src/app/api/init/route.js create mode 100644 src/app/api/keys/[id]/route.js create mode 100644 src/app/api/keys/route.js create mode 100644 src/app/api/models/alias/route.js create mode 100644 src/app/api/models/route.js create mode 100644 src/app/api/oauth/[provider]/[action]/route.js create mode 100644 src/app/api/providers/[id]/models/route.js create mode 100644 src/app/api/providers/[id]/route.js create mode 100644 src/app/api/providers/[id]/test/route.js create mode 100644 src/app/api/providers/client/route.js create mode 100644 src/app/api/providers/route.js create mode 100644 src/app/api/providers/validate/route.js create mode 100644 src/app/api/settings/route.js create mode 100644 src/app/api/shutdown/route.js create mode 100644 src/app/api/sync/cloud/route.js create mode 100644 src/app/api/sync/initialize/route.js create mode 100644 src/app/api/tags/route.js create mode 100644 src/app/api/usage/[connectionId]/route.js create mode 100644 src/app/api/v1/api/chat/route.js create mode 100644 src/app/api/v1/chat/completions/route.js create mode 100644 src/app/api/v1/messages/count_tokens/route.js create mode 100644 src/app/api/v1/messages/route.js create mode 100644 src/app/api/v1/responses/route.js create mode 100644 src/app/api/v1/route.js create mode 100644 src/app/api/v1beta/models/[...path]/route.js create mode 100644 src/app/api/v1beta/models/route.js create mode 100644 src/app/callback/page.js create mode 100644 src/app/favicon.ico create mode 100644 src/app/globals.css create mode 100644 src/app/landing/components/AnimatedBackground.js create mode 100644 src/app/landing/components/Features.js create mode 100644 src/app/landing/components/FlowAnimation.js create mode 100644 src/app/landing/components/Footer.js create mode 100644 src/app/landing/components/GetStarted.js create mode 100644 src/app/landing/components/HeroSection.js create mode 100644 src/app/landing/components/HowItWorks.js create mode 100644 src/app/landing/components/Navigation.js create mode 100644 src/app/landing/page.js create mode 100644 src/app/layout.js create mode 100644 src/app/page.js create mode 100644 src/lib/initCloudSync.js create mode 100644 src/lib/localDb.js create mode 100644 src/lib/oauth/constants/oauth.js create mode 100644 src/lib/oauth/providers.js create mode 100644 src/lib/oauth/services/antigravity.js create mode 100644 src/lib/oauth/services/claude.js create mode 100644 src/lib/oauth/services/codex.js create mode 100644 src/lib/oauth/services/gemini.js create mode 100644 src/lib/oauth/services/github.js create mode 100644 src/lib/oauth/services/iflow.js create mode 100644 src/lib/oauth/services/index.js create mode 100644 src/lib/oauth/services/oauth.js create mode 100644 src/lib/oauth/services/openai.js create mode 100644 src/lib/oauth/services/qwen.js create mode 100644 src/lib/oauth/utils/banner.js create mode 100644 src/lib/oauth/utils/pkce.js create mode 100644 src/lib/oauth/utils/server.js create mode 100644 src/lib/oauth/utils/ui.js create mode 100644 src/lib/usage/fetcher.js create mode 100644 src/models/index.js create mode 100644 src/server-init.js create mode 100644 src/shared/components/Avatar.js create mode 100644 src/shared/components/Badge.js create mode 100644 src/shared/components/Button.js create mode 100644 src/shared/components/Card.js create mode 100644 src/shared/components/Footer.js create mode 100644 src/shared/components/Header.js create mode 100644 src/shared/components/Input.js create mode 100644 src/shared/components/Loading.js create mode 100644 src/shared/components/Modal.js create mode 100644 src/shared/components/ModelSelectModal.js create mode 100644 src/shared/components/OAuthModal.js create mode 100644 src/shared/components/Select.js create mode 100644 src/shared/components/Sidebar.js create mode 100644 src/shared/components/ThemeProvider.js create mode 100644 src/shared/components/ThemeToggle.js create mode 100644 src/shared/components/Toggle.js create mode 100644 src/shared/components/index.js create mode 100644 src/shared/components/layouts/AuthLayout.js create mode 100644 src/shared/components/layouts/DashboardLayout.js create mode 100644 src/shared/components/layouts/index.js create mode 100644 src/shared/constants/cliTools.js create mode 100644 src/shared/constants/colors.js create mode 100644 src/shared/constants/config.js create mode 100644 src/shared/constants/index.js create mode 100644 src/shared/constants/models.js create mode 100644 src/shared/constants/providers.js create mode 100644 src/shared/hooks/index.js create mode 100644 src/shared/hooks/useCopyToClipboard.js create mode 100644 src/shared/hooks/useTheme.js create mode 100644 src/shared/services/cloudSyncScheduler.js create mode 100644 src/shared/services/initializeCloudSync.js create mode 100644 src/shared/utils/api.js create mode 100644 src/shared/utils/apiKey.js create mode 100644 src/shared/utils/cloud.js create mode 100644 src/shared/utils/cn.js create mode 100644 src/shared/utils/index.js create mode 100644 src/shared/utils/machine.js create mode 100644 src/shared/utils/machineId.js create mode 100644 src/sse/handlers/chat.js create mode 100644 src/sse/services/auth.js create mode 100644 src/sse/services/model.js create mode 100644 src/sse/services/tokenRefresh.js create mode 100644 src/sse/utils/logger.js create mode 100644 src/store/index.js create mode 100644 src/store/providerStore.js create mode 100644 src/store/themeStore.js create mode 100644 src/store/userStore.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7ff12a81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build +cloud/* + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.bin/* +data/ +logs/* +source/* +.cursor/* +docs/* +test/* +bin/* +open-sse/test/* +RM.vn.md +RM.md +cursor/* +stitch_router4_landing_page/* diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..9bec212d --- /dev/null +++ b/.npmignore @@ -0,0 +1,60 @@ +# Database files - NEVER publish +data/ +**/data/ +**/db.json + +# Development +src/ +docs/ +test/ +agents/ +scripts/ +worker/ +shared-sse/ +copilot-api/ +CLIProxyAPI/ + +# Config files +*.md +!README.md +.gitignore +.env* +jsconfig.json +eslint.config.mjs +postcss.config.mjs +next.config.mjs +tsconfig.json + +# Build artifacts that shouldn't be published +.next/cache/ +.next/standalone/data/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 00000000..061a8a9b --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# 🚀 9ROUTER + +[![npm version](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router) +[![License](https://img.shields.io/npm/l/9router.svg)](https://github.com/yourusername/9router/blob/main/LICENSE) + +AI endpoint proxy with web dashboard - A JavaScript port of [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI). + +![9Router Dashboard](https://github.com/decolua/9router/raw/main/images/9router.png) + +## 📖 Introduction + +**9Router** is a powerful AI API proxy server that provides unified access to multiple AI providers through a single endpoint. It features automatic format translation, intelligent fallback routing, OAuth authentication, and a modern web dashboard for easy management. + +**Key Highlights:** +- **JavaScript Port**: Converted from [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) (Go) to JavaScript/Node.js for better accessibility and easier deployment +- **Universal CLI Support**: Works seamlessly with Claude Code, OpenAI Codex, Cline, RooCode, AmpCode, and other CLI tools +- **Cross-Platform**: Runs on Windows, Linux, and macOS +- **Easy Deployment**: Simple installation via npx, or deploy to VPS + +## ✨ Features + +### Core Features +- **🔄 Multi-Provider Support**: Unified endpoint for 15+ AI providers (Claude, OpenAI, Gemini, GitHub Copilot, Qwen, iFlow, DeepSeek, Kimi, MiniMax, GLM, etc.) +- **🔐 OAuth & API Key Authentication**: Supports both OAuth2 flow and API key authentication +- **đŸŽ¯ Format Translation**: Automatic request/response translation between OpenAI, Claude, Gemini, Codex, and Ollama formats +- **🌐 Web Dashboard**: Beautiful React-based dashboard for managing providers, combos, API keys, and settings +- **📊 Usage Tracking**: Real-time monitoring and analytics for all API requests + +### Advanced Features +- **🎲 Combo System**: Create model combos with automatic fallback support +- **â™ģī¸ Intelligent Fallback**: Automatic account rotation when rate limits or errors occur +- **⚡ Response Caching**: Optimized caching for Claude Code (1-hour cache vs default 5 minutes) +- **🔧 Model Aliases**: Create custom model aliases for easier management +- **🔍 WebSearch Hook**: Model-based web search integration for Claude Code CLI +- **â˜ī¸ Cloud Deployment**: Deploy to Cloud for Cursor IDE integration with global edge performance + +### Format Support +- **OpenAI Format**: Standard OpenAI Chat Completions API +- **Claude Format**: Anthropic Messages API +- **Gemini Format**: Google Generative AI API +- **OpenAI Responses API**: Codex CLI format +- **Ollama Format**: Compatible with Ollama-based tools + +### CLI Integration +- Works with: Claude Code, OpenAI Codex, Cline, RooCode, AmpCode, Cursor, and more +- Seamless integration with popular AI coding assistants +- WebSearch hook for enhanced Claude Code capabilities + +## đŸ“Ļ Install + +```bash +# Run directly with npx (recommended) +npx 9router + +# Or install globally +npm install -g 9router +9router +``` + +## 🚀 Quick Start + +```bash +9router # Start server with default settings +9router --port 8080 # Custom port +9router --no-browser # Don't open browser +9router --skip-update # Skip auto-update check +9router --help # Show help +``` + +**Dashboard**: `http://localhost:20128/dashboard` + +## 💾 Data Location + +User data stored at: +- macOS/Linux: `~/.9router/db.json` +- Windows: `%APPDATA%/9router/db.json` + +## đŸ› ī¸ Development + +### Setup +```bash +# Clone repository +git clone https://github.com/yourusername/9router.git +cd 9router + +# Install dependencies +npm install + +# Start development server +npm run dev +``` + +### Project Structure +``` +9router/ +├── bin/ # CLI entry point +│ ├── cli.js # Main CLI script +│ └── hooks/ # CLI hooks (WebSearch) +├── src/ +│ ├── app/ # Next.js app (dashboard & API routes) +│ ├── lib/ # Core libraries (DB, OAuth, etc.) +│ ├── shared/ # Shared components & utilities +│ └── sse/ # SSE streaming handlers +├── open-sse/ # Core proxy engine (translator, handlers) +│ ├── translator/ # Format translators +│ ├── handlers/ # Request handlers +│ ├── services/ # Core services +│ └── config/ # Provider configurations +└── public/ # Static assets +``` + +## 📤 Build & Publish + +```bash +# Build standalone binary +npm run build:standalone + +# Test locally +npm link +9router --help + +# Publish to npm +npm login +npm publish +``` + +## 🧰 Tech Stack + +| Layer | Technology | +|-------|------------| +| **Runtime** | Node.js 20+ / Bun | +| **Framework** | Next.js 15 | +| **Dashboard** | React 19 + Tailwind CSS 4 | +| **Database** | LowDB (JSON file-based) | +| **CLI** | Node.js CLI with auto-update | +| **Streaming** | Server-Sent Events (SSE) | +| **Auth** | OAuth 2.0 (PKCE) + API Keys | +| **Deployment** | Standalone / VPS | +| **State Management** | Zustand | + +### Core Libraries +- **lowdb**: Lightweight JSON database +- **undici**: High-performance HTTP client +- **uuid**: Unique identifier generation +- **open**: Cross-platform browser launcher + +## 🙏 Acknowledgments + +Special thanks to: + +- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)**: The original Go implementation that inspired this project. 9Router is a JavaScript port with enhanced features and web dashboard. + +## 📄 License + +MIT License - see [LICENSE](LICENSE) for details. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..f4438352 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; + +const eslintConfig = defineConfig([ + ...nextVitals, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/images/9router.png b/images/9router.png new file mode 100644 index 0000000000000000000000000000000000000000..d8520db1384c896c6b7b362bc00e08c96cf0a8bd GIT binary patch literal 299457 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

u#du-MDTOtqc<{&>> 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 LG~|n9P5$|R^u}2_ literal 0 HcmV?d00001 diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..3d72db00 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "open-sse": ["../open-sse"], + "open-sse/*": ["../open-sse/*"] + }, + "module": "ESNext", + "moduleResolution": "bundler" + } +} diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 00000000..27204737 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,44 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "standalone", + env: { + NEXT_PUBLIC_CLOUD_URL: "https://9router.com", + }, + webpack: (config, { isServer }) => { + // Ignore fs/path modules in browser bundle + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + path: false, + }; + } + return config; + }, + async rewrites() { + return [ + { + source: "/v1/v1/:path*", + destination: "/api/v1/:path*" + }, + { + source: "/v1/v1", + destination: "/api/v1" + }, + { + source: "/codex/:path*", + destination: "/api/v1/responses" + }, + { + source: "/v1/:path*", + destination: "/api/v1/:path*" + }, + { + source: "/v1", + destination: "/api/v1" + } + ]; + } +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 00000000..e6ca09f3 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "9router-app", + "version": "0.2.13", + "description": "9Router web dashboard", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "build:standalone": "next build && node scripts/prepare-standalone.js", + "start": "next start", + "start:cli": "node bin/cli.js", + "lint": "eslint", + "prepublishOnly": "npm run build:standalone" + }, + "dependencies": { + "fs": "^0.0.1-security", + "lowdb": "^7.0.1", + "next": "^15.2.0", + "node-machine-id": "^1.1.12", + "open": "^10.1.0", + "open-sse": "^1.0.0", + "ora": "^5.4.1", + "react": "19.2.1", + "react-dom": "19.2.1", + "undici": "^7.16.0", + "uuid": "^13.0.0", + "zustand": "^5.0.9" + }, + "overrides": { + "open-sse": "file:../open-sse" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.18", + "eslint": "^9", + "eslint-config-next": "16.0.10", + "tailwindcss": "^4" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 00000000..fa4a1da8 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 00000000..a72e45be --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,11 @@ + + + 9 + + + + + + + + diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/providers/antigravity.png b/public/providers/antigravity.png new file mode 100644 index 0000000000000000000000000000000000000000..b567e54a404cf45755e8092cf0e3790191a9673e GIT binary patch literal 14976 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 K^Pk?SMGXMBF}nu< literal 0 HcmV?d00001 diff --git a/public/providers/claude.png b/public/providers/claude.png new file mode 100644 index 0000000000000000000000000000000000000000..5a38939ef7c7ba62ca61ab31d212529169b3a366 GIT binary patch literal 5351 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 FV1n+BOcDS9 literal 0 HcmV?d00001 diff --git a/public/providers/cline.png b/public/providers/cline.png new file mode 100644 index 0000000000000000000000000000000000000000..67161e92d455f4cf870d490e24427dae9c9febb1 GIT binary patch literal 9538 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 OmtYZsM^&WrZT=rZ4mA@1 literal 0 HcmV?d00001 diff --git a/public/providers/codex.png b/public/providers/codex.png new file mode 100644 index 0000000000000000000000000000000000000000..2f0ecb9757f22c1a80d34c4a958237da2b0f77cd GIT binary patch literal 3525 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 RTM6Z~{Dz2?2B*Ys3*y1&QZHLF|&6 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 literal 0 HcmV?d00001 diff --git a/public/providers/gemini-cli.png b/public/providers/gemini-cli.png new file mode 100644 index 0000000000000000000000000000000000000000..2f26055b035fa07d54b1959115720466371e9048 GIT binary patch literal 46696 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�gN)!E-q~ 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 literal 0 HcmV?d00001 diff --git a/public/providers/qwen.png b/public/providers/qwen.png new file mode 100644 index 0000000000000000000000000000000000000000..d773698ae28c64059947080b6ee849c38cefc6b0 GIT binary patch literal 53565 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 F_#fbx+id^< literal 0 HcmV?d00001 diff --git a/public/providers/roo.png b/public/providers/roo.png new file mode 100644 index 0000000000000000000000000000000000000000..582c0b7d16b226d5e81dbed292547e95f037d471 GIT binary patch literal 14079 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 \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/prepare-standalone.js b/scripts/prepare-standalone.js new file mode 100644 index 00000000..e17e7bb6 --- /dev/null +++ b/scripts/prepare-standalone.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +const projectRoot = path.resolve(__dirname, ".."); +const standaloneDir = path.join(projectRoot, ".next/standalone"); +const staticSrc = path.join(projectRoot, ".next/static"); +const staticDest = path.join(standaloneDir, ".next/static"); +const publicSrc = path.join(projectRoot, "public"); +const publicDest = path.join(standaloneDir, "public"); + +function copyRecursive(src, dest) { + if (!fs.existsSync(src)) return; + + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + const entries = fs.readdirSync(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyRecursive(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +console.log("Preparing standalone build..."); + +// Copy static files +if (fs.existsSync(staticSrc)) { + copyRecursive(staticSrc, staticDest); + console.log("✓ Copied .next/static"); +} + +// Copy public folder +if (fs.existsSync(publicSrc)) { + copyRecursive(publicSrc, publicDest); + console.log("✓ Copied public"); +} + +console.log("✓ Standalone build ready"); + + + + + + + + + + + + + + + + + + diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js new file mode 100644 index 00000000..8d930bf0 --- /dev/null +++ b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js @@ -0,0 +1,193 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardSkeleton } from "@/shared/components"; +import { CLI_TOOLS } from "@/shared/constants/cliTools"; +import { PROVIDER_MODELS, getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; +import { ClaudeToolCard, CodexToolCard, DefaultToolCard } from "./components"; + +const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; + +export default function CLIToolsPageClient({ machineId }) { + const [connections, setConnections] = useState([]); + const [loading, setLoading] = useState(true); + const [expandedTool, setExpandedTool] = useState(null); + const [modelMappings, setModelMappings] = useState({}); + const [cloudEnabled, setCloudEnabled] = useState(false); + const [apiKeys, setApiKeys] = useState([]); + + useEffect(() => { + fetchConnections(); + loadCloudSettings(); + fetchApiKeys(); + }, []); + + const loadCloudSettings = async () => { + try { + const res = await fetch("/api/settings"); + if (res.ok) { + const data = await res.json(); + setCloudEnabled(data.cloudEnabled || false); + } + } catch (error) { + console.log("Error loading cloud settings:", error); + } + }; + + const fetchApiKeys = async () => { + try { + const res = await fetch("/api/keys"); + if (res.ok) { + const data = await res.json(); + setApiKeys(data.keys || []); + } + } catch (error) { + console.log("Error fetching API keys:", error); + } + }; + + const fetchConnections = async () => { + try { + const res = await fetch("/api/providers"); + const data = await res.json(); + if (res.ok) { + setConnections(data.connections || []); + } + } catch (error) { + console.log("Error fetching connections:", error); + } finally { + setLoading(false); + } + }; + + const getActiveProviders = () => { + return connections.filter(c => c.isActive !== false); + }; + + const getAllAvailableModels = () => { + const activeProviders = getActiveProviders(); + const models = []; + const seenModels = new Set(); + + activeProviders.forEach(conn => { + const alias = PROVIDER_ID_TO_ALIAS[conn.provider] || conn.provider; + const providerModels = getModelsByProviderId(conn.provider); + providerModels.forEach(m => { + const modelValue = `${alias}/${m.id}`; + if (!seenModels.has(modelValue)) { + seenModels.add(modelValue); + models.push({ + value: modelValue, + label: `${alias}/${m.id}`, + provider: conn.provider, + alias: alias, + connectionName: conn.name, + modelId: m.id, + }); + } + }); + }); + + if (models.length === 0) { + Object.entries(PROVIDER_MODELS).forEach(([alias, providerModels]) => { + providerModels.forEach(m => { + const modelValue = `${alias}/${m.id}`; + models.push({ + value: modelValue, + label: `${alias}/${m.id}`, + provider: alias, + alias: alias, + connectionName: alias, + modelId: m.id, + }); + }); + }); + } + + return models; + }; + + const handleModelMappingChange = (toolId, modelAlias, targetModel) => { + setModelMappings(prev => ({ + ...prev, + [toolId]: { + ...prev[toolId], + [modelAlias]: targetModel, + }, + })); + }; + + const getBaseUrl = () => { + if (cloudEnabled && CLOUD_URL) { + return CLOUD_URL; + } + if (typeof window !== "undefined") { + return window.location.origin; + } + return "http://localhost:3000"; + }; + + if (loading) { + return ( +

+
+ + + +
+
+ ); + } + + const availableModels = getAllAvailableModels(); + const hasActiveProviders = availableModels.length > 0; + + const renderToolCard = (toolId, tool) => { + const commonProps = { + tool, + isExpanded: expandedTool === toolId, + onToggle: () => setExpandedTool(expandedTool === toolId ? null : toolId), + baseUrl: getBaseUrl(), + apiKeys, + }; + + switch (toolId) { + case "claude": + return ( + handleModelMappingChange(toolId, alias, target)} + hasActiveProviders={hasActiveProviders} + cloudEnabled={cloudEnabled} + /> + ); + case "codex": + return ; + default: + return ; + } + }; + + return ( +
+ {!hasActiveProviders && ( + +
+ warning +
+

No active providers

+

Please add and connect providers first to configure CLI tools.

+
+
+
+ )} + +
+ {Object.entries(CLI_TOOLS).map(([toolId, tool]) => renderToolCard(toolId, tool))} +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js new file mode 100644 index 00000000..b9152cbe --- /dev/null +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js @@ -0,0 +1,320 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, Button, ModelSelectModal } from "@/shared/components"; +import Image from "next/image"; + +const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; + +export default function ClaudeToolCard({ + tool, + isExpanded, + onToggle, + activeProviders, + modelMappings, + onModelMappingChange, + baseUrl, + hasActiveProviders, + apiKeys, + cloudEnabled, +}) { + const [claudeStatus, setClaudeStatus] = useState(null); + const [checkingClaude, setCheckingClaude] = useState(false); + const [applying, setApplying] = useState(false); + const [restoring, setRestoring] = useState(false); + const [message, setMessage] = useState(null); + const [showInstallGuide, setShowInstallGuide] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [currentEditingAlias, setCurrentEditingAlias] = useState(null); + const [selectedApiKey, setSelectedApiKey] = useState(""); + const [copiedConfig, setCopiedConfig] = useState(false); + const [modelAliases, setModelAliases] = useState({}); + + const getConfigStatus = () => { + if (!claudeStatus?.installed) return null; + const currentUrl = claudeStatus.settings?.env?.ANTHROPIC_BASE_URL; + if (!currentUrl) return "not_configured"; + const localMatch = currentUrl.includes("localhost") || currentUrl.includes("127.0.0.1"); + const cloudMatch = cloudEnabled && CLOUD_URL && currentUrl.startsWith(CLOUD_URL); + if (localMatch || cloudMatch) return "configured"; + return "other"; + }; + + const configStatus = getConfigStatus(); + + useEffect(() => { + if (apiKeys?.length > 0 && !selectedApiKey) { + setSelectedApiKey(apiKeys[0].key); + } + }, [apiKeys]); + + useEffect(() => { + if (isExpanded && !claudeStatus) { + checkClaudeStatus(); + fetchModelAliases(); + } + }, [isExpanded]); + + const fetchModelAliases = async () => { + try { + const res = await fetch("/api/models/alias"); + const data = await res.json(); + if (res.ok) setModelAliases(data.aliases || {}); + } catch (error) { + console.log("Error fetching model aliases:", error); + } + }; + + useEffect(() => { + if (claudeStatus?.installed) { + const env = claudeStatus.settings?.env || {}; + tool.defaultModels.forEach((model) => { + if (model.envKey) { + const value = env[model.envKey] || model.defaultValue || ""; + if (value) onModelMappingChange(model.alias, value); + } + }); + // Only set selectedApiKey if it exists in apiKeys list + const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN; + if (tokenFromFile && apiKeys?.some(k => k.key === tokenFromFile)) { + setSelectedApiKey(tokenFromFile); + } + } + }, [claudeStatus, apiKeys]); + + const checkClaudeStatus = async () => { + setCheckingClaude(true); + try { + const res = await fetch("/api/cli-tools/claude-settings"); + const data = await res.json(); + setClaudeStatus(data); + } catch (error) { + setClaudeStatus({ installed: false, error: error.message }); + } finally { + setCheckingClaude(false); + } + }; + + const handleApplySettings = async () => { + setApplying(true); + setMessage(null); + try { + const env = { ANTHROPIC_BASE_URL: baseUrl }; + + // Get key from dropdown, fallback to first key or sk_9router for localhost + const keyToUse = selectedApiKey?.trim() + || (apiKeys?.length > 0 ? apiKeys[0].key : null) + || (!cloudEnabled ? "sk_9router" : null); + + if (keyToUse) { + env.ANTHROPIC_AUTH_TOKEN = keyToUse; + } + + tool.defaultModels.forEach((model) => { + const targetModel = modelMappings[model.alias]; + if (targetModel && model.envKey) env[model.envKey] = targetModel; + }); + const res = await fetch("/api/cli-tools/claude-settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ env }), + }); + const data = await res.json(); + if (res.ok) { + setMessage({ type: "success", text: "Settings applied successfully!" }); + setClaudeStatus(prev => ({ ...prev, hasBackup: true, settings: { ...prev?.settings, env } })); + } else { + setMessage({ type: "error", text: data.error || "Failed to apply settings" }); + } + } catch (error) { + setMessage({ type: "error", text: error.message }); + } finally { + setApplying(false); + } + }; + + const handleResetSettings = async () => { + setRestoring(true); + setMessage(null); + try { + const res = await fetch("/api/cli-tools/claude-settings", { method: "DELETE" }); + const data = await res.json(); + if (res.ok) { + setMessage({ type: "success", text: "Settings reset successfully!" }); + tool.defaultModels.forEach((model) => onModelMappingChange(model.alias, model.defaultValue || "")); + setSelectedApiKey(""); + } else { + setMessage({ type: "error", text: data.error || "Failed to reset settings" }); + } + } catch (error) { + setMessage({ type: "error", text: error.message }); + } finally { + setRestoring(false); + } + }; + + const openModelSelector = (alias) => { + setCurrentEditingAlias(alias); + setModalOpen(true); + }; + + const handleModelSelect = (model) => { + if (currentEditingAlias) onModelMappingChange(currentEditingAlias, model.value); + }; + + // Generate settings.json content for manual copy + const getSettingsContent = () => { + const keyToUse = (selectedApiKey && selectedApiKey.trim()) + ? selectedApiKey + : (!cloudEnabled ? "sk_9router" : ""); + const env = { ANTHROPIC_BASE_URL: baseUrl, ANTHROPIC_AUTH_TOKEN: keyToUse }; + tool.defaultModels.forEach((model) => { + const targetModel = modelMappings[model.alias]; + if (targetModel && model.envKey) env[model.envKey] = targetModel; + }); + return JSON.stringify({ env }, null, 2); + }; + + const copyToClipboard = async (text) => { + try { + await navigator.clipboard.writeText(text); + setCopiedConfig(true); + setTimeout(() => setCopiedConfig(false), 2000); + } catch (err) { + console.log("Failed to copy:", err); + } + }; + + return ( + +
+
+
+ {tool.name} { e.target.style.display = "none"; }} /> +
+
+
+

{tool.name}

+ {configStatus === "configured" && Connected} + {configStatus === "not_configured" && Not configured} + {configStatus === "other" && Other endpoint} +
+

{tool.description}

+
+
+ expand_more +
+ + {isExpanded && ( +
+ {checkingClaude && ( +
+ progress_activity + Checking Claude CLI... +
+ )} + + {!checkingClaude && claudeStatus && !claudeStatus.installed && ( +
+
+ warning +
+

Claude CLI not installed

+

Please install Claude CLI to use this feature.

+
+ +
+ {showInstallGuide && ( +
+

Installation Guide

+
+
+

macOS / Linux / Windows:

+ npm install -g @anthropic-ai/claude-code +
+

After installation, run claude to verify.

+
+
+ )} +
+ )} + + {!checkingClaude && claudeStatus?.installed && ( + <> +
+ check_circle + URL: + {baseUrl} +
+ +
+ Key: + {apiKeys.length > 0 ? ( + + ) : ( + + {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router"} + + )} +
+ +
+ {tool.defaultModels.map((model) => ( +
+ {model.name} + arrow_forward + onModelMappingChange(model.alias, e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> + + {modelMappings[model.alias] && } +
+ ))} +
+ + {message && ( +
+ {message.type === "success" ? "check_circle" : "error"} + {message.text} +
+ )} + +
+ + + +
+ + {/* Manual Config Section */} +
+

Or copy config manually:

+
+
+ ~/.claude/settings.json + +
+
{getSettingsContent()}
+
+
+ + )} +
+ )} + + setModalOpen(false)} onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} modelAliases={modelAliases} title={`Select model for ${currentEditingAlias}`} /> +
+ ); +} + diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js new file mode 100644 index 00000000..9764c9ec --- /dev/null +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js @@ -0,0 +1,307 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, Button, ModelSelectModal } from "@/shared/components"; +import Image from "next/image"; + +export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled }) { + const [codexStatus, setCodexStatus] = useState(null); + const [checkingCodex, setCheckingCodex] = useState(false); + const [applying, setApplying] = useState(false); + const [restoring, setRestoring] = useState(false); + const [message, setMessage] = useState(null); + const [showInstallGuide, setShowInstallGuide] = useState(false); + const [selectedApiKey, setSelectedApiKey] = useState(""); + const [selectedModel, setSelectedModel] = useState(""); + const [copiedConfig, setCopiedConfig] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [modelAliases, setModelAliases] = useState({}); + + useEffect(() => { + if (apiKeys?.length > 0 && !selectedApiKey) { + setSelectedApiKey(apiKeys[0].key); + } + }, [apiKeys]); + + useEffect(() => { + if (isExpanded && !codexStatus) { + checkCodexStatus(); + fetchModelAliases(); + } + }, [isExpanded]); + + const fetchModelAliases = async () => { + try { + const res = await fetch("/api/models/alias"); + const data = await res.json(); + if (res.ok) setModelAliases(data.aliases || {}); + } catch (error) { + console.log("Error fetching model aliases:", error); + } + }; + + // Parse model from config content + useEffect(() => { + if (codexStatus?.config) { + const modelMatch = codexStatus.config.match(/^model\s*=\s*"([^"]+)"/m); + if (modelMatch) setSelectedModel(modelMatch[1]); + } + }, [codexStatus]); + + const getConfigStatus = () => { + if (!codexStatus?.installed) return null; + if (!codexStatus.config) return "not_configured"; + const hasBaseUrl = codexStatus.config.includes(baseUrl) || codexStatus.config.includes("localhost") || codexStatus.config.includes("127.0.0.1"); + return hasBaseUrl ? "configured" : "other"; + }; + + const configStatus = getConfigStatus(); + + const checkCodexStatus = async () => { + setCheckingCodex(true); + try { + const res = await fetch("/api/cli-tools/codex-settings"); + const data = await res.json(); + setCodexStatus(data); + } catch (error) { + setCodexStatus({ installed: false, error: error.message }); + } finally { + setCheckingCodex(false); + } + }; + + const handleApplySettings = async () => { + setApplying(true); + setMessage(null); + try { + // Use sk_9router for localhost if no key, otherwise use selected key + const keyToUse = (selectedApiKey && selectedApiKey.trim()) + ? selectedApiKey + : (!cloudEnabled ? "sk_9router" : selectedApiKey); + + const res = await fetch("/api/cli-tools/codex-settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ baseUrl, apiKey: keyToUse, model: selectedModel }), + }); + const data = await res.json(); + if (res.ok) { + setMessage({ type: "success", text: "Settings applied successfully!" }); + checkCodexStatus(); + } else { + setMessage({ type: "error", text: data.error || "Failed to apply settings" }); + } + } catch (error) { + setMessage({ type: "error", text: error.message }); + } finally { + setApplying(false); + } + }; + + const handleResetSettings = async () => { + setRestoring(true); + setMessage(null); + try { + const res = await fetch("/api/cli-tools/codex-settings", { method: "DELETE" }); + const data = await res.json(); + if (res.ok) { + setMessage({ type: "success", text: "Settings reset successfully!" }); + setSelectedModel(""); + checkCodexStatus(); + } else { + setMessage({ type: "error", text: data.error || "Failed to reset settings" }); + } + } catch (error) { + setMessage({ type: "error", text: error.message }); + } finally { + setRestoring(false); + } + }; + + const handleModelSelect = (model) => { + setSelectedModel(model.value); + setModalOpen(false); + }; + + const configContent = `# 9Router Configuration for Codex CLI +model = "${selectedModel}" +model_provider = "9router" + +[model_providers.9router] +name = "9Router" +base_url = "${baseUrl}/v1" +wire_api = "responses" +`; + + const keyToUse = (selectedApiKey && selectedApiKey.trim()) + ? selectedApiKey + : (!cloudEnabled ? "sk_9router" : ""); + + const authContent = JSON.stringify({ + OPENAI_API_KEY: keyToUse + }, null, 2); + + const copyToClipboard = async (text) => { + try { + await navigator.clipboard.writeText(text); + setCopiedConfig(true); + setTimeout(() => setCopiedConfig(false), 2000); + } catch (err) { + console.log("Failed to copy:", err); + } + }; + + return ( + +
+
+
+ {tool.name} { e.target.style.display = "none"; }} /> +
+
+
+

{tool.name}

+ {configStatus === "configured" && Connected} + {configStatus === "not_configured" && Not configured} + {configStatus === "other" && Other endpoint} +
+

{tool.description}

+
+
+ expand_more +
+ + {isExpanded && ( +
+ {checkingCodex && ( +
+ progress_activity + Checking Codex CLI... +
+ )} + + {!checkingCodex && codexStatus && !codexStatus.installed && ( +
+
+ warning +
+

Codex CLI not installed

+

Please install Codex CLI to use auto-apply feature.

+
+ +
+ {showInstallGuide && ( +
+

Installation Guide

+
+
+

macOS / Linux / Windows:

+ npm install -g @openai/codex +
+

After installation, run codex to verify.

+
+

+ Codex uses ~/.codex/auth.json with OPENAI_API_KEY. + Click "Apply" to auto-configure. +

+
+
+
+ )} +
+ )} + + {!checkingCodex && codexStatus?.installed && ( + <> +
+ check_circle + URL: + {baseUrl}/v1 +
+ +
+ Key: + {apiKeys.length > 0 ? ( + + ) : ( + + {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router"} + + )} +
+ +
+ Model + arrow_forward + setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> + + {selectedModel && } +
+ + {message && ( +
+ {message.type === "success" ? "check_circle" : "error"} + {message.text} +
+ )} + +
+ + + +
+ + )} + + {/* Manual Config Section */} +
+

Or copy config manually:

+ +
+
+ ~/.codex/config.toml + +
+
{configContent}
+
+ +
+
+ ~/.codex/auth.json + +
+
{authContent}
+
+
+
+ )} + + setModalOpen(false)} + onSelect={handleModelSelect} + selectedModel={selectedModel} + activeProviders={activeProviders} + modelAliases={modelAliases} + title="Select Model for Codex" + /> +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js new file mode 100644 index 00000000..cf220b8c --- /dev/null +++ b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js @@ -0,0 +1,284 @@ +"use client"; + +import { useState } from "react"; +import { Card, ModelSelectModal } from "@/shared/components"; +import Image from "next/image"; + +export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders = [], cloudEnabled = false }) { + const [copiedField, setCopiedField] = useState(null); + const [showModelModal, setShowModelModal] = useState(false); + const [modelValue, setModelValue] = useState(""); + + // Initialize state directly with computed value - no need for useEffect + const [selectedApiKey, setSelectedApiKey] = useState(() => + apiKeys?.length > 0 ? apiKeys[0].key : "" + ); + + const replaceVars = (text) => { + const keyToUse = (selectedApiKey && selectedApiKey.trim()) + ? selectedApiKey + : (!cloudEnabled ? "sk_9router" : "your-api-key"); + + return text + .replace(/\{\{baseUrl\}\}/g, baseUrl || "http://localhost:3000") + .replace(/\{\{apiKey\}\}/g, keyToUse) + .replace(/\{\{model\}\}/g, modelValue || "provider/model-id"); + }; + + const handleCopy = async (text, field) => { + await navigator.clipboard.writeText(replaceVars(text)); + setCopiedField(field); + setTimeout(() => setCopiedField(null), 2000); + }; + + const handleSelectModel = (model) => { + setModelValue(model.value); + }; + + const hasActiveProviders = activeProviders.length > 0; + + const renderApiKeySelector = () => { + return ( +
+ {apiKeys && apiKeys.length > 0 ? ( + <> + + + + ) : ( + + {cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router"} + + )} +
+ ); + }; + + const renderModelSelector = () => { + return ( +
+ setModelValue(e.target.value)} + placeholder="provider/model-id" + className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" + /> + + {modelValue && ( + <> + + + + )} +
+ ); + }; + + const renderNotes = () => { + if (!tool.notes || tool.notes.length === 0) return null; + + return ( +
+ {tool.notes.map((note, index) => { + // Skip cloudCheck note if cloud is enabled + if (note.type === "cloudCheck" && cloudEnabled) return null; + + const isWarning = note.type === "warning"; + const isError = note.type === "cloudCheck" && !cloudEnabled; + + let bgClass = "bg-blue-500/10 border-blue-500/30"; + let textClass = "text-blue-600 dark:text-blue-400"; + let iconClass = "text-blue-500"; + let icon = "info"; + + if (isWarning) { + bgClass = "bg-yellow-500/10 border-yellow-500/30"; + textClass = "text-yellow-600 dark:text-yellow-400"; + iconClass = "text-yellow-500"; + icon = "warning"; + } else if (isError) { + bgClass = "bg-red-500/10 border-red-500/30"; + textClass = "text-red-600 dark:text-red-400"; + iconClass = "text-red-500"; + icon = "error"; + } + + return ( +
+ {icon} +

{note.text}

+
+ ); + })} +
+ ); + }; + + const canShowGuide = () => { + if (tool.requiresCloud && !cloudEnabled) return false; + return true; + }; + + const renderGuideSteps = () => { + if (!tool.guideSteps) return

Coming soon...

; + + return ( +
+ {renderNotes()} + {canShowGuide() && tool.guideSteps.map((item) => ( +
+
+ {item.step} +
+
+

{item.title}

+ {item.desc &&

{item.desc}

} + {item.type === "apiKeySelector" && renderApiKeySelector()} + {item.type === "modelSelector" && renderModelSelector()} + {item.value && ( +
+ + {replaceVars(item.value)} + + {item.copyable && ( + + )} +
+ )} +
+
+ ))} + + {canShowGuide() && tool.codeBlock && ( +
+
+ {tool.codeBlock.language} + +
+
+              {replaceVars(tool.codeBlock.code)}
+            
+
+ )} +
+ ); + }; + + const renderIcon = () => { + if (tool.image) { + return ( + {tool.name} { e.target.style.display = "none"; }} + /> + ); + } + if (tool.icon) { + return {tool.icon}; + } + return ( + {tool.name} { e.target.style.display = "none"; }} + /> + ); + }; + + return ( + +
+
+
+ {renderIcon()} +
+
+

{tool.name}

+

{tool.description}

+
+
+ expand_more +
+ + {isExpanded && ( +
+ {renderGuideSteps()} +
+ )} + + setShowModelModal(false)} + onSelect={handleSelectModel} + selectedModel={modelValue} + activeProviders={activeProviders} + title="Select Model" + /> +
+ ); +} + diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/index.js b/src/app/(dashboard)/dashboard/cli-tools/components/index.js new file mode 100644 index 00000000..dd07abcb --- /dev/null +++ b/src/app/(dashboard)/dashboard/cli-tools/components/index.js @@ -0,0 +1,4 @@ +export { default as ClaudeToolCard } from "./ClaudeToolCard"; +export { default as CodexToolCard } from "./CodexToolCard"; +export { default as DefaultToolCard } from "./DefaultToolCard"; + diff --git a/src/app/(dashboard)/dashboard/cli-tools/page.js b/src/app/(dashboard)/dashboard/cli-tools/page.js new file mode 100644 index 00000000..24f6030a --- /dev/null +++ b/src/app/(dashboard)/dashboard/cli-tools/page.js @@ -0,0 +1,7 @@ +import { getMachineId } from "@/shared/utils/machine"; +import CLIToolsPageClient from "./CLIToolsPageClient"; + +export default async function CLIToolsPage() { + const machineId = await getMachineId(); + return ; +} diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js new file mode 100644 index 00000000..24e7f3ab --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/page.js @@ -0,0 +1,434 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal } from "@/shared/components"; +import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; + +// Validate combo name: only a-z, A-Z, 0-9, -, _ +const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/; + +export default function CombosPage() { + const [combos, setCombos] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreateModal, setShowCreateModal] = useState(false); + const [editingCombo, setEditingCombo] = useState(null); + const [activeProviders, setActiveProviders] = useState([]); + const { copied, copy } = useCopyToClipboard(); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + const [combosRes, providersRes] = await Promise.all([ + fetch("/api/combos"), + fetch("/api/providers"), + ]); + const combosData = await combosRes.json(); + const providersData = await providersRes.json(); + + if (combosRes.ok) setCombos(combosData.combos || []); + if (providersRes.ok) { + const active = (providersData.connections || []).filter( + c => c.testStatus === "active" || c.testStatus === "success" + ); + setActiveProviders(active); + } + } catch (error) { + console.log("Error fetching data:", error); + } finally { + setLoading(false); + } + }; + + const handleCreate = async (data) => { + try { + const res = await fetch("/api/combos", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (res.ok) { + await fetchData(); + setShowCreateModal(false); + } else { + const err = await res.json(); + alert(err.error || "Failed to create combo"); + } + } catch (error) { + console.log("Error creating combo:", error); + } + }; + + const handleUpdate = async (id, data) => { + try { + const res = await fetch(`/api/combos/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (res.ok) { + await fetchData(); + setEditingCombo(null); + } else { + const err = await res.json(); + alert(err.error || "Failed to update combo"); + } + } catch (error) { + console.log("Error updating combo:", error); + } + }; + + const handleDelete = async (id) => { + if (!confirm("Delete this combo?")) return; + try { + const res = await fetch(`/api/combos/${id}`, { method: "DELETE" }); + if (res.ok) { + setCombos(combos.filter(c => c.id !== id)); + } + } catch (error) { + console.log("Error deleting combo:", error); + } + }; + + if (loading) { + return ( +
+ + +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Combos

+

+ Create model combos with fallback support +

+
+ +
+ + {/* Combos List */} + {combos.length === 0 ? ( + +
+ + layers + +

No combos yet

+ +
+
+ ) : ( +
+ {combos.map((combo) => ( + setEditingCombo(combo)} + onDelete={() => handleDelete(combo.id)} + /> + ))} +
+ )} + + {/* Create Modal - Use key to force remount and reset state */} + setShowCreateModal(false)} + onSave={handleCreate} + activeProviders={activeProviders} + /> + + {/* Edit Modal - Use key to force remount and reset state */} + setEditingCombo(null)} + onSave={(data) => handleUpdate(editingCombo.id, data)} + activeProviders={activeProviders} + /> +
+ ); +} + +function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) { + return ( + +
+
+ {/* Name + Copy */} +
+ layers + {combo.name} + +
+ + {/* Models list */} +
+ {combo.models.length === 0 ? ( +

No models added

+ ) : ( + combo.models.map((model, index) => ( +
+ {index + 1}. + + {model} + + {index === 0 && ( + Primary + )} + {index > 0 && ( + Fallback + )} +
+ )) + )} +
+
+ + {/* Actions */} +
+ + +
+
+
+ ); +} + +function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { + // Initialize state with combo values - key prop on parent handles reset on remount + const [name, setName] = useState(combo?.name || ""); + const [models, setModels] = useState(combo?.models || []); + const [showModelSelect, setShowModelSelect] = useState(false); + const [saving, setSaving] = useState(false); + const [nameError, setNameError] = useState(""); + const [modelAliases, setModelAliases] = useState({}); + + // Fetch model aliases when modal opens + useEffect(() => { + if (isOpen) { + const fetchModelAliases = async () => { + try { + const res = await fetch("/api/models/alias"); + const data = await res.json(); + if (res.ok) setModelAliases(data.aliases || {}); + } catch (error) { + console.log("Error fetching model aliases:", error); + } + }; + fetchModelAliases(); + } + }, [isOpen]); + + const validateName = (value) => { + if (!value.trim()) { + setNameError("Name is required"); + return false; + } + if (!VALID_NAME_REGEX.test(value)) { + setNameError("Only letters, numbers, - and _ allowed"); + return false; + } + setNameError(""); + return true; + }; + + const handleNameChange = (e) => { + const value = e.target.value; + setName(value); + if (value) validateName(value); + else setNameError(""); + }; + + const handleAddModel = (model) => { + if (!models.includes(model.value)) { + setModels([...models, model.value]); + } + }; + + const handleRemoveModel = (index) => { + setModels(models.filter((_, i) => i !== index)); + }; + + const handleModelChange = (index, value) => { + const newModels = [...models]; + newModels[index] = value; + setModels(newModels); + }; + + const handleMoveUp = (index) => { + if (index === 0) return; + const newModels = [...models]; + [newModels[index - 1], newModels[index]] = [newModels[index], newModels[index - 1]]; + setModels(newModels); + }; + + const handleMoveDown = (index) => { + if (index === models.length - 1) return; + const newModels = [...models]; + [newModels[index], newModels[index + 1]] = [newModels[index + 1], newModels[index]]; + setModels(newModels); + }; + + const handleSave = async () => { + if (!validateName(name)) return; + setSaving(true); + await onSave({ name: name.trim(), models }); + setSaving(false); + }; + + const isEdit = !!combo; + + return ( + <> + +
+ {/* Name */} +
+ +

+ Only letters, numbers, - and _ allowed +

+
+ + {/* Models */} +
+
+ + +
+ + {models.length === 0 ? ( +
+

No models added

+

Click "Add Model" to add

+
+ ) : ( +
+ {models.map((model, index) => ( +
+ {/* Priority arrows */} +
+ + +
+ + {/* Model Input */} + handleModelChange(index, e.target.value)} + placeholder="model-name" + className="flex-1" + /> + + {/* Remove */} + +
+ ))} +
+ )} +
+ + {/* Actions */} +
+ + +
+
+
+ + {/* Model Select Modal */} + setShowModelSelect(false)} + onSelect={handleAddModel} + activeProviders={activeProviders} + modelAliases={modelAliases} + title="Add Model to Combo" + /> + + ); +} + diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js new file mode 100644 index 00000000..ea7dc10b --- /dev/null +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -0,0 +1,605 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, Button, Input, Modal, CardSkeleton } from "@/shared/components"; +import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; + +const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; + +export default function APIPageClient({ machineId }) { + const [keys, setKeys] = useState([]); + const [loading, setLoading] = useState(true); + const [showAddModal, setShowAddModal] = useState(false); + const [newKeyName, setNewKeyName] = useState(""); + const [createdKey, setCreatedKey] = useState(null); + + // Cloud sync state + const [cloudEnabled, setCloudEnabled] = useState(false); + const [showCloudModal, setShowCloudModal] = useState(false); + const [showDisableModal, setShowDisableModal] = useState(false); + const [cloudSyncing, setCloudSyncing] = useState(false); + const [cloudStatus, setCloudStatus] = useState(null); + const [syncStep, setSyncStep] = useState(""); // "syncing" | "verifying" | "disabling" | "" + + const { copied, copy } = useCopyToClipboard(); + + useEffect(() => { + fetchData(); + loadCloudSettings(); + }, []); + + const loadCloudSettings = async () => { + try { + const res = await fetch("/api/settings"); + if (res.ok) { + const data = await res.json(); + setCloudEnabled(data.cloudEnabled || false); + } + } catch (error) { + console.log("Error loading cloud settings:", error); + } + }; + + const fetchData = async () => { + try { + const keysRes = await fetch("/api/keys"); + const keysData = await keysRes.json(); + if (keysRes.ok) { + setKeys(keysData.keys || []); + } + } catch (error) { + console.log("Error fetching data:", error); + } finally { + setLoading(false); + } + }; + + const handleCloudToggle = (checked) => { + if (checked) { + setShowCloudModal(true); + } else { + setShowDisableModal(true); + } + }; + + const handleEnableCloud = async () => { + setCloudSyncing(true); + setSyncStep("syncing"); + try { + const res = await fetch("/api/sync/cloud", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "enable" }) + }); + + const data = await res.json(); + if (res.ok) { + setSyncStep("verifying"); + + if (data.verified) { + setCloudEnabled(true); + setCloudStatus({ type: "success", message: "Cloud Proxy connected and verified!" }); + setShowCloudModal(false); + } else { + setCloudEnabled(true); + setCloudStatus({ + type: "warning", + message: data.verifyError || "Connected but verification failed" + }); + setShowCloudModal(false); + } + + // Refresh keys list if new key was created + if (data.createdKey) { + await fetchData(); + } + } else { + setCloudStatus({ type: "error", message: data.error || "Failed to enable cloud" }); + } + } catch (error) { + setCloudStatus({ type: "error", message: error.message }); + } finally { + setCloudSyncing(false); + setSyncStep(""); + } + }; + + const handleConfirmDisable = async () => { + setCloudSyncing(true); + setSyncStep("syncing"); + + try { + // Step 1: Sync latest data from cloud + const syncRes = await fetch("/api/sync/cloud", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "sync" }) + }); + + setSyncStep("disabling"); + + // Step 2: Disable cloud + const disableRes = await fetch("/api/sync/cloud", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "disable" }) + }); + + if (disableRes.ok) { + setCloudEnabled(false); + setCloudStatus({ type: "success", message: "Cloud disabled" }); + setShowDisableModal(false); + } + } catch (error) { + console.log("Error disabling cloud:", error); + setCloudStatus({ type: "error", message: "Failed to disable cloud" }); + } finally { + setCloudSyncing(false); + setSyncStep(""); + } + }; + + const handleSyncCloud = async () => { + if (!cloudEnabled) return; + + setCloudSyncing(true); + try { + const res = await fetch("/api/sync/cloud", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "sync" }) + }); + + const data = await res.json(); + if (res.ok) { + setCloudStatus({ type: "success", message: "Synced successfully" }); + } else { + setCloudStatus({ type: "error", message: data.error }); + } + } catch (error) { + setCloudStatus({ type: "error", message: error.message }); + } finally { + setCloudSyncing(false); + } + }; + + const handleCreateKey = async () => { + if (!newKeyName.trim()) return; + + try { + const res = await fetch("/api/keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: newKeyName }), + }); + const data = await res.json(); + + if (res.ok) { + setCreatedKey(data.key); + await fetchData(); + setNewKeyName(""); + setShowAddModal(false); + } + } catch (error) { + console.log("Error creating key:", error); + } + }; + + const handleDeleteKey = async (id) => { + if (!confirm("Delete this API key?")) return; + + try { + const res = await fetch(`/api/keys/${id}`, { method: "DELETE" }); + if (res.ok) { + setKeys(keys.filter((k) => k.id !== id)); + } + } catch (error) { + console.log("Error deleting key:", error); + } + }; + + const isLocalhost = typeof window !== "undefined" && + (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"); + const baseUrl = typeof window !== "undefined" ? `${window.location.origin}/v1` : "/v1"; + const localApiKey = "HELLO"; + // New format: /v1 (machineId in key), Old format: /{machineId}/v1 + const cloudEndpointNew = `${CLOUD_URL}/v1`; + const cloudEndpointOld = `${CLOUD_URL}/${machineId}/v1`; + + if (loading) { + return ( +
+ + +
+ ); + } + + // Use new format endpoint (machineId embedded in key) + const currentEndpoint = cloudEnabled ? cloudEndpointNew : baseUrl; + + const cloudBenefits = [ + { icon: "public", title: "Access Anywhere", desc: "No port forwarding needed" }, + { icon: "group", title: "Share Endpoint", desc: "Easy team collaboration" }, + { icon: "schedule", title: "Always Online", desc: "24/7 availability" }, + { icon: "speed", title: "Global Edge", desc: "Fast worldwide access" }, + ]; + + return ( +
+ {/* Endpoint Card */} + +
+
+

API Endpoint

+

+ {cloudEnabled ? "Using Cloud Proxy" : "Using Local Server"} +

+
+
+ {cloudEnabled ? ( + + ) : ( + + )} +
+
+ + {/* Endpoint URL */} +
+ + +
+ +
+ + {/* API Keys */} + +
+

API Keys

+ +
+ + {keys.length === 0 ? ( +
+ + vpn_key + +

No API keys yet

+
+ ) : ( +
+ + + + + + + + + + + {keys.map((key) => ( + + + + + + + ))} + +
NameKeyCreatedActions
{key.name} +
+ + {key.key} + +
+
+ {new Date(key.createdAt).toLocaleDateString()} + + +
+
+ )} +
+ + {/* Cloud Proxy Card - Hidden */} + {false && ( + +
+ {/* Header */} +
+
+
+ cloud +
+
+

Cloud Proxy

+

+ {cloudEnabled ? "Connected & Ready" : "Access your API from anywhere"} +

+
+
+
+ {cloudEnabled ? ( + + ) : ( + + )} +
+
+ + {/* Benefits Grid */} +
+ {cloudBenefits.map((benefit) => ( +
+ {benefit.icon} +

{benefit.title}

+

{benefit.desc}

+
+ ))} +
+
+
+ )} + + {/* Cloud Enable Modal */} + setShowCloudModal(false)} + > +
+
+

+ What you will get +

+
    +
  • â€ĸ Access your API from anywhere in the world
  • +
  • â€ĸ Share endpoint with your team easily
  • +
  • â€ĸ No need to open ports or configure firewall
  • +
  • â€ĸ Fast global edge network
  • +
+
+ +
+

+ Note +

+
    +
  • â€ĸ Cloud will keep your auth session for 1 day. If not used, it will be automatically deleted.
  • +
  • â€ĸ Cloud is currently unstable with Claude Code OAuth in some cases.
  • +
+
+ + {/* Sync Progress */} + {cloudSyncing && ( +
+ progress_activity +
+

+ {syncStep === "syncing" && "Syncing data to cloud..."} + {syncStep === "verifying" && "Verifying connection..."} +

+
+
+ )} + +
+ + +
+
+
+ + {/* Add Key Modal */} + { + setShowAddModal(false); + setNewKeyName(""); + }} + > +
+ setNewKeyName(e.target.value)} + placeholder="Production Key" + /> +
+ + +
+
+
+ + {/* Created Key Modal */} + setCreatedKey(null)} + > +
+
+

+ Save this key now! +

+

+ This is the only time you will see this key. Store it securely. +

+
+
+ + +
+ +
+
+ + {/* Disable Cloud Modal */} + !cloudSyncing && setShowDisableModal(false)} + > +
+
+
+ warning +
+

+ Warning +

+

+ All auth sessions will be deleted from cloud. +

+
+
+
+ + {/* Sync Progress */} + {cloudSyncing && ( +
+ progress_activity +
+

+ {syncStep === "syncing" && "Syncing latest data..."} + {syncStep === "disabling" && "Disabling cloud..."} +

+
+
+ )} + +

Are you sure you want to disable cloud proxy?

+ +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/(dashboard)/dashboard/endpoint/page.js b/src/app/(dashboard)/dashboard/endpoint/page.js new file mode 100644 index 00000000..96a3e31e --- /dev/null +++ b/src/app/(dashboard)/dashboard/endpoint/page.js @@ -0,0 +1,7 @@ +import { getMachineId } from "@/shared/utils/machine"; +import EndpointPageClient from "./EndpointPageClient"; + +export default async function EndpointPage() { + const machineId = await getMachineId(); + return ; +} diff --git a/src/app/(dashboard)/dashboard/page.js b/src/app/(dashboard)/dashboard/page.js new file mode 100644 index 00000000..6a94a1fe --- /dev/null +++ b/src/app/(dashboard)/dashboard/page.js @@ -0,0 +1,7 @@ +import { getMachineId } from "@/shared/utils/machine"; +import EndpointPageClient from "./endpoint/EndpointPageClient"; + +export default async function DashboardPage() { + const machineId = await getMachineId(); + return ; +} diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js new file mode 100644 index 00000000..a68d0eee --- /dev/null +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -0,0 +1,95 @@ +"use client"; + +import { Card, Button, Badge, Toggle } from "@/shared/components"; +import { useTheme } from "@/shared/hooks/useTheme"; +import { APP_CONFIG } from "@/shared/constants/config"; + +export default function ProfilePage() { + const { theme, setTheme, isDark } = useTheme(); + + return ( +
+
+ {/* Local Mode Info */} + +
+
+ computer +
+
+

Local Mode

+

Running on your machine

+
+
+
+

+ All data is stored locally in the data/db.json file. +

+
+
+ + {/* Theme Preferences */} + +

Appearance

+
+
+
+

Dark Mode

+

+ Switch between light and dark themes +

+
+ setTheme(isDark ? "light" : "dark")} + /> +
+ + {/* Theme Options */} +
+ {["light", "dark", "system"].map((option) => ( + + ))} +
+
+
+ + {/* Data Management */} + +

Data

+
+
+
+

Database Location

+

~/9router/data/db.json

+
+
+
+
+ + {/* App Info */} +
+

{APP_CONFIG.name} v{APP_CONFIG.version}

+

Local Mode - All data stored on your machine

+
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js new file mode 100644 index 00000000..e64d8531 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -0,0 +1,764 @@ +"use client"; + +import { useState, useEffect, useMemo } from "react"; +import { useParams } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; +import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal } from "@/shared/components"; +import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, getProviderAlias } from "@/shared/constants/providers"; +import { getModelsByProviderId } from "@/shared/constants/models"; +import { PROVIDER_ENDPOINTS } from "@/shared/constants/config"; +import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; + +export default function ProviderDetailPage() { + const params = useParams(); + const providerId = params.id; + const [connections, setConnections] = useState([]); + const [loading, setLoading] = useState(true); + const [showOAuthModal, setShowOAuthModal] = useState(false); + const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [selectedConnection, setSelectedConnection] = useState(null); + const [modelAliases, setModelAliases] = useState({}); + const { copied, copy } = useCopyToClipboard(); + + const providerInfo = OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId]; + const isOAuth = !!OAUTH_PROVIDERS[providerId]; + const models = getModelsByProviderId(providerId); + const providerAlias = getProviderAlias(providerId); + + useEffect(() => { + fetchConnections(); + fetchAliases(); + }, [providerId]); + + const fetchAliases = async () => { + try { + const res = await fetch("/api/models/alias"); + const data = await res.json(); + if (res.ok) { + setModelAliases(data.aliases || {}); + } + } catch (error) { + console.log("Error fetching aliases:", error); + } + }; + + const handleSetAlias = async (modelId, alias) => { + const fullModel = `${providerAlias}/${modelId}`; + try { + const res = await fetch("/api/models/alias", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model: fullModel, alias }), + }); + if (res.ok) { + await fetchAliases(); + } else { + const data = await res.json(); + alert(data.error || "Failed to set alias"); + } + } catch (error) { + console.log("Error setting alias:", error); + } + }; + + const handleDeleteAlias = async (alias) => { + try { + const res = await fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, { + method: "DELETE", + }); + if (res.ok) { + await fetchAliases(); + } + } catch (error) { + console.log("Error deleting alias:", error); + } + }; + + const fetchConnections = async () => { + try { + const res = await fetch("/api/providers"); + const data = await res.json(); + if (res.ok) { + const filtered = (data.connections || []).filter(c => c.provider === providerId); + setConnections(filtered); + } + } catch (error) { + console.log("Error fetching connections:", error); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id) => { + if (!confirm("Delete this connection?")) return; + try { + const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); + if (res.ok) { + setConnections(connections.filter(c => c.id !== id)); + } + } catch (error) { + console.log("Error deleting connection:", error); + } + }; + + const handleOAuthSuccess = () => { + fetchConnections(); + setShowOAuthModal(false); + }; + + const handleSaveApiKey = async (formData) => { + try { + const res = await fetch("/api/providers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider: providerId, ...formData }), + }); + if (res.ok) { + await fetchConnections(); + setShowAddApiKeyModal(false); + } + } catch (error) { + console.log("Error saving connection:", error); + } + }; + + const handleUpdateConnection = async (formData) => { + try { + const res = await fetch(`/api/providers/${selectedConnection.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + if (res.ok) { + await fetchConnections(); + setShowEditModal(false); + } + } catch (error) { + console.log("Error updating connection:", error); + } + }; + + const handleSwapPriority = async (conn1, conn2) => { + if (!conn1 || !conn2) return; + try { + // Swap priorities + await Promise.all([ + fetch(`/api/providers/${conn1.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ priority: conn2.priority }), + }), + fetch(`/api/providers/${conn2.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ priority: conn1.priority }), + }), + ]); + await fetchConnections(); + } catch (error) { + console.log("Error swapping priority:", error); + } + }; + + if (!providerInfo) { + return ( +
+

Provider not found

+ + Back to Providers + +
+ ); + } + + if (loading) { + return ( +
+ + +
+ ); + } + + return ( +
+ {/* Header */} +
+ + arrow_back + Back to Providers + +
+
+ {providerInfo.name} { e.target.style.display = "none"; }} + /> +
+
+

{providerInfo.name}

+

+ {connections.length} connection{connections.length !== 1 ? "s" : ""} +

+
+
+
+ + {/* Connections */} + +
+

Connections

+ +
+ + {connections.length === 0 ? ( +
+ + {isOAuth ? "lock" : "key"} + +

No connections yet

+
+ ) : ( +
+ {connections + .sort((a, b) => (a.priority || 0) - (b.priority || 0)) + .map((conn, index) => ( + handleSwapPriority(conn, connections[index - 1])} + onMoveDown={() => handleSwapPriority(conn, connections[index + 1])} + onEdit={() => { + setSelectedConnection(conn); + setShowEditModal(true); + }} + onDelete={() => handleDelete(conn.id)} + /> + ))} +
+ )} +
+ + {/* Models */} + +

+ {providerInfo.passthroughModels ? "Model Aliases" : "Available Models"} +

+ {providerInfo.passthroughModels ? ( + + ) : models.length === 0 ? ( +

No models configured

+ ) : ( +
+ {models.map((model) => { + const fullModel = `${providerAlias}/${model.id}`; + // Also check for old format (providerId/model) for backward compatibility + const oldFormatModel = `${providerId}/${model.id}`; + const existingAlias = Object.entries(modelAliases).find( + ([, m]) => m === fullModel || m === oldFormatModel + )?.[0]; + return ( + handleSetAlias(model.id, alias)} + onDeleteAlias={() => handleDeleteAlias(existingAlias)} + /> + ); + })} +
+ )} + +
+ + {/* Modals */} + setShowOAuthModal(false)} + /> + setShowAddApiKeyModal(false)} + /> + setShowEditModal(false)} + /> +
+ ); +} + +function ModelRow({ model, fullModel, alias, copied, onCopy }) { + return ( +
+ smart_toy + {fullModel} + +
+ ); +} + +function PassthroughModelsSection({ providerAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias }) { + const [newModel, setNewModel] = useState(""); + const [adding, setAdding] = useState(false); + + // Filter aliases for this provider - models are persisted via alias + const providerAliases = Object.entries(modelAliases).filter( + ([, model]) => model.startsWith(`${providerAlias}/`) + ); + + const allModels = providerAliases.map(([alias, fullModel]) => ({ + modelId: fullModel.replace(`${providerAlias}/`, ""), + fullModel, + alias, + })); + + // Generate default alias from modelId (last part after /) + const generateDefaultAlias = (modelId) => { + const parts = modelId.split("/"); + return parts[parts.length - 1]; + }; + + const handleAdd = async () => { + if (!newModel.trim() || adding) return; + const modelId = newModel.trim(); + const defaultAlias = generateDefaultAlias(modelId); + + // Check if alias already exists + if (modelAliases[defaultAlias]) { + alert(`Alias "${defaultAlias}" already exists. Please use a different model or edit existing alias.`); + return; + } + + setAdding(true); + try { + await onSetAlias(modelId, defaultAlias); + setNewModel(""); + } catch (error) { + console.log("Error adding model:", error); + } finally { + setAdding(false); + } + }; + + return ( +
+

+ OpenRouter supports any model. Add models and create aliases for quick access. +

+ + {/* Add new model */} +
+
+ + setNewModel(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleAdd()} + placeholder="anthropic/claude-3-opus" + className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> +
+ +
+ + {/* Models list */} + {allModels.length > 0 && ( +
+ {allModels.map(({ modelId, fullModel, alias }) => ( + onDeleteAlias(alias)} + /> + ))} +
+ )} +
+ ); +} + +function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias }) { + return ( +
+ smart_toy + +
+

{modelId}

+ +
+ {fullModel} + +
+
+ + {/* Delete button */} + +
+ ); +} + +function CooldownTimer({ until }) { + const [remaining, setRemaining] = useState(""); + + useEffect(() => { + const updateRemaining = () => { + const diff = new Date(until).getTime() - Date.now(); + if (diff <= 0) { + setRemaining(""); + return; + } + const secs = Math.floor(diff / 1000); + if (secs < 60) { + setRemaining(`${secs}s`); + } else if (secs < 3600) { + setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`); + } else { + const hrs = Math.floor(secs / 3600); + const mins = Math.floor((secs % 3600) / 60); + setRemaining(`${hrs}h ${mins}m`); + } + }; + + updateRemaining(); + const interval = setInterval(updateRemaining, 1000); + return () => clearInterval(interval); + }, [until]); + + if (!remaining) return null; + + return ( + + ⏱ {remaining} + + ); +} + +function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onEdit, onDelete }) { + const displayName = isOAuth + ? connection.name || connection.email || connection.displayName || "OAuth Account" + : connection.name; + + // Use useState + useEffect for impure Date.now() to avoid calling during render + const [isCooldown, setIsCooldown] = useState(false); + + useEffect(() => { + const checkCooldown = () => { + const cooldown = connection.rateLimitedUntil && + new Date(connection.rateLimitedUntil).getTime() > Date.now(); + setIsCooldown(cooldown); + }; + + checkCooldown(); + // Update every second while in cooldown + const interval = connection.rateLimitedUntil ? setInterval(checkCooldown, 1000) : null; + return () => { + if (interval) clearInterval(interval); + }; + }, [connection.rateLimitedUntil]); + + // Determine effective status (override unavailable if cooldown expired) + const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown) + ? "active" // Cooldown expired → treat as active + : connection.testStatus; + + const getStatusVariant = () => { + if (effectiveStatus === "active" || effectiveStatus === "success") return "success"; + if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error"; + return "default"; + }; + + const hasError = effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable"; + + return ( +
+
+ {/* Priority arrows */} +
+ + +
+ + {isOAuth ? "lock" : "key"} + +
+

{displayName}

+
+ + {effectiveStatus || "Unknown"} + + {isCooldown && } + {connection.lastError && ( + + {connection.lastError} + + )} + #{connection.priority} + {connection.globalPriority && ( + Auto: {connection.globalPriority} + )} +
+
+
+
+ + +
+
+ ); +} + +function AddApiKeyModal({ isOpen, provider, onSave, onClose }) { + const [formData, setFormData] = useState({ + name: "", + apiKey: "", + priority: 1, + }); + const [validating, setValidating] = useState(false); + const [validationResult, setValidationResult] = useState(null); + + const handleValidate = async () => { + setValidating(true); + try { + const res = await fetch("/api/providers/validate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider, apiKey: formData.apiKey }), + }); + const data = await res.json(); + setValidationResult(data.valid ? "success" : "failed"); + } catch { + setValidationResult("failed"); + } finally { + setValidating(false); + } + }; + + const handleSubmit = () => { + onSave({ + name: formData.name, + apiKey: formData.apiKey, + priority: formData.priority, + testStatus: validationResult === "success" ? "active" : "unknown", + }); + }; + + if (!provider) return null; + + return ( + +
+ setFormData({ ...formData, name: e.target.value })} + placeholder="Production Key" + /> +
+ setFormData({ ...formData, apiKey: e.target.value })} + className="flex-1" + /> +
+ +
+
+ {validationResult && ( + + {validationResult === "success" ? "Valid" : "Invalid"} + + )} + setFormData({ ...formData, priority: parseInt(e.target.value) || 1 })} + /> +
+ + +
+
+
+ ); +} + +function EditConnectionModal({ isOpen, connection, onSave, onClose }) { + const [formData, setFormData] = useState({ + name: "", + priority: 1, + }); + const [testing, setTesting] = useState(false); + const [testResult, setTestResult] = useState(null); + + useEffect(() => { + if (connection) { + setFormData({ + name: connection.name || "", + priority: connection.priority || 1, + }); + setTestResult(null); + } + }, [connection]); + + const handleTest = async () => { + if (!connection?.provider) return; + setTesting(true); + setTestResult(null); + try { + const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" }); + const data = await res.json(); + setTestResult(data.valid ? "success" : "failed"); + if (data.valid) { + onSave({ testStatus: "active", lastError: null, lastErrorAt: null }); + } else { + onSave({ testStatus: "error", lastError: data.error, lastErrorAt: new Date().toISOString() }); + } + } catch { + setTestResult("failed"); + } finally { + setTesting(false); + } + }; + + const handleSubmit = () => { + const updates = { name: formData.name, priority: formData.priority }; + onSave(updates); + }; + + if (!connection) return null; + + const isOAuth = connection.authType === "oauth"; + + return ( + +
+ setFormData({ ...formData, name: e.target.value })} + placeholder={isOAuth ? "Account name" : "Production Key"} + /> + {isOAuth && connection.email && ( +
+

Email

+

{connection.email}

+
+ )} + setFormData({ ...formData, priority: parseInt(e.target.value) || 1 })} + /> + + {/* Test Connection */} +
+ + {testResult && ( + + {testResult === "success" ? "Valid" : "Failed"} + + )} +
+ +
+ + +
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/providers/new/page.js b/src/app/(dashboard)/dashboard/providers/new/page.js new file mode 100644 index 00000000..57d0a7f5 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/new/page.js @@ -0,0 +1,220 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Card, Button, Input, Select, Toggle } from "@/shared/components"; +import { AI_PROVIDERS, AUTH_METHODS } from "@/shared/constants/config"; + +const providerOptions = Object.values(AI_PROVIDERS).map((p) => ({ + value: p.id, + label: p.name, +})); + +const authMethodOptions = Object.values(AUTH_METHODS).map((m) => ({ + value: m.id, + label: m.name, +})); + +export default function NewProviderPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + provider: "", + authMethod: "api_key", + apiKey: "", + displayName: "", + isActive: true, + }); + const [errors, setErrors] = useState({}); + + const handleChange = (field, value) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: null })); + } + }; + + const validate = () => { + const newErrors = {}; + if (!formData.provider) newErrors.provider = "Please select a provider"; + if (formData.authMethod === "api_key" && !formData.apiKey) { + newErrors.apiKey = "API Key is required"; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!validate()) return; + + setLoading(true); + try { + const response = await fetch("/api/providers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + router.push("/dashboard/providers"); + } else { + const data = await response.json(); + setErrors({ submit: data.error || "Failed to create provider" }); + } + } catch (error) { + setErrors({ submit: "An error occurred. Please try again." }); + } finally { + setLoading(false); + } + }; + + const selectedProvider = AI_PROVIDERS[formData.provider]; + + return ( +
+ {/* Header */} +
+ + arrow_back + Back to Providers + +

Add New Provider

+

+ Configure a new AI provider to use with your applications. +

+
+ + {/* Form */} + +
+ {/* Provider Selection */} + handleChange("apiKey", e.target.value)} + error={errors.apiKey} + hint="Your API key will be encrypted and stored securely." + required + /> + )} + + {/* OAuth2 Button */} + {formData.authMethod === "oauth2" && ( + +

+ Connect your account using OAuth2 authentication. +

+ +
+ )} + + {/* Display Name */} + handleChange("displayName", e.target.value)} + hint="Optional. A friendly name to identify this configuration." + /> + + {/* Active Toggle */} + handleChange("isActive", checked)} + label="Active" + description="Enable this provider for use in your applications" + /> + + {/* Error Message */} + {errors.submit && ( +
+ {errors.submit} +
+ )} + + {/* Actions */} +
+ + + + +
+ +
+
+ ); +} + diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js new file mode 100644 index 00000000..f1ce6c79 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/page.js @@ -0,0 +1,235 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardSkeleton, Badge } from "@/shared/components"; +import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; +import Image from "next/image"; +import Link from "next/link"; +import { getErrorCode, getRelativeTime } from "@/shared/utils"; + +export default function ProvidersPage() { + const [connections, setConnections] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + const res = await fetch("/api/providers"); + const data = await res.json(); + if (res.ok) setConnections(data.connections || []); + } catch (error) { + console.log("Error fetching data:", error); + } finally { + setLoading(false); + } + }; + + const getProviderStats = (providerId, authType) => { + const providerConnections = connections.filter( + c => c.provider === providerId && c.authType === authType + ); + + // Helper: check if connection is effectively active (cooldown expired) + const getEffectiveStatus = (conn) => { + const isCooldown = conn.rateLimitedUntil && new Date(conn.rateLimitedUntil).getTime() > Date.now(); + return (conn.testStatus === "unavailable" && !isCooldown) ? "active" : conn.testStatus; + }; + + const connected = providerConnections.filter(c => { + const status = getEffectiveStatus(c); + return status === "active" || status === "success"; + }).length; + + const errorConns = providerConnections.filter(c => { + const status = getEffectiveStatus(c); + return status === "error" || status === "expired" || status === "unavailable"; + }); + + const error = errorConns.length; + const total = providerConnections.length; + + // Get latest error info + const latestError = errorConns.sort((a, b) => + new Date(b.lastErrorAt || 0) - new Date(a.lastErrorAt || 0) + )[0]; + const errorCode = latestError ? getErrorCode(latestError.lastError) : null; + const errorTime = latestError?.lastErrorAt ? getRelativeTime(latestError.lastErrorAt) : null; + + return { connected, error, total, errorCode, errorTime }; + }; + + if (loading) { + return ( +
+ + +
+ ); + } + + return ( +
+ {/* OAuth Providers */} +
+

OAuth Providers

+
+ {Object.entries(OAUTH_PROVIDERS).map(([key, info]) => ( + + ))} +
+
+ + {/* API Key Providers */} +
+

API Key Providers

+
+ {Object.entries(APIKEY_PROVIDERS).map(([key, info]) => ( + + ))} +
+
+
+ ); +} + +function ProviderCard({ providerId, provider, stats }) { + const { connected, error, errorCode, errorTime } = stats; + const [imgError, setImgError] = useState(false); + + const getStatusDisplay = () => { + const parts = []; + if (connected > 0) { + parts.push( + + {connected} Connected + + ); + } + if (error > 0) { + const errText = errorCode ? `${error} Error (${errorCode})` : `${error} Error`; + parts.push( + + {errText} + + ); + } + if (parts.length === 0) { + return No connections; + } + return parts; + }; + + return ( + + +
+
+
+ {!imgError ? ( + {provider.name} setImgError(true)} + /> + ) : ( + + {provider.textIcon || provider.id.slice(0, 2).toUpperCase()} + + )} +
+
+

{provider.name}

+
+ {getStatusDisplay()} + {errorTime && â€ĸ {errorTime}} +
+
+
+ + chevron_right + +
+
+ + ); +} + +// API Key providers - only use textIcon, no image +function ApiKeyProviderCard({ providerId, provider, stats }) { + const { connected, error, errorCode, errorTime } = stats; + + const getStatusDisplay = () => { + const parts = []; + if (connected > 0) { + parts.push( + + {connected} Connected + + ); + } + if (error > 0) { + const errText = errorCode ? `${error} Error (${errorCode})` : `${error} Error`; + parts.push( + + {errText} + + ); + } + if (parts.length === 0) { + return No connections; + } + return parts; + }; + + return ( + + +
+
+
+ + {provider.textIcon || provider.id.slice(0, 2).toUpperCase()} + +
+
+

{provider.name}

+
+ {getStatusDisplay()} + {errorTime && â€ĸ {errorTime}} +
+
+
+ + chevron_right + +
+
+ + ); +} diff --git a/src/app/(dashboard)/layout.js b/src/app/(dashboard)/layout.js new file mode 100644 index 00000000..5ef563e9 --- /dev/null +++ b/src/app/(dashboard)/layout.js @@ -0,0 +1,6 @@ +import { DashboardLayout } from "@/shared/components"; + +export default function DashboardRootLayout({ children }) { + return {children}; +} + diff --git a/src/app/api/cli-tools/claude-settings/route.js b/src/app/api/cli-tools/claude-settings/route.js new file mode 100644 index 00000000..3c2a84a6 --- /dev/null +++ b/src/app/api/cli-tools/claude-settings/route.js @@ -0,0 +1,187 @@ +"use server"; + +import { NextResponse } from "next/server"; +import { exec } from "child_process"; +import { promisify } from "util"; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; + +const execAsync = promisify(exec); + +// Get claude settings path based on OS +const getClaudeSettingsPath = () => { + const homeDir = os.homedir(); + return path.join(homeDir, ".claude", "settings.json"); +}; + + +// Check if claude CLI is installed +const checkClaudeInstalled = async () => { + try { + const isWindows = os.platform() === "win32"; + const command = isWindows ? "where claude" : "which claude"; + await execAsync(command); + return true; + } catch { + return false; + } +}; + +// Read current settings +const readSettings = async () => { + try { + const settingsPath = getClaudeSettingsPath(); + const content = await fs.readFile(settingsPath, "utf-8"); + return JSON.parse(content); + } catch (error) { + if (error.code === "ENOENT") { + return null; + } + throw error; + } +}; + +// GET - Check claude CLI and read current settings +export async function GET() { + try { + const isInstalled = await checkClaudeInstalled(); + + if (!isInstalled) { + return NextResponse.json({ + installed: false, + settings: null, + message: "Claude CLI is not installed", + }); + } + + const settings = await readSettings(); + const has9Router = !!(settings?.env?.ANTHROPIC_BASE_URL); + + return NextResponse.json({ + installed: true, + settings: settings, + has9Router: has9Router, + settingsPath: getClaudeSettingsPath(), + }); + } catch (error) { + console.log("Error checking claude settings:", error); + return NextResponse.json( + { error: "Failed to check claude settings" }, + { status: 500 } + ); + } +} + +// POST - Backup old fields and write new settings +export async function POST(request) { + try { + const { env } = await request.json(); + + if (!env || typeof env !== "object") { + return NextResponse.json( + { error: "Invalid env object" }, + { status: 400 } + ); + } + + const settingsPath = getClaudeSettingsPath(); + const claudeDir = path.dirname(settingsPath); + + // Ensure .claude directory exists + await fs.mkdir(claudeDir, { recursive: true }); + + // Read current settings + let currentSettings = {}; + try { + const content = await fs.readFile(settingsPath, "utf-8"); + currentSettings = JSON.parse(content); + } catch (error) { + if (error.code !== "ENOENT") { + throw error; + } + } + + // Merge new env with existing settings + const newSettings = { + ...currentSettings, + env: { + ...(currentSettings.env || {}), + ...env, + }, + }; + + // Write new settings + await fs.writeFile(settingsPath, JSON.stringify(newSettings, null, 2)); + + return NextResponse.json({ + success: true, + message: "Settings updated successfully", + }); + } catch (error) { + console.log("Error updating claude settings:", error); + return NextResponse.json( + { error: "Failed to update claude settings" }, + { status: 500 } + ); + } +} + +// Fields to remove when resetting +const RESET_ENV_KEYS = [ + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "API_TIMEOUT_MS", +]; + +// DELETE - Reset settings (remove env fields) +export async function DELETE() { + try { + const settingsPath = getClaudeSettingsPath(); + + // Read current settings + let currentSettings = {}; + try { + const content = await fs.readFile(settingsPath, "utf-8"); + currentSettings = JSON.parse(content); + } catch (error) { + if (error.code === "ENOENT") { + return NextResponse.json({ + success: true, + message: "No settings file to reset", + }); + } + throw error; + } + + // Remove specified env fields + if (currentSettings.env) { + RESET_ENV_KEYS.forEach((key) => { + delete currentSettings.env[key]; + }); + + // Clean up empty env object + if (Object.keys(currentSettings.env).length === 0) { + delete currentSettings.env; + } + } + + // Write updated settings + await fs.writeFile(settingsPath, JSON.stringify(currentSettings, null, 2)); + + return NextResponse.json({ + success: true, + message: "Settings reset successfully", + }); + } catch (error) { + console.log("Error resetting claude settings:", error); + return NextResponse.json( + { error: "Failed to reset claude settings" }, + { status: 500 } + ); + } +} + diff --git a/src/app/api/cli-tools/codex-settings/route.js b/src/app/api/cli-tools/codex-settings/route.js new file mode 100644 index 00000000..387f9a5b --- /dev/null +++ b/src/app/api/cli-tools/codex-settings/route.js @@ -0,0 +1,246 @@ +"use server"; + +import { NextResponse } from "next/server"; +import { exec } from "child_process"; +import { promisify } from "util"; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; + +const execAsync = promisify(exec); + +const getCodexDir = () => path.join(os.homedir(), ".codex"); +const getCodexConfigPath = () => path.join(getCodexDir(), "config.toml"); +const getCodexAuthPath = () => path.join(getCodexDir(), "auth.json"); + +// Parse TOML config to object (simple parser for codex config) +const parseToml = (content) => { + const result = { _root: {}, _sections: {} }; + let currentSection = "_root"; + + content.split("\n").forEach((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) return; + + // Section header like [model_providers.9router] + const sectionMatch = trimmed.match(/^\[(.+)\]$/); + if (sectionMatch) { + currentSection = sectionMatch[1]; + result._sections[currentSection] = {}; + return; + } + + // Key = value + const kvMatch = trimmed.match(/^([^=]+)\s*=\s*(.+)$/); + if (kvMatch) { + const key = kvMatch[1].trim(); + let value = kvMatch[2].trim(); + // Remove quotes + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + if (currentSection === "_root") { + result._root[key] = value; + } else { + result._sections[currentSection][key] = value; + } + } + }); + + return result; +}; + +// Convert parsed object back to TOML string +const toToml = (parsed) => { + let lines = []; + + // Root level keys + Object.entries(parsed._root).forEach(([key, value]) => { + lines.push(`${key} = "${value}"`); + }); + + // Sections + Object.entries(parsed._sections).forEach(([section, values]) => { + lines.push(""); + lines.push(`[${section}]`); + Object.entries(values).forEach(([key, value]) => { + lines.push(`${key} = "${value}"`); + }); + }); + + return lines.join("\n") + "\n"; +}; + +// Check if codex CLI is installed +const checkCodexInstalled = async () => { + try { + const isWindows = os.platform() === "win32"; + const command = isWindows ? "where codex" : "which codex"; + await execAsync(command); + return true; + } catch { + return false; + } +}; + +// Read current config.toml +const readConfig = async () => { + try { + const configPath = getCodexConfigPath(); + const content = await fs.readFile(configPath, "utf-8"); + return content; + } catch (error) { + if (error.code === "ENOENT") return null; + throw error; + } +}; + +// Check if config has 9Router settings +const has9RouterConfig = (config) => { + if (!config) return false; + return config.includes("model_provider = \"9router\"") || config.includes("[model_providers.9router]"); +}; + +// GET - Check codex CLI and read current settings +export async function GET() { + try { + const isInstalled = await checkCodexInstalled(); + + if (!isInstalled) { + return NextResponse.json({ + installed: false, + config: null, + message: "Codex CLI is not installed", + }); + } + + const config = await readConfig(); + + return NextResponse.json({ + installed: true, + config, + has9Router: has9RouterConfig(config), + configPath: getCodexConfigPath(), + }); + } catch (error) { + console.log("Error checking codex settings:", error); + return NextResponse.json({ error: "Failed to check codex settings" }, { status: 500 }); + } +} + +// POST - Update 9Router settings (merge with existing config) +export async function POST(request) { + try { + const { baseUrl, apiKey, model } = await request.json(); + + if (!baseUrl || !apiKey || !model) { + return NextResponse.json({ error: "baseUrl, apiKey and model are required" }, { status: 400 }); + } + + const codexDir = getCodexDir(); + const configPath = getCodexConfigPath(); + + // Ensure directory exists + await fs.mkdir(codexDir, { recursive: true }); + + // Read and parse existing config + let parsed = { _root: {}, _sections: {} }; + try { + const existingConfig = await fs.readFile(configPath, "utf-8"); + parsed = parseToml(existingConfig); + } catch { /* No existing config */ } + + // Update only 9Router related fields (api_key goes to auth.json, not config.toml) + parsed._root.model = model; + parsed._root.model_provider = "9router"; + + // Update or create 9router provider section (no api_key - Codex reads from auth.json) + parsed._sections["model_providers.9router"] = { + name: "9Router", + base_url: `${baseUrl}/v1`, + wire_api: "responses", + }; + + // Write merged config + const configContent = toToml(parsed); + await fs.writeFile(configPath, configContent); + + // Update auth.json with OPENAI_API_KEY (Codex reads this first) + const authPath = getCodexAuthPath(); + let authData = {}; + try { + const existingAuth = await fs.readFile(authPath, "utf-8"); + authData = JSON.parse(existingAuth); + } catch { /* No existing auth */ } + + authData.OPENAI_API_KEY = apiKey; + await fs.writeFile(authPath, JSON.stringify(authData, null, 2)); + + return NextResponse.json({ + success: true, + message: "Codex settings applied successfully!", + configPath, + }); + } catch (error) { + console.log("Error updating codex settings:", error); + return NextResponse.json({ error: "Failed to update codex settings" }, { status: 500 }); + } +} + +// DELETE - Remove 9Router settings only (keep other settings) +export async function DELETE() { + try { + const configPath = getCodexConfigPath(); + + // Read and parse existing config + let parsed = { _root: {}, _sections: {} }; + try { + const existingConfig = await fs.readFile(configPath, "utf-8"); + parsed = parseToml(existingConfig); + } catch (error) { + if (error.code === "ENOENT") { + return NextResponse.json({ + success: true, + message: "No config file to reset", + }); + } + throw error; + } + + // Remove 9Router related root fields only if they point to 9router + if (parsed._root.model_provider === "9router") { + delete parsed._root.model; + delete parsed._root.model_provider; + } + + // Remove 9router provider section + delete parsed._sections["model_providers.9router"]; + + // Write updated config + const configContent = toToml(parsed); + await fs.writeFile(configPath, configContent); + + // Remove OPENAI_API_KEY from auth.json + const authPath = getCodexAuthPath(); + try { + const existingAuth = await fs.readFile(authPath, "utf-8"); + const authData = JSON.parse(existingAuth); + delete authData.OPENAI_API_KEY; + + // Write back or delete if empty + if (Object.keys(authData).length === 0) { + await fs.unlink(authPath); + } else { + await fs.writeFile(authPath, JSON.stringify(authData, null, 2)); + } + } catch { /* No auth file */ } + + return NextResponse.json({ + success: true, + message: "9Router settings removed successfully", + }); + } catch (error) { + console.log("Error resetting codex settings:", error); + return NextResponse.json({ error: "Failed to reset codex settings" }, { status: 500 }); + } +} diff --git a/src/app/api/cloud/auth/route.js b/src/app/api/cloud/auth/route.js new file mode 100644 index 00000000..0c4fa780 --- /dev/null +++ b/src/app/api/cloud/auth/route.js @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { validateApiKey, getProviderConnections, getModelAliases } from "@/models"; + +// Verify API key and return provider credentials +export async function POST(request) { + try { + const authHeader = request.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + // return NextResponse.json({ error: "Missing API key" }, { status: 401 }); + } + + const apiKey = authHeader.slice(7); + + // Validate API key + const isValid = await validateApiKey(apiKey); + if (!isValid) { + // return NextResponse.json({ error: "Invalid API key" }, { status: 401 }); + } + + // Get active provider connections + const connections = await getProviderConnections({ isActive: true }); + + // Map connections + const mappedConnections = connections.map(conn => ({ + provider: conn.provider, + authType: conn.authType, + apiKey: conn.apiKey || null, + accessToken: conn.accessToken || null, + refreshToken: conn.refreshToken || null, + projectId: conn.projectId || null, + expiresAt: conn.expiresAt, + priority: conn.priority, + globalPriority: conn.globalPriority, + defaultModel: conn.defaultModel, + isActive: conn.isActive + })); + + // Get model aliases + const modelAliases = await getModelAliases(); + + return NextResponse.json({ + connections: mappedConnections, + modelAliases + }); + + } catch (error) { + console.log("Cloud auth error:", error); + return NextResponse.json({ error: "Internal error" }, { status: 500 }); + } +} diff --git a/src/app/api/cloud/credentials/update/route.js b/src/app/api/cloud/credentials/update/route.js new file mode 100644 index 00000000..fa25ba3c --- /dev/null +++ b/src/app/api/cloud/credentials/update/route.js @@ -0,0 +1,57 @@ +import { NextResponse } from "next/server"; +import { validateApiKey, getProviderConnections, updateProviderConnection } from "@/models"; + +// Update provider credentials (for cloud token refresh) +export async function PUT(request) { + try { + const authHeader = request.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return NextResponse.json({ error: "Missing API key" }, { status: 401 }); + } + + const apiKey = authHeader.slice(7); + const body = await request.json(); + const { provider, credentials } = body; + + if (!provider || !credentials) { + return NextResponse.json({ error: "Provider and credentials required" }, { status: 400 }); + } + + // Validate API key + const isValid = await validateApiKey(apiKey); + if (!isValid) { + return NextResponse.json({ error: "Invalid API key" }, { status: 401 }); + } + + // Find active connection for provider + const connections = await getProviderConnections({ provider, isActive: true }); + const connection = connections[0]; + + if (!connection) { + return NextResponse.json({ error: `No active connection found for provider: ${provider}` }, { status: 404 }); + } + + // Update credentials + const updateData = {}; + if (credentials.accessToken) { + updateData.accessToken = credentials.accessToken; + } + if (credentials.refreshToken) { + updateData.refreshToken = credentials.refreshToken; + } + if (credentials.expiresIn) { + updateData.expiresAt = new Date(Date.now() + credentials.expiresIn * 1000).toISOString(); + } + + await updateProviderConnection(connection.id, updateData); + + return NextResponse.json({ + success: true, + message: `Credentials updated for provider: ${provider}` + }); + + } catch (error) { + console.log("Update credentials error:", error); + return NextResponse.json({ error: "Failed to update credentials" }, { status: 500 }); + } +} diff --git a/src/app/api/cloud/model/resolve/route.js b/src/app/api/cloud/model/resolve/route.js new file mode 100644 index 00000000..db6b64ce --- /dev/null +++ b/src/app/api/cloud/model/resolve/route.js @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { validateApiKey, getModelAliases } from "@/models"; + +// Resolve model alias to provider/model +export async function POST(request) { + try { + const authHeader = request.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return NextResponse.json({ error: "Missing API key" }, { status: 401 }); + } + + const apiKey = authHeader.slice(7); + + const body = await request.json(); + const { alias } = body; + + if (!alias) { + return NextResponse.json({ error: "Missing alias" }, { status: 400 }); + } + + // Validate API key + const isValid = await validateApiKey(apiKey); + if (!isValid) { + return NextResponse.json({ error: "Invalid API key" }, { status: 401 }); + } + + // Get model aliases + const modelAliases = await getModelAliases(); + const resolved = modelAliases[alias]; + + if (resolved) { + // Parse provider/model + const firstSlash = resolved.indexOf("/"); + if (firstSlash > 0) { + return NextResponse.json({ + alias, + provider: resolved.slice(0, firstSlash), + model: resolved.slice(firstSlash + 1) + }); + } + } + + // Not found + return NextResponse.json({ error: "Alias not found" }, { status: 404 }); + + } catch (error) { + console.log("Model resolve error:", error); + return NextResponse.json({ error: "Internal error" }, { status: 500 }); + } +} diff --git a/src/app/api/cloud/models/alias/route.js b/src/app/api/cloud/models/alias/route.js new file mode 100644 index 00000000..8ae33dd7 --- /dev/null +++ b/src/app/api/cloud/models/alias/route.js @@ -0,0 +1,92 @@ +import { NextResponse } from "next/server"; +import { validateApiKey, getModelAliases, setModelAlias, isCloudEnabled } from "@/models"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +// PUT /api/cloud/models/alias - Set model alias (for cloud/CLI) +export async function PUT(request) { + try { + const authHeader = request.headers.get("authorization"); + const apiKey = authHeader?.replace("Bearer ", ""); + + if (!apiKey) { + return NextResponse.json({ error: "Missing API key" }, { status: 401 }); + } + + const isValid = await validateApiKey(apiKey); + if (!isValid) { + return NextResponse.json({ error: "Invalid API key" }, { status: 401 }); + } + + const body = await request.json(); + const { model, alias } = body; + + if (!model || !alias) { + return NextResponse.json({ error: "Model and alias required" }, { status: 400 }); + } + + // Check if alias already exists for different model + const aliases = await getModelAliases(); + const existingModel = aliases[alias]; + if (existingModel && existingModel !== model) { + return NextResponse.json({ + error: `Alias '${alias}' already in use for model '${existingModel}'` + }, { status: 400 }); + } + + // Update alias + await setModelAlias(alias, model); + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json({ + success: true, + model, + alias, + message: `Alias '${alias}' set for model '${model}'` + }); + } catch (error) { + console.log("Error updating alias:", error); + return NextResponse.json({ error: "Failed to update alias" }, { status: 500 }); + } +} + +/** + * Sync to Cloud if enabled + */ +async function syncToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing aliases to cloud:", error); + } +} + +// GET /api/cloud/models/alias - Get all aliases +export async function GET(request) { + try { + const authHeader = request.headers.get("authorization"); + const apiKey = authHeader?.replace("Bearer ", ""); + + if (!apiKey) { + return NextResponse.json({ error: "Missing API key" }, { status: 401 }); + } + + const isValid = await validateApiKey(apiKey); + if (!isValid) { + return NextResponse.json({ error: "Invalid API key" }, { status: 401 }); + } + + const aliases = await getModelAliases(); + + return NextResponse.json({ aliases }); + } catch (error) { + console.log("Error fetching aliases:", error); + return NextResponse.json({ error: "Failed to fetch aliases" }, { status: 500 }); + } +} diff --git a/src/app/api/combos/[id]/route.js b/src/app/api/combos/[id]/route.js new file mode 100644 index 00000000..af86fa04 --- /dev/null +++ b/src/app/api/combos/[id]/route.js @@ -0,0 +1,94 @@ +import { NextResponse } from "next/server"; +import { getComboById, updateCombo, deleteCombo, getComboByName, isCloudEnabled } from "@/lib/localDb"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +// Validate combo name: only a-z, A-Z, 0-9, -, _ +const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/; + +// GET /api/combos/[id] - Get combo by ID +export async function GET(request, { params }) { + try { + const { id } = await params; + const combo = await getComboById(id); + + if (!combo) { + return NextResponse.json({ error: "Combo not found" }, { status: 404 }); + } + + return NextResponse.json(combo); + } catch (error) { + console.log("Error fetching combo:", error); + return NextResponse.json({ error: "Failed to fetch combo" }, { status: 500 }); + } +} + +// PUT /api/combos/[id] - Update combo +export async function PUT(request, { params }) { + try { + const { id } = await params; + const body = await request.json(); + + // Validate name format if provided + if (body.name) { + if (!VALID_NAME_REGEX.test(body.name)) { + return NextResponse.json({ error: "Name can only contain letters, numbers, - and _" }, { status: 400 }); + } + + // Check if name already exists (exclude current combo) + const existing = await getComboByName(body.name); + if (existing && existing.id !== id) { + return NextResponse.json({ error: "Combo name already exists" }, { status: 400 }); + } + } + + const combo = await updateCombo(id, body); + + if (!combo) { + return NextResponse.json({ error: "Combo not found" }, { status: 404 }); + } + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json(combo); + } catch (error) { + console.log("Error updating combo:", error); + return NextResponse.json({ error: "Failed to update combo" }, { status: 500 }); + } +} + +// DELETE /api/combos/[id] - Delete combo +export async function DELETE(request, { params }) { + try { + const { id } = await params; + const success = await deleteCombo(id); + + if (!success) { + return NextResponse.json({ error: "Combo not found" }, { status: 404 }); + } + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json({ success: true }); + } catch (error) { + console.log("Error deleting combo:", error); + return NextResponse.json({ error: "Failed to delete combo" }, { status: 500 }); + } +} + +/** + * Sync to Cloud if enabled + */ +async function syncToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing to cloud:", error); + } +} diff --git a/src/app/api/combos/route.js b/src/app/api/combos/route.js new file mode 100644 index 00000000..fbb89d1e --- /dev/null +++ b/src/app/api/combos/route.js @@ -0,0 +1,66 @@ +import { NextResponse } from "next/server"; +import { getCombos, createCombo, getComboByName, isCloudEnabled } from "@/lib/localDb"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +// Validate combo name: only a-z, A-Z, 0-9, -, _ +const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/; + +// GET /api/combos - Get all combos +export async function GET() { + try { + const combos = await getCombos(); + return NextResponse.json({ combos }); + } catch (error) { + console.log("Error fetching combos:", error); + return NextResponse.json({ error: "Failed to fetch combos" }, { status: 500 }); + } +} + +// POST /api/combos - Create new combo +export async function POST(request) { + try { + const body = await request.json(); + const { name, models } = body; + + if (!name) { + return NextResponse.json({ error: "Name is required" }, { status: 400 }); + } + + // Validate name format + if (!VALID_NAME_REGEX.test(name)) { + return NextResponse.json({ error: "Name can only contain letters, numbers, - and _" }, { status: 400 }); + } + + // Check if name already exists + const existing = await getComboByName(name); + if (existing) { + return NextResponse.json({ error: "Combo name already exists" }, { status: 400 }); + } + + const combo = await createCombo({ name, models: models || [] }); + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json(combo, { status: 201 }); + } catch (error) { + console.log("Error creating combo:", error); + return NextResponse.json({ error: "Failed to create combo" }, { status: 500 }); + } +} + +/** + * Sync to Cloud if enabled + */ +async function syncToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing to cloud:", error); + } +} diff --git a/src/app/api/init/route.js b/src/app/api/init/route.js new file mode 100644 index 00000000..2630babf --- /dev/null +++ b/src/app/api/init/route.js @@ -0,0 +1,7 @@ +// Auto-initialize cloud sync when server starts +import "@/lib/initCloudSync"; + +// This API route is called automatically to initialize sync +export async function GET() { + return new Response("Initialized", { status: 200 }); +} diff --git a/src/app/api/keys/[id]/route.js b/src/app/api/keys/[id]/route.js new file mode 100644 index 00000000..825e2e78 --- /dev/null +++ b/src/app/api/keys/[id]/route.js @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { deleteApiKey, isCloudEnabled } from "@/lib/localDb"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +// DELETE /api/keys/[id] - Delete API key +export async function DELETE(request, { params }) { + try { + const { id } = await params; + + const deleted = await deleteApiKey(id); + if (!deleted) { + return NextResponse.json({ error: "Key not found" }, { status: 404 }); + } + + // Auto sync to Cloud if enabled + await syncKeysToCloudIfEnabled(); + + return NextResponse.json({ message: "Key deleted successfully" }); + } catch (error) { + console.log("Error deleting key:", error); + return NextResponse.json({ error: "Failed to delete key" }, { status: 500 }); + } +} + +/** + * Sync API keys to Cloud if enabled + */ +async function syncKeysToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing keys to cloud:", error); + } +} diff --git a/src/app/api/keys/route.js b/src/app/api/keys/route.js new file mode 100644 index 00000000..28bf0a2c --- /dev/null +++ b/src/app/api/keys/route.js @@ -0,0 +1,59 @@ +import { NextResponse } from "next/server"; +import { getApiKeys, createApiKey, isCloudEnabled } from "@/lib/localDb"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +// GET /api/keys - List API keys +export async function GET() { + try { + const keys = await getApiKeys(); + return NextResponse.json({ keys }); + } catch (error) { + console.log("Error fetching keys:", error); + return NextResponse.json({ error: "Failed to fetch keys" }, { status: 500 }); + } +} + +// POST /api/keys - Create new API key +export async function POST(request) { + try { + const body = await request.json(); + const { name } = body; + + if (!name) { + return NextResponse.json({ error: "Name is required" }, { status: 400 }); + } + + // Always get machineId from server + const machineId = await getConsistentMachineId(); + const apiKey = await createApiKey(name, machineId); + + // Auto sync to Cloud if enabled + await syncKeysToCloudIfEnabled(); + + return NextResponse.json({ + key: apiKey.key, + name: apiKey.name, + id: apiKey.id, + machineId: apiKey.machineId, + }, { status: 201 }); + } catch (error) { + console.log("Error creating key:", error); + return NextResponse.json({ error: "Failed to create key" }, { status: 500 }); + } +} + +/** + * Sync API keys to Cloud if enabled + */ +async function syncKeysToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing keys to cloud:", error); + } +} diff --git a/src/app/api/models/alias/route.js b/src/app/api/models/alias/route.js new file mode 100644 index 00000000..3c0d1562 --- /dev/null +++ b/src/app/api/models/alias/route.js @@ -0,0 +1,83 @@ +import { NextResponse } from "next/server"; +import { getModelAliases, setModelAlias, deleteModelAlias, isCloudEnabled } from "@/models"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +// GET /api/models/alias - Get all aliases +export async function GET() { + try { + const aliases = await getModelAliases(); + return NextResponse.json({ aliases }); + } catch (error) { + console.log("Error fetching aliases:", error); + return NextResponse.json({ error: "Failed to fetch aliases" }, { status: 500 }); + } +} + +// PUT /api/models/alias - Set model alias +export async function PUT(request) { + try { + const body = await request.json(); + const { model, alias } = body; + + if (!model || !alias) { + return NextResponse.json({ error: "Model and alias required" }, { status: 400 }); + } + + const aliases = await getModelAliases(); + + // Check if alias already used by different model + const existingModel = aliases[alias]; + if (existingModel && existingModel !== model) { + return NextResponse.json({ + error: `Alias '${alias}' already in use for model '${existingModel}'` + }, { status: 400 }); + } + + // Delete old alias for this model (if exists and different from new alias) + const oldAlias = Object.entries(aliases).find(([a, m]) => m === model && a !== alias)?.[0]; + if (oldAlias) { + await deleteModelAlias(oldAlias); + } + + await setModelAlias(alias, model); + await syncToCloudIfEnabled(); + + return NextResponse.json({ success: true, model, alias }); + } catch (error) { + console.log("Error updating alias:", error); + return NextResponse.json({ error: "Failed to update alias" }, { status: 500 }); + } +} + +// DELETE /api/models/alias?alias=xxx - Delete alias +export async function DELETE(request) { + try { + const { searchParams } = new URL(request.url); + const alias = searchParams.get("alias"); + + if (!alias) { + return NextResponse.json({ error: "Alias required" }, { status: 400 }); + } + + await deleteModelAlias(alias); + await syncToCloudIfEnabled(); + + return NextResponse.json({ success: true }); + } catch (error) { + console.log("Error deleting alias:", error); + return NextResponse.json({ error: "Failed to delete alias" }, { status: 500 }); + } +} + +async function syncToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing aliases to cloud:", error); + } +} diff --git a/src/app/api/models/route.js b/src/app/api/models/route.js new file mode 100644 index 00000000..bac99e65 --- /dev/null +++ b/src/app/api/models/route.js @@ -0,0 +1,55 @@ +import { NextResponse } from "next/server"; +import { getModelAliases, setModelAlias } from "@/models"; +import { AI_MODELS } from "@/shared/constants/config"; + +// GET /api/models - Get models with aliases +export async function GET() { + try { + const modelAliases = await getModelAliases(); + + const models = AI_MODELS.map((m) => { + const fullModel = `${m.provider}/${m.model}`; + return { + ...m, + fullModel, + alias: modelAliases[fullModel] || m.model, + }; + }); + + return NextResponse.json({ models }); + } catch (error) { + console.log("Error fetching models:", error); + return NextResponse.json({ error: "Failed to fetch models" }, { status: 500 }); + } +} + +// PUT /api/models - Update model alias +export async function PUT(request) { + try { + const body = await request.json(); + const { model, alias } = body; + + if (!model || !alias) { + return NextResponse.json({ error: "Model and alias required" }, { status: 400 }); + } + + const modelAliases = await getModelAliases(); + + // Check if alias already exists for different model + const existingModel = Object.entries(modelAliases).find( + ([key, val]) => val === alias && key !== model + ); + + if (existingModel) { + return NextResponse.json({ error: "Alias already in use" }, { status: 400 }); + } + + // Update alias + await setModelAlias(model, alias); + + return NextResponse.json({ success: true, model, alias }); + } catch (error) { + console.log("Error updating alias:", error); + return NextResponse.json({ error: "Failed to update alias" }, { status: 500 }); + } +} diff --git a/src/app/api/oauth/[provider]/[action]/route.js b/src/app/api/oauth/[provider]/[action]/route.js new file mode 100644 index 00000000..4ea39b68 --- /dev/null +++ b/src/app/api/oauth/[provider]/[action]/route.js @@ -0,0 +1,187 @@ +import { NextResponse } from "next/server"; +import { + getProvider, + generateAuthData, + exchangeTokens, + requestDeviceCode, + pollForToken +} from "@/lib/oauth/providers"; +import { createProviderConnection, isCloudEnabled } from "@/models"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +/** + * Dynamic OAuth API Route + * Handles: authorize, exchange, device-code, poll + */ + +// GET /api/oauth/[provider]/authorize - Generate auth URL +// GET /api/oauth/[provider]/device-code - Request device code (for device_code flow) +export async function GET(request, { params }) { + try { + const { provider, action } = await params; + const { searchParams } = new URL(request.url); + + if (action === "authorize") { + const redirectUri = searchParams.get("redirect_uri") || "http://localhost:8080/callback"; + const authData = generateAuthData(provider, redirectUri); + return NextResponse.json(authData); + } + + if (action === "device-code") { + const providerData = getProvider(provider); + if (providerData.flowType !== "device_code") { + return NextResponse.json({ error: "Provider does not support device code flow" }, { status: 400 }); + } + + const authData = generateAuthData(provider, null); + + // For providers that don't use PKCE (like GitHub), don't pass codeChallenge + let deviceData; + if (provider === "github") { + deviceData = await requestDeviceCode(provider); + } else { + // Qwen and other providers use PKCE + deviceData = await requestDeviceCode(provider, authData.codeChallenge); + } + + return NextResponse.json({ + ...deviceData, + codeVerifier: authData.codeVerifier, + }); + } + + return NextResponse.json({ error: "Unknown action" }, { status: 400 }); + } catch (error) { + console.log("OAuth GET error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +// POST /api/oauth/[provider]/exchange - Exchange code for tokens and save +// POST /api/oauth/[provider]/poll - Poll for token (device_code flow) +export async function POST(request, { params }) { + try { + const { provider, action } = await params; + const body = await request.json(); + + if (action === "exchange") { + const { code, redirectUri, codeVerifier, state } = body; + + if (!code || !redirectUri || !codeVerifier) { + return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); + } + + // Exchange code for tokens + const tokenData = await exchangeTokens(provider, code, redirectUri, codeVerifier, state); + + // Save to database + const connection = await createProviderConnection({ + provider, + authType: "oauth", + ...tokenData, + expiresAt: tokenData.expiresIn + ? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString() + : null, + testStatus: "active", + }); + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json({ + success: true, + connection: { + id: connection.id, + provider: connection.provider, + email: connection.email, + displayName: connection.displayName, + } + }); + } + + if (action === "poll") { + const { deviceCode, codeVerifier } = body; + + if (!deviceCode) { + return NextResponse.json({ error: "Missing device code" }, { status: 400 }); + } + + // For providers that don't use PKCE (like GitHub), don't pass codeVerifier + let result; + if (provider === "github") { + result = await pollForToken(provider, deviceCode); + } else { + // Qwen and other providers use PKCE + if (!codeVerifier) { + return NextResponse.json({ error: "Missing code verifier" }, { status: 400 }); + } + result = await pollForToken(provider, deviceCode, codeVerifier); + } + + if (result.success) { + // Save to database + const connection = await createProviderConnection({ + provider, + authType: "oauth", + ...result.tokens, + expiresAt: result.tokens.expiresIn + ? new Date(Date.now() + result.tokens.expiresIn * 1000).toISOString() + : null, + testStatus: "active", + }); + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json({ + success: true, + connection: { + id: connection.id, + provider: connection.provider, + } + }); + } + + // Still pending or error + if (!result.pending) { + // Save error to database for actual errors (not pending) + await createProviderConnection({ + provider, + authType: "oauth", + testStatus: "error", + lastError: result.errorDescription, + errorCode: result.error, + lastErrorAt: new Date().toISOString(), + }); + } + + return NextResponse.json({ + success: false, + error: result.error, + errorDescription: result.errorDescription, + pending: result.pending || result.error === "authorization_pending", + }); + } + + return NextResponse.json({ error: "Unknown action" }, { status: 400 }); + } catch (error) { + console.log("OAuth POST error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +/** + * Sync to Cloud if enabled + */ +async function syncToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing to cloud after OAuth:", error); + } +} diff --git a/src/app/api/providers/[id]/models/route.js b/src/app/api/providers/[id]/models/route.js new file mode 100644 index 00000000..a514eec6 --- /dev/null +++ b/src/app/api/providers/[id]/models/route.js @@ -0,0 +1,148 @@ +import { NextResponse } from "next/server"; +import { getProviderConnectionById } from "@/models"; + +// Provider models endpoints configuration +const PROVIDER_MODELS_CONFIG = { + claude: { + url: "https://api.anthropic.com/v1/models", + method: "GET", + headers: { + "Anthropic-Version": "2023-06-01", + "Content-Type": "application/json" + }, + authHeader: "x-api-key", + parseResponse: (data) => data.data || [] + }, + gemini: { + url: "https://generativelanguage.googleapis.com/v1beta/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authQuery: "key", // Use query param for API key + parseResponse: (data) => data.models || [] + }, + "gemini-cli": { + url: "https://generativelanguage.googleapis.com/v1beta/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.models || [] + }, + qwen: { + url: "https://portal.qwen.ai/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || [] + }, + antigravity: { + url: "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models", + method: "POST", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + body: {}, + parseResponse: (data) => data.models || [] + }, + openai: { + url: "https://api.openai.com/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || [] + }, + openrouter: { + url: "https://openrouter.ai/api/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || [] + }, + anthropic: { + url: "https://api.anthropic.com/v1/models", + method: "GET", + headers: { + "Anthropic-Version": "2023-06-01", + "Content-Type": "application/json" + }, + authHeader: "x-api-key", + parseResponse: (data) => data.data || [] + } +}; + +/** + * GET /api/providers/[id]/models - Get models list from provider + */ +export async function GET(request, { params }) { + try { + const { id } = await params; + const connection = await getProviderConnectionById(id); + + if (!connection) { + return NextResponse.json({ error: "Connection not found" }, { status: 404 }); + } + + const config = PROVIDER_MODELS_CONFIG[connection.provider]; + if (!config) { + return NextResponse.json( + { error: `Provider ${connection.provider} does not support models listing` }, + { status: 400 } + ); + } + + // Get auth token + const token = connection.accessToken || connection.apiKey; + if (!token) { + return NextResponse.json({ error: "No valid token found" }, { status: 401 }); + } + + // Build request URL + let url = config.url; + if (config.authQuery) { + url += `?${config.authQuery}=${token}`; + } + + // Build headers + const headers = { ...config.headers }; + if (config.authHeader && !config.authQuery) { + headers[config.authHeader] = (config.authPrefix || "") + token; + } + + // Make request + const fetchOptions = { + method: config.method, + headers + }; + + if (config.body && config.method === "POST") { + fetchOptions.body = JSON.stringify(config.body); + } + + const response = await fetch(url, fetchOptions); + + if (!response.ok) { + const errorText = await response.text(); + console.log(`Error fetching models from ${connection.provider}:`, errorText); + return NextResponse.json( + { error: `Failed to fetch models: ${response.status}` }, + { status: response.status } + ); + } + + const data = await response.json(); + const models = config.parseResponse(data); + + return NextResponse.json({ + provider: connection.provider, + connectionId: connection.id, + models + }); + } catch (error) { + console.log("Error fetching provider models:", error); + return NextResponse.json({ error: "Failed to fetch models" }, { status: 500 }); + } +} + diff --git a/src/app/api/providers/[id]/route.js b/src/app/api/providers/[id]/route.js new file mode 100644 index 00000000..5d1b1477 --- /dev/null +++ b/src/app/api/providers/[id]/route.js @@ -0,0 +1,102 @@ +import { NextResponse } from "next/server"; +import { getProviderConnectionById, updateProviderConnection, deleteProviderConnection, isCloudEnabled } from "@/models"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +// GET /api/providers/[id] - Get single connection +export async function GET(request, { params }) { + try { + const { id } = await params; + const connection = await getProviderConnectionById(id); + + if (!connection) { + return NextResponse.json({ error: "Connection not found" }, { status: 404 }); + } + + // Hide sensitive fields + const result = { ...connection }; + delete result.apiKey; + delete result.accessToken; + delete result.refreshToken; + delete result.idToken; + + return NextResponse.json({ connection: result }); + } catch (error) { + console.log("Error fetching connection:", error); + return NextResponse.json({ error: "Failed to fetch connection" }, { status: 500 }); + } +} + +// PUT /api/providers/[id] - Update connection +export async function PUT(request, { params }) { + try { + const { id } = await params; + const body = await request.json(); + const { name, priority, globalPriority, defaultModel, isActive, apiKey } = body; + + const existing = await getProviderConnectionById(id); + if (!existing) { + return NextResponse.json({ error: "Connection not found" }, { status: 404 }); + } + + const updateData = {}; + if (name !== undefined) updateData.name = name; + if (priority !== undefined) updateData.priority = priority; + if (globalPriority !== undefined) updateData.globalPriority = globalPriority; + if (defaultModel !== undefined) updateData.defaultModel = defaultModel; + if (isActive !== undefined) updateData.isActive = isActive; + if (apiKey && existing.authType === "apikey") updateData.apiKey = apiKey; + + const updated = await updateProviderConnection(id, updateData); + + // Hide sensitive fields + const result = { ...updated }; + delete result.apiKey; + delete result.accessToken; + delete result.refreshToken; + delete result.idToken; + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json({ connection: result }); + } catch (error) { + console.log("Error updating connection:", error); + return NextResponse.json({ error: "Failed to update connection" }, { status: 500 }); + } +} + +// DELETE /api/providers/[id] - Delete connection +export async function DELETE(request, { params }) { + try { + const { id } = await params; + + const deleted = await deleteProviderConnection(id); + if (!deleted) { + return NextResponse.json({ error: "Connection not found" }, { status: 404 }); + } + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json({ message: "Connection deleted successfully" }); + } catch (error) { + console.log("Error deleting connection:", error); + return NextResponse.json({ error: "Failed to delete connection" }, { status: 500 }); + } +} + +/** + * Sync to Cloud if enabled + */ +async function syncToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing providers to cloud:", error); + } +} diff --git a/src/app/api/providers/[id]/test/route.js b/src/app/api/providers/[id]/test/route.js new file mode 100644 index 00000000..30459c67 --- /dev/null +++ b/src/app/api/providers/[id]/test/route.js @@ -0,0 +1,95 @@ +import { NextResponse } from "next/server"; +import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb"; + +// POST /api/providers/[id]/test - Test connection +export async function POST(request, { params }) { + try { + const { id } = await params; + const connection = await getProviderConnectionById(id); + + if (!connection) { + return NextResponse.json({ error: "Connection not found" }, { status: 404 }); + } + + let isValid = false; + let error = null; + + try { + if (connection.authType === "apikey") { + // Test API key + switch (connection.provider) { + case "openai": + const openaiRes = await fetch("https://api.openai.com/v1/models", { + headers: { "Authorization": `Bearer ${connection.apiKey}` }, + }); + isValid = openaiRes.ok; + break; + + case "anthropic": + const anthropicRes = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "x-api-key": connection.apiKey, + "anthropic-version": "2023-06-01", + "content-type": "application/json", + }, + body: JSON.stringify({ + model: "claude-3-haiku-20240307", + max_tokens: 1, + messages: [{ role: "user", content: "test" }], + }), + }); + isValid = anthropicRes.status !== 401; + break; + + case "gemini": + const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${connection.apiKey}`); + isValid = geminiRes.ok; + break; + + case "openrouter": + const openrouterRes = await fetch("https://openrouter.ai/api/v1/models", { + headers: { "Authorization": `Bearer ${connection.apiKey}` }, + }); + isValid = openrouterRes.ok; + break; + + default: + error = "Provider test not supported"; + } + } else { + // OAuth - check if token exists and not expired + if (connection.accessToken) { + if (connection.expiresAt) { + const expiresAt = new Date(connection.expiresAt).getTime(); + isValid = expiresAt > Date.now(); + if (!isValid) error = "Token expired"; + } else { + isValid = true; + } + } else { + error = "No access token"; + } + } + } catch (err) { + error = err.message; + isValid = false; + } + + // Update status in db + await updateProviderConnection(id, { + testStatus: isValid ? "active" : "error", + lastError: isValid ? null : error, + lastErrorAt: isValid ? null : new Date().toISOString(), + }); + + return NextResponse.json({ + valid: isValid, + error: isValid ? null : error, + }); + } catch (error) { + console.log("Error testing connection:", error); + return NextResponse.json({ error: "Test failed" }, { status: 500 }); + } +} + diff --git a/src/app/api/providers/client/route.js b/src/app/api/providers/client/route.js new file mode 100644 index 00000000..5428cd81 --- /dev/null +++ b/src/app/api/providers/client/route.js @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; +import { getProviderConnections } from "@/lib/localDb"; + +// GET /api/providers/client - List all connections for client (includes sensitive fields for sync) +export async function GET() { + try { + const connections = await getProviderConnections(); + + // Include sensitive fields for sync to cloud (only accessible from same origin) + const clientConnections = connections.map(c => ({ + ...c, + // Don't hide sensitive fields here since this is for internal sync + })); + + return NextResponse.json({ connections: clientConnections }); + } catch (error) { + console.log("Error fetching providers for client:", error); + return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 }); + } +} diff --git a/src/app/api/providers/route.js b/src/app/api/providers/route.js new file mode 100644 index 00000000..6b1fbc77 --- /dev/null +++ b/src/app/api/providers/route.js @@ -0,0 +1,84 @@ +import { NextResponse } from "next/server"; +import { getProviderConnections, createProviderConnection, isCloudEnabled } from "@/models"; +import { APIKEY_PROVIDERS } from "@/shared/constants/config"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { syncToCloud } from "@/app/api/sync/cloud/route"; + +// GET /api/providers - List all connections +export async function GET() { + try { + const connections = await getProviderConnections(); + + // Hide sensitive fields + const safeConnections = connections.map(c => ({ + ...c, + apiKey: undefined, + accessToken: undefined, + refreshToken: undefined, + idToken: undefined, + })); + + return NextResponse.json({ connections: safeConnections }); + } catch (error) { + console.log("Error fetching providers:", error); + return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 }); + } +} + +// POST /api/providers - Create new connection (API Key only, OAuth via separate flow) +export async function POST(request) { + try { + const body = await request.json(); + const { provider, apiKey, name, priority, globalPriority, defaultModel, testStatus } = body; + + // Validation + if (!provider || !APIKEY_PROVIDERS[provider]) { + return NextResponse.json({ error: "Invalid provider" }, { status: 400 }); + } + if (!apiKey) { + return NextResponse.json({ error: "API Key is required" }, { status: 400 }); + } + if (!name) { + return NextResponse.json({ error: "Name is required" }, { status: 400 }); + } + + const newConnection = await createProviderConnection({ + provider, + authType: "apikey", + name, + apiKey, + priority: priority || 1, + globalPriority: globalPriority || null, + defaultModel: defaultModel || null, + isActive: true, + testStatus: testStatus || "unknown", + }); + + // Hide sensitive fields + const result = { ...newConnection }; + delete result.apiKey; + + // Auto sync to Cloud if enabled + await syncToCloudIfEnabled(); + + return NextResponse.json({ connection: result }, { status: 201 }); + } catch (error) { + console.log("Error creating provider:", error); + return NextResponse.json({ error: "Failed to create provider" }, { status: 500 }); + } +} + +/** + * Sync to Cloud if enabled + */ +async function syncToCloudIfEnabled() { + try { + const cloudEnabled = await isCloudEnabled(); + if (!cloudEnabled) return; + + const machineId = await getConsistentMachineId(); + await syncToCloud(machineId); + } catch (error) { + console.log("Error syncing providers to cloud:", error); + } +} diff --git a/src/app/api/providers/validate/route.js b/src/app/api/providers/validate/route.js new file mode 100644 index 00000000..94d5b23f --- /dev/null +++ b/src/app/api/providers/validate/route.js @@ -0,0 +1,96 @@ +import { NextResponse } from "next/server"; + +// POST /api/providers/validate - Validate API key with provider +export async function POST(request) { + try { + const body = await request.json(); + const { provider, apiKey } = body; + + if (!provider || !apiKey) { + return NextResponse.json({ error: "Provider and API key required" }, { status: 400 }); + } + + let isValid = false; + let error = null; + + // Validate with each provider + try { + switch (provider) { + case "openai": + const openaiRes = await fetch("https://api.openai.com/v1/models", { + headers: { "Authorization": `Bearer ${apiKey}` }, + }); + isValid = openaiRes.ok; + break; + + case "anthropic": + const anthropicRes = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + "content-type": "application/json", + }, + body: JSON.stringify({ + model: "claude-3-haiku-20240307", + max_tokens: 1, + messages: [{ role: "user", content: "test" }], + }), + }); + isValid = anthropicRes.status !== 401; + break; + + case "gemini": + const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`); + isValid = geminiRes.ok; + break; + + case "openrouter": + const openrouterRes = await fetch("https://openrouter.ai/api/v1/models", { + headers: { "Authorization": `Bearer ${apiKey}` }, + }); + isValid = openrouterRes.ok; + break; + + case "glm": + case "kimi": + case "minimax": { + const claudeBaseUrls = { + glm: "https://api.z.ai/api/anthropic/v1/messages", + kimi: "https://api.kimi.com/coding/v1/messages", + minimax: "https://api.minimax.io/anthropic/v1/messages", + }; + const claudeRes = await fetch(claudeBaseUrls[provider], { + method: "POST", + headers: { + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + "content-type": "application/json", + }, + body: JSON.stringify({ + model: "claude-sonnet-4-20250514", + max_tokens: 1, + messages: [{ role: "user", content: "test" }], + }), + }); + isValid = claudeRes.status !== 401; + break; + } + + default: + return NextResponse.json({ error: "Provider validation not supported" }, { status: 400 }); + } + } catch (err) { + error = err.message; + isValid = false; + } + + return NextResponse.json({ + valid: isValid, + error: isValid ? null : (error || "Invalid API key"), + }); + } catch (error) { + console.log("Error validating API key:", error); + return NextResponse.json({ error: "Validation failed" }, { status: 500 }); + } +} diff --git a/src/app/api/settings/route.js b/src/app/api/settings/route.js new file mode 100644 index 00000000..423f1826 --- /dev/null +++ b/src/app/api/settings/route.js @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { getSettings } from "@/lib/localDb"; + +export async function GET() { + try { + const settings = await getSettings(); + return NextResponse.json(settings); + } catch (error) { + console.log("Error getting settings:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/shutdown/route.js b/src/app/api/shutdown/route.js new file mode 100644 index 00000000..7ce76ef9 --- /dev/null +++ b/src/app/api/shutdown/route.js @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; + +export async function POST() { + const response = NextResponse.json({ success: true, message: "Shutting down..." }); + + setTimeout(() => { + process.exit(0); + }, 500); + + return response; +} + diff --git a/src/app/api/sync/cloud/route.js b/src/app/api/sync/cloud/route.js new file mode 100644 index 00000000..b0ae37d9 --- /dev/null +++ b/src/app/api/sync/cloud/route.js @@ -0,0 +1,255 @@ +import { NextResponse } from "next/server"; +import { getProviderConnections, getModelAliases, getCombos, getApiKeys, createApiKey, updateProviderConnection, updateSettings } from "@/lib/localDb"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; + +const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; + +/** + * POST /api/sync/cloud + * Sync data with Cloud + */ +export async function POST(request) { + try { + const body = await request.json(); + const { action } = body; + + // Always get machineId from server, don't trust client + const machineId = await getConsistentMachineId(); + + switch (action) { + case "enable": + await updateSettings({ cloudEnabled: true }); + // Auto create key if none exists + const keys = await getApiKeys(); + let createdKey = null; + if (keys.length === 0) { + createdKey = await createApiKey("Default Key", machineId); + } + return syncAndVerify(machineId, createdKey?.key, keys); + case "sync": { + const syncResult = await syncToCloud(machineId); + if (syncResult.error) { + return NextResponse.json(syncResult, { status: 502 }); + } + return NextResponse.json(syncResult); + } + case "disable": + await updateSettings({ cloudEnabled: false }); + return handleDisable(machineId, request); + default: + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); + } + } catch (error) { + console.log("Cloud sync error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +/** + * Sync data to Cloud (exported for reuse) + * @param {string} machineId + * @param {string|null} createdKey - Key created during enable + */ +export async function syncToCloud(machineId, createdKey = null) { + // Get current data from db + const providers = await getProviderConnections(); + const modelAliases = await getModelAliases(); + const combos = await getCombos(); + const apiKeys = await getApiKeys(); + + // Send to Cloud + const response = await fetch(`${CLOUD_URL}/sync/${machineId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + providers, + modelAliases, + combos, + apiKeys + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.log("Cloud sync failed:", errorText); + return NextResponse.json({ error: "Cloud sync failed" }, { status: 502 }); + } + + const result = await response.json(); + + // Update local db with tokens from Cloud (providers stored by ID) + if (result.data && result.data.providers) { + await updateLocalTokens(result.data.providers); + } + + const responseData = { + success: true, + message: "Synced successfully", + changes: result.changes + }; + + if (createdKey) { + responseData.createdKey = createdKey; + } + + return responseData; +} + +/** + * Sync and verify connection with ping + */ +async function syncAndVerify(machineId, createdKey, existingKeys) { + // Step 1: Sync data to cloud + const syncResult = await syncToCloud(machineId, createdKey); + if (syncResult.error) { + return NextResponse.json(syncResult, { status: 502 }); + } + + // Step 2: Verify connection by pinging the cloud + const apiKey = createdKey || existingKeys[0]?.key; + if (!apiKey) { + return NextResponse.json({ + ...syncResult, + verified: false, + verifyError: "No API key available" + }); + } + + try { + const pingResponse = await fetch(`${CLOUD_URL}/${machineId}/v1/verify`, { + method: "GET", + headers: { + "Authorization": `Bearer ${apiKey}`, + "Content-Type": "application/json" + } + }); + + if (pingResponse.ok) { + return NextResponse.json({ + ...syncResult, + verified: true + }); + } else { + return NextResponse.json({ + ...syncResult, + verified: false, + verifyError: `Ping failed: ${pingResponse.status}` + }); + } + } catch (error) { + return NextResponse.json({ + ...syncResult, + verified: false, + verifyError: error.message + }); + } +} + +/** + * Disable Cloud - delete cache and update Claude CLI settings + */ +async function handleDisable(machineId, request) { + const response = await fetch(`${CLOUD_URL}/sync/${machineId}`, { + method: "DELETE" + }); + + if (!response.ok) { + const errorText = await response.text(); + console.log("Cloud disable failed:", errorText); + return NextResponse.json({ error: "Failed to disable cloud" }, { status: 502 }); + } + + // Update Claude CLI settings to use local endpoint + const host = request.headers.get("host") || "localhost:3000"; + await updateClaudeSettingsToLocal(machineId, host); + + return NextResponse.json({ + success: true, + message: "Cloud disabled" + }); +} + +/** + * Update Claude CLI settings to use local endpoint (only if currently using cloud) + */ +async function updateClaudeSettingsToLocal(machineId, host) { + try { + const settingsPath = path.join(os.homedir(), ".claude", "settings.json"); + const cloudUrl = `${CLOUD_URL}/${machineId}`; + const localUrl = `http://${host}`; + + // Read current settings + let settings; + try { + const content = await fs.readFile(settingsPath, "utf-8"); + settings = JSON.parse(content); + } catch (error) { + if (error.code === "ENOENT") { + return; // No settings file, nothing to update + } + throw error; + } + + // Check if ANTHROPIC_BASE_URL matches cloud URL + const currentUrl = settings.env?.ANTHROPIC_BASE_URL; + if (!currentUrl || currentUrl !== cloudUrl) { + return; // Not using cloud URL, don't modify + } + + // Update to local URL + settings.env.ANTHROPIC_BASE_URL = localUrl; + await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); + console.log(`Updated Claude CLI settings: ${cloudUrl} → ${localUrl}`); + } catch (error) { + console.log("Failed to update Claude CLI settings:", error.message); + } +} + +/** + * Update local db with data from Cloud + * Simple logic: if Cloud is newer, sync entire provider + * cloudProviders is object keyed by provider ID + */ +async function updateLocalTokens(cloudProviders) { + const localProviders = await getProviderConnections(); + + for (const localProvider of localProviders) { + const cloudProvider = cloudProviders[localProvider.id]; + if (!cloudProvider) continue; + + const cloudUpdatedAt = new Date(cloudProvider.updatedAt || 0).getTime(); + const localUpdatedAt = new Date(localProvider.updatedAt || 0).getTime(); + + // Simple logic: if Cloud is newer, sync entire provider + if (cloudUpdatedAt > localUpdatedAt) { + const updates = { + // Tokens + accessToken: cloudProvider.accessToken, + refreshToken: cloudProvider.refreshToken, + expiresAt: cloudProvider.expiresAt, + expiresIn: cloudProvider.expiresIn, + + // Provider specific data + providerSpecificData: cloudProvider.providerSpecificData || localProvider.providerSpecificData, + + // Status fields + testStatus: cloudProvider.status || "active", + lastError: cloudProvider.lastError, + lastErrorAt: cloudProvider.lastErrorAt, + errorCode: cloudProvider.errorCode, + rateLimitedUntil: cloudProvider.rateLimitedUntil, + + // Metadata + updatedAt: cloudProvider.updatedAt + }; + + await updateProviderConnection(localProvider.id, updates); + console.log(`Updated ${localProvider.provider} (${localProvider.id}) from Cloud (newer: ${new Date(cloudUpdatedAt).toISOString()})`); + } else { + console.log(`Skipped ${localProvider.provider} (${localProvider.id}) - Local is newer or equal`); + } + } +} diff --git a/src/app/api/sync/initialize/route.js b/src/app/api/sync/initialize/route.js new file mode 100644 index 00000000..524cc85e --- /dev/null +++ b/src/app/api/sync/initialize/route.js @@ -0,0 +1,36 @@ +import { NextResponse } from "next/server"; +import initializeCloudSync from "@/shared/services/initializeCloudSync"; + +let syncInitialized = false; + +// POST /api/sync/initialize - Initialize cloud sync scheduler +export async function POST(request) { + try { + if (syncInitialized) { + return NextResponse.json({ + message: "Cloud sync already initialized" + }); + } + + await initializeCloudSync(); + syncInitialized = true; + + return NextResponse.json({ + success: true, + message: "Cloud sync initialized successfully" + }); + } catch (error) { + console.log("Error initializing cloud sync:", error); + return NextResponse.json({ + error: "Failed to initialize cloud sync" + }, { status: 500 }); + } +} + +// GET /api/sync/status - Check sync initialization status +export async function GET(request) { + return NextResponse.json({ + initialized: syncInitialized, + message: syncInitialized ? "Cloud sync is running" : "Cloud sync not initialized" + }); +} diff --git a/src/app/api/tags/route.js b/src/app/api/tags/route.js new file mode 100644 index 00000000..53bfb2ed --- /dev/null +++ b/src/app/api/tags/route.js @@ -0,0 +1,18 @@ +import { ollamaModels } from "open-sse/config/ollamaModels.js"; + +const CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "*" +}; + +export async function OPTIONS() { + return new Response(null, { headers: CORS_HEADERS }); +} + +export async function GET() { + return new Response(JSON.stringify(ollamaModels), { + headers: { "Content-Type": "application/json", ...CORS_HEADERS } + }); +} + diff --git a/src/app/api/usage/[connectionId]/route.js b/src/app/api/usage/[connectionId]/route.js new file mode 100644 index 00000000..acec75f9 --- /dev/null +++ b/src/app/api/usage/[connectionId]/route.js @@ -0,0 +1,30 @@ +import { getProviderConnectionById } from "@/lib/localDb"; +import { getUsageForProvider } from "open-sse/services/usage.js"; + +/** + * GET /api/usage/[connectionId] - Get usage data for a specific connection + */ +export async function GET(request, { params }) { + try { + const { connectionId } = await params; + + // Get connection from database + const connection = await getProviderConnectionById(connectionId); + if (!connection) { + return Response.json({ error: "Connection not found" }, { status: 404 }); + } + + // Only OAuth connections have usage APIs + if (connection.authType !== "oauth") { + return Response.json({ message: "Usage not available for API key connections" }); + } + + // Fetch usage from provider API + const usage = await getUsageForProvider(connection); + return Response.json(usage); + } catch (error) { + console.log("Error fetching usage:", error); + return Response.json({ error: error.message }, { status: 500 }); + } +} + diff --git a/src/app/api/v1/api/chat/route.js b/src/app/api/v1/api/chat/route.js new file mode 100644 index 00000000..b7ae8e80 --- /dev/null +++ b/src/app/api/v1/api/chat/route.js @@ -0,0 +1,38 @@ +import { handleChat } from "@/sse/handlers/chat.js"; +import { initTranslators } from "open-sse/translator/index.js"; +import { transformToOllama } from "open-sse/utils/ollamaTransform.js"; + +let initialized = false; + +async function ensureInitialized() { + if (!initialized) { + await initTranslators(); + initialized = true; + console.log("[SSE] Translators initialized"); + } +} + +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*" + } + }); +} + +export async function POST(request) { + await ensureInitialized(); + + const clonedReq = request.clone(); + let modelName = "llama3.2"; + try { + const body = await clonedReq.json(); + modelName = body.model || "llama3.2"; + } catch {} + + const response = await handleChat(request); + return transformToOllama(response, modelName); +} + diff --git a/src/app/api/v1/chat/completions/route.js b/src/app/api/v1/chat/completions/route.js new file mode 100644 index 00000000..cb74a508 --- /dev/null +++ b/src/app/api/v1/chat/completions/route.js @@ -0,0 +1,37 @@ +import { callCloudWithMachineId } from "@/shared/utils/cloud.js"; +import { handleChat } from "@/sse/handlers/chat.js"; +import { initTranslators } from "open-sse/translator/index.js"; + +let initialized = false; + +/** + * Initialize translators once + */ +async function ensureInitialized() { + if (!initialized) { + await initTranslators(); + initialized = true; + console.log("[SSE] Translators initialized"); + } +} + +/** + * Handle CORS preflight + */ +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*" + } + }); +} + +export async function POST(request) { + // Fallback to local handling + await ensureInitialized(); + + return await handleChat(request); +} + diff --git a/src/app/api/v1/messages/count_tokens/route.js b/src/app/api/v1/messages/count_tokens/route.js new file mode 100644 index 00000000..c5a2918f --- /dev/null +++ b/src/app/api/v1/messages/count_tokens/route.js @@ -0,0 +1,52 @@ +const CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "*" +}; + +/** + * Handle CORS preflight + */ +export async function OPTIONS() { + return new Response(null, { headers: CORS_HEADERS }); +} + +/** + * POST /v1/messages/count_tokens - Mock token count response + */ +export async function POST(request) { + let body; + try { + body = await request.json(); + } catch { + return new Response(JSON.stringify({ error: "Invalid JSON body" }), { + status: 400, + headers: { "Content-Type": "application/json", ...CORS_HEADERS } + }); + } + + // Estimate token count based on content length + const messages = body.messages || []; + let totalChars = 0; + for (const msg of messages) { + if (typeof msg.content === "string") { + totalChars += msg.content.length; + } else if (Array.isArray(msg.content)) { + for (const part of msg.content) { + if (part.type === "text" && part.text) { + totalChars += part.text.length; + } + } + } + } + + // Rough estimate: ~4 chars per token + const inputTokens = Math.ceil(totalChars / 4); + + return new Response(JSON.stringify({ + input_tokens: inputTokens + }), { + headers: { "Content-Type": "application/json", ...CORS_HEADERS } + }); +} + diff --git a/src/app/api/v1/messages/route.js b/src/app/api/v1/messages/route.js new file mode 100644 index 00000000..7bfbbbf7 --- /dev/null +++ b/src/app/api/v1/messages/route.js @@ -0,0 +1,37 @@ +import { handleChat } from "@/sse/handlers/chat.js"; +import { initTranslators } from "open-sse/translator/index.js"; + +let initialized = false; + +/** + * Initialize translators once + */ +async function ensureInitialized() { + if (!initialized) { + await initTranslators(); + initialized = true; + console.log("[SSE] Translators initialized for /v1/messages"); + } +} + +/** + * Handle CORS preflight + */ +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*" + } + }); +} + +/** + * POST /v1/messages - Claude format (auto convert via handleChat) + */ +export async function POST(request) { + await ensureInitialized(); + return await handleChat(request); +} + diff --git a/src/app/api/v1/responses/route.js b/src/app/api/v1/responses/route.js new file mode 100644 index 00000000..543138d3 --- /dev/null +++ b/src/app/api/v1/responses/route.js @@ -0,0 +1,31 @@ +import { handleChat } from "@/sse/handlers/chat.js"; +import { initTranslators } from "open-sse/translator/index.js"; + +let initialized = false; + +async function ensureInitialized() { + if (!initialized) { + await initTranslators(); + initialized = true; + console.log("[SSE] Translators initialized for /v1/responses"); + } +} + +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*" + } + }); +} + +/** + * POST /v1/responses - OpenAI Responses API format + * Now handled by translator pattern (openai-responses format auto-detected) + */ +export async function POST(request) { + await ensureInitialized(); + return await handleChat(request); +} diff --git a/src/app/api/v1/route.js b/src/app/api/v1/route.js new file mode 100644 index 00000000..382c6c3d --- /dev/null +++ b/src/app/api/v1/route.js @@ -0,0 +1,32 @@ +const CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "*" +}; + +/** + * Handle CORS preflight + */ +export async function OPTIONS() { + return new Response(null, { headers: CORS_HEADERS }); +} + +/** + * GET /v1 - Return models list (OpenAI compatible) + */ +export async function GET() { + const models = [ + { id: "claude-sonnet-4-20250514", object: "model", owned_by: "anthropic" }, + { id: "claude-3-5-sonnet-20241022", object: "model", owned_by: "anthropic" }, + { id: "gpt-4o", object: "model", owned_by: "openai" }, + { id: "gemini-2.5-pro", object: "model", owned_by: "google" } + ]; + + return new Response(JSON.stringify({ + object: "list", + data: models + }), { + headers: { "Content-Type": "application/json", ...CORS_HEADERS } + }); +} + diff --git a/src/app/api/v1beta/models/[...path]/route.js b/src/app/api/v1beta/models/[...path]/route.js new file mode 100644 index 00000000..cb9a63ca --- /dev/null +++ b/src/app/api/v1beta/models/[...path]/route.js @@ -0,0 +1,113 @@ +import { handleChat } from "@/sse/handlers/chat.js"; +import { initTranslators } from "open-sse/translator/index.js"; + +let initialized = false; + +/** + * Initialize translators once + */ +async function ensureInitialized() { + if (!initialized) { + await initTranslators(); + initialized = true; + console.log("[SSE] Translators initialized for /v1beta/models"); + } +} + +/** + * Handle CORS preflight + */ +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*" + } + }); +} + +/** + * POST /v1beta/models/{model}:generateContent - Gemini compatible endpoint + * Converts Gemini format to internal format and handles via handleChat + */ +export async function POST(request, { params }) { + await ensureInitialized(); + + try { + const { path } = await params; + // path = ["provider", "model:generateContent"] or ["model:generateContent"] + + let model; + if (path.length >= 2) { + // Format: /v1beta/models/provider/model:generateContent + const provider = path[0]; + const modelAction = path[1]; + const modelName = modelAction.replace(":generateContent", "").replace(":streamGenerateContent", ""); + model = `${provider}/${modelName}`; + } else { + // Format: /v1beta/models/model:generateContent + const modelAction = path[0]; + model = modelAction.replace(":generateContent", "").replace(":streamGenerateContent", ""); + } + + const body = await request.json(); + + // Convert Gemini format to OpenAI/internal format + const convertedBody = convertGeminiToInternal(body, model); + + // Create new request with converted body + const newRequest = new Request(request.url, { + method: "POST", + headers: request.headers, + body: JSON.stringify(convertedBody), + }); + + return await handleChat(newRequest); + } catch (error) { + console.log("Error handling Gemini request:", error); + return Response.json( + { error: { message: error.message, code: 500 } }, + { status: 500 } + ); + } +} + +/** + * Convert Gemini request format to internal format + */ +function convertGeminiToInternal(geminiBody, model) { + const messages = []; + + // Convert system instruction + if (geminiBody.systemInstruction) { + const systemText = geminiBody.systemInstruction.parts + ?.map(p => p.text) + .join("\n") || ""; + if (systemText) { + messages.push({ role: "system", content: systemText }); + } + } + + // Convert contents to messages + if (geminiBody.contents) { + for (const content of geminiBody.contents) { + const role = content.role === "model" ? "assistant" : "user"; + const text = content.parts?.map(p => p.text).join("\n") || ""; + messages.push({ role, content: text }); + } + } + + // Determine if streaming + const stream = geminiBody.generationConfig?.stream !== false; + + return { + model, + messages, + stream, + max_tokens: geminiBody.generationConfig?.maxOutputTokens, + temperature: geminiBody.generationConfig?.temperature, + top_p: geminiBody.generationConfig?.topP, + }; +} + diff --git a/src/app/api/v1beta/models/route.js b/src/app/api/v1beta/models/route.js new file mode 100644 index 00000000..806ffa54 --- /dev/null +++ b/src/app/api/v1beta/models/route.js @@ -0,0 +1,44 @@ +import { PROVIDER_MODELS } from "@/shared/constants/models"; + +/** + * Handle CORS preflight + */ +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "*" + } + }); +} + +/** + * GET /v1beta/models - Gemini compatible models list + * Returns models in Gemini API format + */ +export async function GET() { + try { + // Collect all models from all providers + const models = []; + + for (const [provider, providerModels] of Object.entries(PROVIDER_MODELS)) { + for (const model of providerModels) { + models.push({ + name: `models/${provider}/${model.id}`, + displayName: model.name || model.id, + description: `${provider} model: ${model.name || model.id}`, + supportedGenerationMethods: ["generateContent"], + inputTokenLimit: 128000, + outputTokenLimit: 8192, + }); + } + } + + return Response.json({ models }); + } catch (error) { + console.log("Error fetching models:", error); + return Response.json({ error: { message: error.message } }, { status: 500 }); + } +} + diff --git a/src/app/callback/page.js b/src/app/callback/page.js new file mode 100644 index 00000000..6638321d --- /dev/null +++ b/src/app/callback/page.js @@ -0,0 +1,142 @@ +"use client"; + +import { Suspense, useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; + +/** + * OAuth Callback Page Content + */ +function CallbackContent() { + const searchParams = useSearchParams(); + const [status, setStatus] = useState("processing"); + + useEffect(() => { + const code = searchParams.get("code"); + const state = searchParams.get("state"); + const error = searchParams.get("error"); + const errorDescription = searchParams.get("error_description"); + + const callbackData = { + code, + state, + error, + errorDescription, + fullUrl: window.location.href, + }; + + let sent = false; + + // Check if this callback is from expected origin/port + const expectedOrigins = [ + window.location.origin, // Same origin (for most providers) + "http://localhost:1455", // Codex specific port + ]; + + // Method 1: postMessage to opener (popup mode) + if (window.opener) { + try { + window.opener.postMessage({ type: "oauth_callback", data: callbackData }, "*"); // Allow any origin for local dev + sent = true; + } catch (e) { + console.log("postMessage failed:", e); + } + } + + // Method 2: BroadcastChannel (same origin tabs) + try { + const channel = new BroadcastChannel("oauth_callback"); + channel.postMessage(callbackData); + channel.close(); + sent = true; + } catch (e) { + console.log("BroadcastChannel failed:", e); + } + + // Method 3: localStorage event (fallback) + try { + localStorage.setItem("oauth_callback", JSON.stringify({ ...callbackData, timestamp: Date.now() })); + sent = true; + } catch (e) { + console.log("localStorage failed:", e); + } + + if (sent && (code || error)) { + // Use setTimeout to avoid synchronous setState in effect + setTimeout(() => { + setStatus("success"); + // Auto close after 1.5 seconds + setTimeout(() => { + window.close(); + // If can't close (not a popup), show success message + setTimeout(() => setStatus("done"), 500); + }, 1500); + }, 0); + } else { + setTimeout(() => setStatus("manual"), 0); + } + }, [searchParams]); + + return ( +
+
+ {status === "processing" && ( + <> +
+ progress_activity +
+

Processing...

+

Please wait while we complete the authorization.

+ + )} + + {(status === "success" || status === "done") && ( + <> +
+ check_circle +
+

Authorization Successful!

+

+ {status === "success" ? "This window will close automatically..." : "You can close this tab now."} +

+ + )} + + {status === "manual" && ( + <> +
+ info +
+

Copy This URL

+

+ Please copy the URL from the address bar and paste it in the application. +

+
+ {typeof window !== "undefined" ? window.location.href : ""} +
+ + )} +
+
+ ); +} + +/** + * OAuth Callback Page + * Receives callback from OAuth providers and sends data back via multiple methods + */ +export default function CallbackPage() { + return ( + +
+
+ progress_activity +
+

Loading...

+
+ + }> + +
+ ); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 00000000..c20e6d71 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,169 @@ +@import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); + +/* Claude-inspired Color Palette */ +:root { + /* Primary - Warm Coral/Terracotta */ + --color-primary: #D97757; + --color-primary-hover: #C56243; + + /* Light theme */ + --color-bg: #FBF9F6; + --color-bg-alt: #F5F1ED; + --color-surface: #FFFFFF; + --color-sidebar: #F0EFEC; + --color-border: #E6E4DD; + --color-text-main: #383733; + --color-text-muted: #75736E; + + /* Shadows */ + --shadow-soft: 0 2px 10px rgba(0, 0, 0, 0.03), 0 10px 25px rgba(0, 0, 0, 0.02); + --shadow-warm: 0 4px 20px -2px rgba(217, 119, 87, 0.15); + --shadow-elevated: 0 20px 40px -4px rgba(60, 50, 45, 0.08); +} + +.dark { + /* Dark theme */ + --color-bg: #191918; + --color-bg-alt: #1F1F1E; + --color-surface: #242423; + --color-sidebar: #1F1F1E; + --color-border: #333331; + --color-text-main: #ECEBE8; + --color-text-muted: #9E9D99; + + /* Dark shadows */ + --shadow-soft: 0 2px 10px rgba(0, 0, 0, 0.2), 0 10px 25px rgba(0, 0, 0, 0.15); + --shadow-warm: 0 4px 20px -2px rgba(217, 119, 87, 0.2); + --shadow-elevated: 0 20px 40px -4px rgba(0, 0, 0, 0.4); +} + +@theme inline { + /* Primary */ + --color-primary: var(--color-primary); + --color-primary-hover: var(--color-primary-hover); + + /* Auto-switch colors (use CSS variables from :root/.dark) */ + --color-bg: var(--color-bg); + --color-surface: var(--color-surface); + --color-sidebar: var(--color-sidebar); + --color-border: var(--color-border); + --color-text-main: var(--color-text-main); + --color-text-muted: var(--color-text-muted); + + /* Static colors (for explicit light/dark usage) */ + --color-bg-light: #FBF9F6; + --color-bg-dark: #191918; + --color-surface-light: #FFFFFF; + --color-surface-dark: #242423; + --color-sidebar-light: #F0EFEC; + --color-sidebar-dark: #1F1F1E; + --color-border-light: #E6E4DD; + --color-border-dark: #333331; + --color-text-main-light: #383733; + --color-text-main-dark: #ECEBE8; + --color-text-muted-light: #75736E; + --color-text-muted-dark: #9E9D99; + + /* Shadows */ + --shadow-soft: var(--shadow-soft); + --shadow-warm: var(--shadow-warm); + --shadow-elevated: var(--shadow-elevated); + + /* Font */ + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; +} + +/* Base styles */ +body { + background-color: var(--color-bg); + color: var(--color-text-main); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Selection */ +::selection { + background-color: rgba(217, 119, 87, 0.2); + color: var(--color-primary); +} + +/* Custom scrollbar */ +.custom-scrollbar::-webkit-scrollbar { + width: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(156, 163, 175, 0.3); + border-radius: 20px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: rgba(156, 163, 175, 0.5); +} + +/* Hero gradient */ +.bg-hero-gradient { + background: linear-gradient(180deg, #F5F1ED 0%, #FEFCFB 100%); +} + +.dark .bg-hero-gradient { + background: linear-gradient(180deg, #1F1F1E 0%, #191918 100%); +} + +/* Material Symbols */ +.material-symbols-outlined { + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; +} + +.material-symbols-outlined.fill-1 { + font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; +} + +/* Animations */ +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes border-glow { + 0%, 100% { + box-shadow: 0 0 5px rgba(217, 119, 87, 0.3), 0 0 10px rgba(217, 119, 87, 0.2); + border-color: rgba(217, 119, 87, 0.5); + } + 50% { + box-shadow: 0 0 10px rgba(217, 119, 87, 0.5), 0 0 20px rgba(217, 119, 87, 0.3); + border-color: rgba(217, 119, 87, 0.8); + } +} + +.animate-border-glow { + animation: border-glow 2s ease-in-out infinite; +} diff --git a/src/app/landing/components/AnimatedBackground.js b/src/app/landing/components/AnimatedBackground.js new file mode 100644 index 00000000..9251aafc --- /dev/null +++ b/src/app/landing/components/AnimatedBackground.js @@ -0,0 +1,57 @@ +"use client"; + +export default function AnimatedBackground() { + return ( + <> + {/* Animated Background */} +
+ {/* Grid pattern */} +
+ + {/* Animated gradient orbs */} +
+
+
+ + {/* Vignette effect */} +
+
+ + {/* CSS Animations */} + + + ); +} + diff --git a/src/app/landing/components/Features.js b/src/app/landing/components/Features.js new file mode 100644 index 00000000..37c29ab6 --- /dev/null +++ b/src/app/landing/components/Features.js @@ -0,0 +1,133 @@ +"use client"; + +const FEATURES = [ + { + icon: "link", + title: "Unified Endpoint", + desc: "Access all providers via a single standard API URL.", + colors: { + border: "hover:border-blue-500/50", + bg: "hover:bg-blue-500/5", + iconBg: "bg-blue-500/10", + iconText: "text-blue-500", + titleHover: "group-hover:text-blue-400" + } + }, + { + icon: "bolt", + title: "Easy Setup", + desc: "Get up and running in minutes with npx command.", + colors: { + border: "hover:border-orange-500/50", + bg: "hover:bg-orange-500/5", + iconBg: "bg-orange-500/10", + iconText: "text-orange-500", + titleHover: "group-hover:text-orange-400" + } + }, + { + icon: "shield_with_heart", + title: "Model Fallback", + desc: "Automatically switch providers on failure or high latency.", + colors: { + border: "hover:border-rose-500/50", + bg: "hover:bg-rose-500/5", + iconBg: "bg-rose-500/10", + iconText: "text-rose-500", + titleHover: "group-hover:text-rose-400" + } + }, + { + icon: "monitoring", + title: "Usage Tracking", + desc: "Detailed analytics and cost monitoring across all models.", + colors: { + border: "hover:border-purple-500/50", + bg: "hover:bg-purple-500/5", + iconBg: "bg-purple-500/10", + iconText: "text-purple-500", + titleHover: "group-hover:text-purple-400" + } + }, + { + icon: "key", + title: "OAuth & API Keys", + desc: "Securely manage credentials in one vault.", + colors: { + border: "hover:border-amber-500/50", + bg: "hover:bg-amber-500/5", + iconBg: "bg-amber-500/10", + iconText: "text-amber-500", + titleHover: "group-hover:text-amber-400" + } + }, + { + icon: "cloud_sync", + title: "Cloud Sync", + desc: "Sync your configurations across devices instantly.", + colors: { + border: "hover:border-sky-500/50", + bg: "hover:bg-sky-500/5", + iconBg: "bg-sky-500/10", + iconText: "text-sky-500", + titleHover: "group-hover:text-sky-400" + } + }, + { + icon: "terminal", + title: "CLI Support", + desc: "Works with Claude Code, Codex, Cline, Cursor, and more.", + colors: { + border: "hover:border-emerald-500/50", + bg: "hover:bg-emerald-500/5", + iconBg: "bg-emerald-500/10", + iconText: "text-emerald-500", + titleHover: "group-hover:text-emerald-400" + } + }, + { + icon: "dashboard", + title: "Dashboard", + desc: "Visual dashboard for real-time traffic analysis.", + colors: { + border: "hover:border-fuchsia-500/50", + bg: "hover:bg-fuchsia-500/5", + iconBg: "bg-fuchsia-500/10", + iconText: "text-fuchsia-500", + titleHover: "group-hover:text-fuchsia-400" + } + }, +]; + +export default function Features() { + return ( +
+
+
+

Powerful Features

+

+ Everything you need to manage your AI infrastructure in one place, built for scale. +

+
+ +
+ {FEATURES.map((feature) => ( +
+
+ {feature.icon} +
+

+ {feature.title} +

+

{feature.desc}

+
+ ))} +
+
+
+ ); +} + diff --git a/src/app/landing/components/FlowAnimation.js b/src/app/landing/components/FlowAnimation.js new file mode 100644 index 00000000..08d53ced --- /dev/null +++ b/src/app/landing/components/FlowAnimation.js @@ -0,0 +1,120 @@ +"use client"; +import { useEffect, useState } from "react"; +import Image from "next/image"; + +const CLI_TOOLS = [ + { id: "claude", name: "Claude Code", image: "/providers/claude.png" }, + { id: "codex", name: "OpenAI Codex", image: "/providers/codex.png" }, + { id: "cline", name: "Cline", image: "/providers/cline.png" }, + { id: "cursor", name: "Cursor", image: "/providers/cursor.png" }, +]; + +const PROVIDERS = [ + { id: "openai", name: "OpenAI", color: "bg-emerald-500", textColor: "text-white" }, + { id: "anthropic", name: "Anthropic", color: "bg-orange-400", textColor: "text-white" }, + { id: "gemini", name: "Gemini", color: "bg-blue-500", textColor: "text-white" }, + { id: "github", name: "GitHub Copilot", color: "bg-gray-700", textColor: "text-white" }, +]; + +export default function FlowAnimation() { + const [activeFlow, setActiveFlow] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setActiveFlow((prev) => (prev + 1) % PROVIDERS.length); + }, 2000); + return () => clearInterval(interval); + }, []); + + return ( +
+ {/* 9Router Hub - Center */} +
+ hub + 9Router +
+
+ + {/* CLI Tools - Left side */} +
+ {CLI_TOOLS.map((tool) => ( +
+
+ {tool.name} +
+
+ ))} +
+ + {/* SVG Lines from CLI to 9Router */} + + + + + + + + {/* SVG Lines from 9Router to Providers */} + + + + + + + + {/* AI Providers - Right side */} +
+ {PROVIDERS.map((provider, idx) => ( +
+ {provider.name} +
+ ))} +
+ + {/* Mobile fallback */} +
+

Interactive diagram visible on desktop

+
+
+ ); +} + diff --git a/src/app/landing/components/Footer.js b/src/app/landing/components/Footer.js new file mode 100644 index 00000000..91209a5a --- /dev/null +++ b/src/app/landing/components/Footer.js @@ -0,0 +1,61 @@ +"use client"; + +export default function Footer() { + return ( +
+ ); +} + diff --git a/src/app/landing/components/GetStarted.js b/src/app/landing/components/GetStarted.js new file mode 100644 index 00000000..4f37bbe7 --- /dev/null +++ b/src/app/landing/components/GetStarted.js @@ -0,0 +1,99 @@ +"use client"; +import { useState } from "react"; + +export default function GetStarted() { + const [copied, setCopied] = useState(false); + + const handleCopy = (text) => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+
+ {/* Left: Steps */} +
+

Get Started in 30 Seconds

+

+ Install 9Router, configure your providers via web dashboard, and start routing AI requests. +

+ +
+
+
1
+
+

Install 9Router

+

Run npx command to start the server instantly

+
+
+ +
+
2
+
+

Open Dashboard

+

Configure providers and API keys via web interface

+
+
+ +
+
3
+
+

Route Requests

+

Point your CLI tools to http://localhost:20128

+
+
+
+
+ + {/* Right: Code block */} +
+
+ {/* Terminal header */} +
+
+
+
+
terminal
+
+ + {/* Terminal content */} +
+
handleCopy("npx 9router")} + > + $ + npx 9router + + {copied ? "✓ Copied" : "Copy"} + +
+ +
+ > Starting 9Router...
+ > Server running on http://localhost:20128
+ > Dashboard: http://localhost:20128/dashboard
+ > Ready to route! ✓ +
+ +
+ 📝 Configure providers in dashboard or use environment variables +
+ +
+ Data Location:
+ macOS/Linux: ~/.9router/db.json
+ Windows: %APPDATA%/9router/db.json +
+
+
+
+
+
+
+ ); +} + diff --git a/src/app/landing/components/HeroSection.js b/src/app/landing/components/HeroSection.js new file mode 100644 index 00000000..cb69116a --- /dev/null +++ b/src/app/landing/components/HeroSection.js @@ -0,0 +1,47 @@ +"use client"; + +export default function HeroSection() { + return ( +
+ {/* Glow effect */} +
+ +
+ {/* Version badge */} +
+ + v1.0 is now live +
+ + {/* Main heading */} +

+ One Endpoint for
+ All AI Providers +

+ + {/* Description */} +

+ AI endpoint proxy with web dashboard - A JavaScript port of CLIProxyAPI. Works seamlessly with Claude Code, OpenAI Codex, Cline, RooCode, and other CLI tools. +

+ + {/* CTA Buttons */} +
+ + + code + View on GitHub + +
+
+
+ ); +} + diff --git a/src/app/landing/components/HowItWorks.js b/src/app/landing/components/HowItWorks.js new file mode 100644 index 00000000..d5c381aa --- /dev/null +++ b/src/app/landing/components/HowItWorks.js @@ -0,0 +1,66 @@ +"use client"; + +export default function HowItWorks() { + return ( +
+
+
+

How 9Router Works

+

+ Data flows seamlessly from your application through our intelligent routing layer to the best provider for the job. +

+
+ +
+ {/* Connection line */} +
+ + {/* Step 1: CLI & SDKs */} +
+
+ terminal +
+
+

1. CLI & SDKs

+

+ Your requests start from your favorite tools or our unified SDK. Just change the base URL. +

+
+
+ + {/* Step 2: 9Router Hub */} +
+
+ hub +
+
+

2. 9Router Hub

+

+ Our engine analyzes the prompt, checks provider health, and routes for lowest latency or cost. +

+
+
+ + {/* Step 3: AI Providers */} +
+
+
+
+
+
+
+
+
+
+

3. AI Providers

+

+ The request is fulfilled by OpenAI, Anthropic, Gemini, or others instantly. +

+
+
+
+
+
+ ); +} + diff --git a/src/app/landing/components/Navigation.js b/src/app/landing/components/Navigation.js new file mode 100644 index 00000000..4d20d2f7 --- /dev/null +++ b/src/app/landing/components/Navigation.js @@ -0,0 +1,67 @@ +"use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function Navigation() { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const router = useRouter(); + + return ( + + ); +} + diff --git a/src/app/landing/page.js b/src/app/landing/page.js new file mode 100644 index 00000000..8bab89ab --- /dev/null +++ b/src/app/landing/page.js @@ -0,0 +1,104 @@ +"use client"; +import Navigation from "./components/Navigation"; +import HeroSection from "./components/HeroSection"; +import FlowAnimation from "./components/FlowAnimation"; +import HowItWorks from "./components/HowItWorks"; +import Features from "./components/Features"; +import GetStarted from "./components/GetStarted"; +import Footer from "./components/Footer"; + +export default function LandingPage() { + return ( +
+ {/* Animated Background */} +
+ {/* Grid pattern */} +
+ + {/* Animated gradient orbs */} +
+
+
+ + {/* Vignette effect */} +
+
+ +
+ + +
+ {/* Hero with Flow Animation */} +
+ +
+ +
+
+ + + + + + {/* CTA Section */} +
+
+
+

Ready to Simplify Your AI Infrastructure?

+

+ Join developers who are streamlining their AI integrations with 9Router. Open source and free to start. +

+
+ + +
+
+
+
+ +
+
+ + {/* Global styles for keyframes */} + +
+ ); +} + diff --git a/src/app/layout.js b/src/app/layout.js new file mode 100644 index 00000000..8c9c52d0 --- /dev/null +++ b/src/app/layout.js @@ -0,0 +1,33 @@ +import { Inter } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "@/shared/components/ThemeProvider"; +import "@/lib/initCloudSync"; // Auto-initialize cloud sync + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", +}); + +export const metadata = { + title: "9Router - AI Infrastructure Management", + description: "One endpoint for all your AI providers. Manage keys, monitor usage, and scale effortlessly.", +}; + +export default function RootLayout({ children }) { + return ( + + + + + + + + {children} + + + + ); +} diff --git a/src/app/page.js b/src/app/page.js new file mode 100644 index 00000000..449619b1 --- /dev/null +++ b/src/app/page.js @@ -0,0 +1,7 @@ +// Auto-initialize cloud sync when server starts +import "@/lib/initCloudSync"; +import LandingPage from "./landing/page"; + +export default function InitPage() { + return ; +} diff --git a/src/lib/initCloudSync.js b/src/lib/initCloudSync.js new file mode 100644 index 00000000..7479ee9e --- /dev/null +++ b/src/lib/initCloudSync.js @@ -0,0 +1,22 @@ +import initializeCloudSync from "@/shared/services/initializeCloudSync"; + +// Initialize cloud sync when this module is imported +let initialized = false; + +export async function ensureCloudSyncInitialized() { + if (!initialized) { + try { + await initializeCloudSync(); + initialized = true; + } catch (error) { + console.error("[ServerInit] Error initializing cloud sync:", error); + } + } + return initialized; +} + +// Auto-initialize when module loads +ensureCloudSyncInitialized().catch(console.log); + +export default ensureCloudSyncInitialized; + diff --git a/src/lib/localDb.js b/src/lib/localDb.js new file mode 100644 index 00000000..8ac22cdd --- /dev/null +++ b/src/lib/localDb.js @@ -0,0 +1,496 @@ +import { Low } from "lowdb"; +import { JSONFile } from "lowdb/node"; +import { v4 as uuidv4 } from "uuid"; +import path from "path"; +import os from "os"; +import fs from "fs"; +import { fileURLToPath } from "url"; + +// Get app name from root package.json config +function getAppName() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + // Look for root package.json (monorepo root) + const rootPkgPath = path.resolve(__dirname, "../../../package.json"); + try { + const pkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8")); + return pkg.config?.appName || "9router"; + } catch { + return "9router"; + } +} + +// Get user data directory based on platform +function getUserDataDir() { + const platform = process.platform; + const homeDir = os.homedir(); + const appName = getAppName(); + + if (platform === "win32") { + return path.join(process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), appName); + } else { + // macOS & Linux: ~/.{appName} + return path.join(homeDir, `.${appName}`); + } +} + +// Data file path - stored in user home directory +const DATA_DIR = getUserDataDir(); +const DB_FILE = path.join(DATA_DIR, "db.json"); + +// Ensure data directory exists +if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); +} + +// Default data structure +const defaultData = { + providerConnections: [], + modelAliases: {}, + combos: [], + apiKeys: [], + settings: { + cloudEnabled: false + } +}; + +// Singleton instance +let dbInstance = null; + +/** + * Get database instance (singleton) + */ +export async function getDb() { + if (!dbInstance) { + const adapter = new JSONFile(DB_FILE); + dbInstance = new Low(adapter, defaultData); + + // Try to read DB with error recovery for corrupt JSON + try { + await dbInstance.read(); + } catch (error) { + if (error instanceof SyntaxError) { + console.warn('[DB] Corrupt JSON detected, resetting to defaults...'); + dbInstance.data = defaultData; + await dbInstance.write(); + } else { + throw error; + } + } + + // Initialize with default data if empty + if (!dbInstance.data) { + dbInstance.data = defaultData; + await dbInstance.write(); + } + } + return dbInstance; +} + +// ============ Provider Connections ============ + +/** + * Get all provider connections + */ +export async function getProviderConnections(filter = {}) { + const db = await getDb(); + let connections = db.data.providerConnections || []; + + if (filter.provider) { + connections = connections.filter(c => c.provider === filter.provider); + } + if (filter.isActive !== undefined) { + connections = connections.filter(c => c.isActive === filter.isActive); + } + + // Sort by priority (lower = higher priority) + connections.sort((a, b) => (a.priority || 999) - (b.priority || 999)); + + return connections; +} + +/** + * Get provider connection by ID + */ +export async function getProviderConnectionById(id) { + const db = await getDb(); + return db.data.providerConnections.find(c => c.id === id) || null; +} + +/** + * Create or update provider connection (upsert by provider + email/name) + */ +export async function createProviderConnection(data) { + const db = await getDb(); + const now = new Date().toISOString(); + + // Check for existing connection with same provider and email (for OAuth) + // or same provider and name (for API key) + let existingIndex = -1; + if (data.authType === "oauth" && data.email) { + existingIndex = db.data.providerConnections.findIndex( + c => c.provider === data.provider && c.authType === "oauth" && c.email === data.email + ); + } else if (data.authType === "apikey" && data.name) { + existingIndex = db.data.providerConnections.findIndex( + c => c.provider === data.provider && c.authType === "apikey" && c.name === data.name + ); + } + + // If exists, update instead of create + if (existingIndex !== -1) { + db.data.providerConnections[existingIndex] = { + ...db.data.providerConnections[existingIndex], + ...data, + updatedAt: now, + }; + await db.write(); + return db.data.providerConnections[existingIndex]; + } + + // Generate name for OAuth if not provided + let connectionName = data.name || null; + if (!connectionName && data.authType === "oauth") { + if (data.email) { + connectionName = data.email; + } else { + // Count existing connections for this provider to generate index + const existingCount = db.data.providerConnections.filter( + c => c.provider === data.provider + ).length; + connectionName = `Account ${existingCount + 1}`; + } + } + + // Auto-increment priority if not provided + let connectionPriority = data.priority; + if (!connectionPriority) { + const providerConnections = db.data.providerConnections.filter( + c => c.provider === data.provider + ); + const maxPriority = providerConnections.reduce((max, c) => Math.max(max, c.priority || 0), 0); + connectionPriority = maxPriority + 1; + } + + // Create new connection - only save fields with actual values + const connection = { + id: uuidv4(), + provider: data.provider, + authType: data.authType || "oauth", + name: connectionName, + priority: connectionPriority, + isActive: data.isActive !== undefined ? data.isActive : true, + createdAt: now, + updatedAt: now, + }; + + // Only add optional fields if they have values + const optionalFields = [ + "displayName", "email", "globalPriority", "defaultModel", + "accessToken", "refreshToken", "expiresAt", "tokenType", + "scope", "idToken", "projectId", "apiKey", "testStatus", + "lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn", "errorCode" + ]; + + for (const field of optionalFields) { + if (data[field] !== undefined && data[field] !== null) { + connection[field] = data[field]; + } + } + + // Only add providerSpecificData if it has content + if (data.providerSpecificData && Object.keys(data.providerSpecificData).length > 0) { + connection.providerSpecificData = data.providerSpecificData; + } + + db.data.providerConnections.push(connection); + await db.write(); + + return connection; +} + +/** + * Update provider connection + */ +export async function updateProviderConnection(id, data) { + const db = await getDb(); + const index = db.data.providerConnections.findIndex(c => c.id === id); + + if (index === -1) return null; + + db.data.providerConnections[index] = { + ...db.data.providerConnections[index], + ...data, + updatedAt: new Date().toISOString(), + }; + + await db.write(); + return db.data.providerConnections[index]; +} + +/** + * Delete provider connection + */ +export async function deleteProviderConnection(id) { + const db = await getDb(); + const index = db.data.providerConnections.findIndex(c => c.id === id); + + if (index === -1) return false; + + db.data.providerConnections.splice(index, 1); + await db.write(); + + return true; +} + +// ============ Model Aliases ============ + +/** + * Get all model aliases + */ +export async function getModelAliases() { + const db = await getDb(); + return db.data.modelAliases || {}; +} + +/** + * Set model alias + */ +export async function setModelAlias(alias, model) { + const db = await getDb(); + db.data.modelAliases[alias] = model; + await db.write(); +} + +/** + * Delete model alias + */ +export async function deleteModelAlias(alias) { + const db = await getDb(); + delete db.data.modelAliases[alias]; + await db.write(); +} + +// ============ Combos ============ + +/** + * Get all combos + */ +export async function getCombos() { + const db = await getDb(); + return db.data.combos || []; +} + +/** + * Get combo by ID + */ +export async function getComboById(id) { + const db = await getDb(); + return (db.data.combos || []).find(c => c.id === id) || null; +} + +/** + * Get combo by name + */ +export async function getComboByName(name) { + const db = await getDb(); + return (db.data.combos || []).find(c => c.name === name) || null; +} + +/** + * Create combo + */ +export async function createCombo(data) { + const db = await getDb(); + if (!db.data.combos) db.data.combos = []; + + const now = new Date().toISOString(); + const combo = { + id: uuidv4(), + name: data.name, + models: data.models || [], + createdAt: now, + updatedAt: now, + }; + + db.data.combos.push(combo); + await db.write(); + return combo; +} + +/** + * Update combo + */ +export async function updateCombo(id, data) { + const db = await getDb(); + if (!db.data.combos) db.data.combos = []; + + const index = db.data.combos.findIndex(c => c.id === id); + if (index === -1) return null; + + db.data.combos[index] = { + ...db.data.combos[index], + ...data, + updatedAt: new Date().toISOString(), + }; + + await db.write(); + return db.data.combos[index]; +} + +/** + * Delete combo + */ +export async function deleteCombo(id) { + const db = await getDb(); + if (!db.data.combos) return false; + + const index = db.data.combos.findIndex(c => c.id === id); + if (index === -1) return false; + + db.data.combos.splice(index, 1); + await db.write(); + return true; +} + +// ============ API Keys ============ + +/** + * Get all API keys + */ +export async function getApiKeys() { + const db = await getDb(); + return db.data.apiKeys || []; +} + +/** + * Generate short random key (8 chars) + */ +function generateShortKey() { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < 8; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +/** + * Create API key + * @param {string} name - Key name + * @param {string} machineId - MachineId (required) + */ +export async function createApiKey(name, machineId) { + if (!machineId) { + throw new Error("machineId is required"); + } + + const db = await getDb(); + const now = new Date().toISOString(); + + // Always use new format: sk-{machineId}-{keyId}-{crc8} + const { generateApiKeyWithMachine } = await import("@/shared/utils/apiKey"); + const result = generateApiKeyWithMachine(machineId); + + const apiKey = { + id: uuidv4(), + name: name, + key: result.key, + machineId: machineId, + createdAt: now, + }; + + db.data.apiKeys.push(apiKey); + await db.write(); + + return apiKey; +} + +/** + * Delete API key + */ +export async function deleteApiKey(id) { + const db = await getDb(); + const index = db.data.apiKeys.findIndex(k => k.id === id); + + if (index === -1) return false; + + db.data.apiKeys.splice(index, 1); + await db.write(); + + return true; +} + +/** + * Validate API key + */ +export async function validateApiKey(key) { + const db = await getDb(); + return db.data.apiKeys.some(k => k.key === key); +} + +// ============ Data Cleanup ============ + +/** + * Remove null/empty fields from all provider connections to reduce db size + */ +export async function cleanupProviderConnections() { + const db = await getDb(); + const fieldsToCheck = [ + "displayName", "email", "globalPriority", "defaultModel", + "accessToken", "refreshToken", "expiresAt", "tokenType", + "scope", "idToken", "projectId", "apiKey", "testStatus", + "lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn" + ]; + + let cleaned = 0; + for (const connection of db.data.providerConnections) { + for (const field of fieldsToCheck) { + if (connection[field] === null || connection[field] === undefined) { + delete connection[field]; + cleaned++; + } + } + // Remove empty providerSpecificData + if (connection.providerSpecificData && Object.keys(connection.providerSpecificData).length === 0) { + delete connection.providerSpecificData; + cleaned++; + } + } + + if (cleaned > 0) { + await db.write(); + } + return cleaned; +} + +// ============ Settings ============ + +/** + * Get settings + */ +export async function getSettings() { + const db = await getDb(); + return db.data.settings || { cloudEnabled: false }; +} + +/** + * Update settings + */ +export async function updateSettings(updates) { + const db = await getDb(); + db.data.settings = { + ...db.data.settings, + ...updates + }; + await db.write(); + return db.data.settings; +} + +/** + * Check if cloud is enabled + */ +export async function isCloudEnabled() { + const settings = await getSettings(); + return settings.cloudEnabled === true; +} + diff --git a/src/lib/oauth/constants/oauth.js b/src/lib/oauth/constants/oauth.js new file mode 100644 index 00000000..db57ee73 --- /dev/null +++ b/src/lib/oauth/constants/oauth.js @@ -0,0 +1,126 @@ +/** + * OAuth Configuration Constants + */ + +// Claude OAuth Configuration (Authorization Code Flow with PKCE) +export const CLAUDE_CONFIG = { + clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", + authorizeUrl: "https://claude.ai/oauth/authorize", + tokenUrl: "https://console.anthropic.com/v1/oauth/token", + scopes: ["org:create_api_key", "user:profile", "user:inference"], + codeChallengeMethod: "S256", +}; + +// Codex (OpenAI) OAuth Configuration (Authorization Code Flow with PKCE) +export const CODEX_CONFIG = { + clientId: "app_EMoamEEZ73f0CkXaXp7hrann", + authorizeUrl: "https://auth.openai.com/oauth/authorize", + tokenUrl: "https://auth.openai.com/oauth/token", + scope: "openid profile email offline_access", + codeChallengeMethod: "S256", + // Additional OpenAI-specific params + extraParams: { + id_token_add_organizations: "true", + codex_cli_simplified_flow: "true", + originator: "codex_cli_rs", + }, +}; + +// Gemini (Google) OAuth Configuration (Standard OAuth2) +export const GEMINI_CONFIG = { + clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com", + clientSecret: "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", + authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth", + tokenUrl: "https://oauth2.googleapis.com/token", + userInfoUrl: "https://www.googleapis.com/oauth2/v1/userinfo", + scopes: [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + ], +}; + +// Qwen OAuth Configuration (Device Code Flow with PKCE) +export const QWEN_CONFIG = { + clientId: "f0304373b74a44d2b584a3fb70ca9e56", + deviceCodeUrl: "https://chat.qwen.ai/api/v1/oauth2/device/code", + tokenUrl: "https://chat.qwen.ai/api/v1/oauth2/token", + scope: "openid profile email model.completion", + codeChallengeMethod: "S256", +}; + +// iFlow OAuth Configuration (Authorization Code) +export const IFLOW_CONFIG = { + clientId: "10009311001", + clientSecret: "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW", + authorizeUrl: "https://iflow.cn/oauth", + tokenUrl: "https://iflow.cn/oauth/token", + userInfoUrl: "https://iflow.cn/api/oauth/getUserInfo", + extraParams: { + loginMethod: "phone", + type: "phone", + }, +}; + +// Antigravity OAuth Configuration (Standard OAuth2 with Google) +export const ANTIGRAVITY_CONFIG = { + clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com", + clientSecret: "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", + authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth", + tokenUrl: "https://oauth2.googleapis.com/token", + userInfoUrl: "https://www.googleapis.com/oauth2/v1/userinfo", + scopes: [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/cclog", + "https://www.googleapis.com/auth/experimentsandconfigs", + ], + // Antigravity specific + loadCodeAssistEndpoint: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", + loadCodeAssistUserAgent: "google-api-nodejs-client/9.15.1", + loadCodeAssistApiClient: "google-cloud-sdk vscode_cloudshelleditor/0.1", + loadCodeAssistClientMetadata: `{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}`, +}; + +// OpenAI OAuth Configuration (Authorization Code Flow with PKCE) +export const OPENAI_CONFIG = { + clientId: "app_EMoamEEZ73f0CkXaXp7hrann", + authorizeUrl: "https://auth.openai.com/oauth/authorize", + tokenUrl: "https://auth.openai.com/oauth/token", + scope: "openid profile email offline_access", + codeChallengeMethod: "S256", + extraParams: { + id_token_add_organizations: "true", + originator: "openai_native", + }, +}; + +// GitHub Copilot OAuth Configuration (Device Code Flow) +export const GITHUB_CONFIG = { + clientId: "Iv1.b507a08c87ecfe98", + deviceCodeUrl: "https://github.com/login/device/code", + tokenUrl: "https://github.com/login/oauth/access_token", + userInfoUrl: "https://api.github.com/user", + scopes: "read:user", + apiVersion: "2022-11-28", // Updated to supported version + copilotTokenUrl: "https://api.github.com/copilot_internal/v2/token", + userAgent: "GitHubCopilotChat/0.26.7", + editorVersion: "vscode/1.85.0", + editorPluginVersion: "copilot-chat/0.26.7", +}; + +// OAuth timeout (5 minutes) +export const OAUTH_TIMEOUT = 300000; + +// Provider list +export const PROVIDERS = { + CLAUDE: "claude", + CODEX: "codex", + GEMINI: "gemini-cli", + QWEN: "qwen", + IFLOW: "iflow", + ANTIGRAVITY: "antigravity", + OPENAI: "openai", + GITHUB: "github", +}; diff --git a/src/lib/oauth/providers.js b/src/lib/oauth/providers.js new file mode 100644 index 00000000..e8251a65 --- /dev/null +++ b/src/lib/oauth/providers.js @@ -0,0 +1,619 @@ +/** + * OAuth Provider Configurations and Handlers + * Centralized DRY approach for all OAuth providers + */ + +import { generatePKCE, generateState } from "./utils/pkce"; +import { + CLAUDE_CONFIG, + CODEX_CONFIG, + GEMINI_CONFIG, + QWEN_CONFIG, + IFLOW_CONFIG, + ANTIGRAVITY_CONFIG, + GITHUB_CONFIG, +} from "./constants/oauth"; + +// Provider configurations +const PROVIDERS = { + claude: { + config: CLAUDE_CONFIG, + flowType: "authorization_code_pkce", + buildAuthUrl: (config, redirectUri, state, codeChallenge) => { + const params = new URLSearchParams({ + code: "true", + client_id: config.clientId, + response_type: "code", + redirect_uri: redirectUri, + scope: config.scopes.join(" "), + code_challenge: codeChallenge, + code_challenge_method: config.codeChallengeMethod, + state: state, + }); + return `${config.authorizeUrl}?${params.toString()}`; + }, + exchangeToken: async (config, code, redirectUri, codeVerifier, state) => { + // Parse code - may contain state after # + let authCode = code; + let codeState = ""; + if (authCode.includes("#")) { + const parts = authCode.split("#"); + authCode = parts[0]; + codeState = parts[1] || ""; + } + + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + code: authCode, + state: codeState || state, + grant_type: "authorization_code", + client_id: config.clientId, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + }, + mapTokens: (tokens) => ({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + scope: tokens.scope, + }), + }, + + codex: { + config: CODEX_CONFIG, + flowType: "authorization_code_pkce", + fixedPort: 1455, + callbackPath: "/auth/callback", + buildAuthUrl: (config, redirectUri, state, codeChallenge) => { + const params = { + response_type: "code", + client_id: config.clientId, + redirect_uri: redirectUri, + scope: config.scope, + code_challenge: codeChallenge, + code_challenge_method: config.codeChallengeMethod, + ...config.extraParams, + state: state, + }; + const queryString = Object.entries(params) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join("&"); + return `${config.authorizeUrl}?${queryString}`; + }, + exchangeToken: async (config, code, redirectUri, codeVerifier) => { + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: config.clientId, + code: code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + }, + mapTokens: (tokens) => ({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + idToken: tokens.id_token, + expiresIn: tokens.expires_in, + }), + }, + + "gemini-cli": { + config: GEMINI_CONFIG, + flowType: "authorization_code", + buildAuthUrl: (config, redirectUri, state) => { + const params = new URLSearchParams({ + client_id: config.clientId, + response_type: "code", + redirect_uri: redirectUri, + scope: config.scopes.join(" "), + state: state, + access_type: "offline", + prompt: "consent", + }); + return `${config.authorizeUrl}?${params.toString()}`; + }, + exchangeToken: async (config, code, redirectUri) => { + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: config.clientId, + client_secret: config.clientSecret, + code: code, + redirect_uri: redirectUri, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + }, + postExchange: async (tokens) => { + // Fetch user info + const userInfoRes = await fetch(`${GEMINI_CONFIG.userInfoUrl}?alt=json`, { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }); + const userInfo = userInfoRes.ok ? await userInfoRes.json() : {}; + + // Fetch project ID + let projectId = ""; + try { + const projectRes = await fetch( + "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", + { + method: "POST", + headers: { + Authorization: `Bearer ${tokens.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" }, + }), + } + ); + if (projectRes.ok) { + const data = await projectRes.json(); + projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || ""; + } + } catch (e) { + console.log("Failed to fetch project ID:", e); + } + + return { userInfo, projectId }; + }, + mapTokens: (tokens, extra) => ({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + scope: tokens.scope, + email: extra?.userInfo?.email, + projectId: extra?.projectId, + }), + }, + + antigravity: { + config: ANTIGRAVITY_CONFIG, + flowType: "authorization_code", + buildAuthUrl: (config, redirectUri, state) => { + const params = new URLSearchParams({ + client_id: config.clientId, + response_type: "code", + redirect_uri: redirectUri, + scope: config.scopes.join(" "), + state: state, + access_type: "offline", + prompt: "consent", + }); + return `${config.authorizeUrl}?${params.toString()}`; + }, + exchangeToken: async (config, code, redirectUri) => { + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: config.clientId, + client_secret: config.clientSecret, + code: code, + redirect_uri: redirectUri, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + }, + postExchange: async (tokens) => { + // Fetch user info + const userInfoRes = await fetch(`${ANTIGRAVITY_CONFIG.userInfoUrl}?alt=json`, { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }); + const userInfo = userInfoRes.ok ? await userInfoRes.json() : {}; + + // Fetch project ID from loadCodeAssist + let projectId = ""; + try { + const projectRes = await fetch(ANTIGRAVITY_CONFIG.loadCodeAssistEndpoint, { + method: "POST", + headers: { + Authorization: `Bearer ${tokens.access_token}`, + "Content-Type": "application/json", + "User-Agent": ANTIGRAVITY_CONFIG.loadCodeAssistUserAgent, + "X-Goog-Api-Client": ANTIGRAVITY_CONFIG.loadCodeAssistApiClient, + "Client-Metadata": ANTIGRAVITY_CONFIG.loadCodeAssistClientMetadata, + }, + body: JSON.stringify({ + metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" }, + }), + }); + if (projectRes.ok) { + const data = await projectRes.json(); + projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || ""; + } + } catch (e) { + console.log("Failed to fetch project ID:", e); + } + + return { userInfo, projectId }; + }, + mapTokens: (tokens, extra) => ({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + scope: tokens.scope, + email: extra?.userInfo?.email, + projectId: extra?.projectId, + }), + }, + + iflow: { + config: IFLOW_CONFIG, + flowType: "authorization_code", + buildAuthUrl: (config, redirectUri, state) => { + const params = new URLSearchParams({ + loginMethod: config.extraParams.loginMethod, + type: config.extraParams.type, + redirect: redirectUri, + state: state, + client_id: config.clientId, + }); + return `${config.authorizeUrl}?${params.toString()}`; + }, + exchangeToken: async (config, code, redirectUri) => { + // Create Basic Auth header + const basicAuth = Buffer.from( + `${config.clientId}:${config.clientSecret}` + ).toString("base64"); + + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + Authorization: `Basic ${basicAuth}`, + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code: code, + redirect_uri: redirectUri, + client_id: config.clientId, + client_secret: config.clientSecret, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + }, + postExchange: async (tokens) => { + // Fetch user info + const userInfoRes = await fetch( + `${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`, + { + headers: { + Accept: "application/json", + }, + } + ); + const result = userInfoRes.ok ? await userInfoRes.json() : {}; + const userInfo = result.success ? result.data : {}; + return { userInfo }; + }, + mapTokens: (tokens, extra) => ({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + apiKey: extra?.userInfo?.apiKey, + email: extra?.userInfo?.email || extra?.userInfo?.phone, + displayName: extra?.userInfo?.nickname || extra?.userInfo?.name, + }), + }, + + qwen: { + config: QWEN_CONFIG, + flowType: "device_code", + requestDeviceCode: async (config, codeChallenge) => { + const response = await fetch(config.deviceCodeUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + client_id: config.clientId, + scope: config.scope, + code_challenge: codeChallenge, + code_challenge_method: config.codeChallengeMethod, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Device code request failed: ${error}`); + } + + return await response.json(); + }, + pollToken: async (config, deviceCode, codeVerifier) => { + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + client_id: config.clientId, + device_code: deviceCode, + code_verifier: codeVerifier, + }), + }); + + return { + ok: response.ok, + data: await response.json(), + }; + }, + mapTokens: (tokens) => ({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + providerSpecificData: { resourceUrl: tokens.resource_url }, + }), + }, + + github: { + config: GITHUB_CONFIG, + flowType: "device_code", + requestDeviceCode: async (config) => { + const response = await fetch(config.deviceCodeUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + client_id: config.clientId, + scope: config.scopes, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Device code request failed: ${error}`); + } + + return await response.json(); + }, + pollToken: async (config, deviceCode) => { + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + client_id: config.clientId, + device_code: deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }), + }); + + // Handle response properly - if not ok, try to get error as text first + let data; + try { + data = await response.json(); + } catch (e) { + // If response is not JSON, get as text + const text = await response.text(); + data = { error: "invalid_response", error_description: text }; + } + + return { + ok: response.ok, + data: data, + }; + }, + postExchange: async (tokens) => { + // Get Copilot token using GitHub access token + const copilotRes = await fetch(GITHUB_CONFIG.copilotTokenUrl, { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + Accept: "application/json", + "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, + "User-Agent": GITHUB_CONFIG.userAgent, + }, + }); + const copilotToken = copilotRes.ok ? await copilotRes.json() : {}; + + // Get user info from GitHub + const userRes = await fetch(GITHUB_CONFIG.userInfoUrl, { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + Accept: "application/json", + "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, + "User-Agent": GITHUB_CONFIG.userAgent, + }, + }); + const userInfo = userRes.ok ? await userRes.json() : {}; + + return { copilotToken, userInfo }; + }, + mapTokens: (tokens, extra) => ({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + providerSpecificData: { + copilotToken: extra?.copilotToken?.token, + copilotTokenExpiresAt: extra?.copilotToken?.expires_at, + githubUserId: extra?.userInfo?.id, + githubLogin: extra?.userInfo?.login, + githubName: extra?.userInfo?.name, + githubEmail: extra?.userInfo?.email, + }, + }), + }, +}; + +/** + * Get provider handler + */ +export function getProvider(name) { + const provider = PROVIDERS[name]; + if (!provider) { + throw new Error(`Unknown provider: ${name}`); + } + return provider; +} + +/** + * Get all provider names + */ +export function getProviderNames() { + return Object.keys(PROVIDERS); +} + +/** + * Generate auth data for a provider + */ +export function generateAuthData(providerName, redirectUri) { + const provider = getProvider(providerName); + const { codeVerifier, codeChallenge, state } = generatePKCE(); + + let authUrl; + if (provider.flowType === "device_code") { + // Device code flow doesn't have auth URL upfront + authUrl = null; + } else if (provider.flowType === "authorization_code_pkce") { + authUrl = provider.buildAuthUrl(provider.config, redirectUri, state, codeChallenge); + } else { + authUrl = provider.buildAuthUrl(provider.config, redirectUri, state); + } + + return { + authUrl, + state, + codeVerifier, + codeChallenge, + redirectUri, + flowType: provider.flowType, + fixedPort: provider.fixedPort, + callbackPath: provider.callbackPath || "/callback", + }; +} + +/** + * Exchange code for tokens + */ +export async function exchangeTokens(providerName, code, redirectUri, codeVerifier, state) { + const provider = getProvider(providerName); + + const tokens = await provider.exchangeToken(provider.config, code, redirectUri, codeVerifier, state); + + let extra = null; + if (provider.postExchange) { + extra = await provider.postExchange(tokens); + } + + return provider.mapTokens(tokens, extra); +} + +/** + * Request device code (for device_code flow) + */ +export async function requestDeviceCode(providerName, codeChallenge) { + const provider = getProvider(providerName); + if (provider.flowType !== "device_code") { + throw new Error(`Provider ${providerName} does not support device code flow`); + } + return await provider.requestDeviceCode(provider.config, codeChallenge); +} + +/** + * Poll for token (for device_code flow) + */ +export async function pollForToken(providerName, deviceCode, codeVerifier) { + const provider = getProvider(providerName); + if (provider.flowType !== "device_code") { + throw new Error(`Provider ${providerName} does not support device code flow`); + } + + const result = await provider.pollToken(provider.config, deviceCode, codeVerifier); + + if (result.ok) { + // For device code flows, success is only when we have an access token + if (result.data.access_token) { + // Call postExchange to get additional data (copilotToken, userInfo, etc.) + let extra = null; + if (provider.postExchange) { + extra = await provider.postExchange(result.data); + } + return { success: true, tokens: provider.mapTokens(result.data, extra) }; + } else { + // Check if it's still pending authorization + if (result.data.error === 'authorization_pending' || result.data.error === 'slow_down') { + // This is not a failure, just still waiting + return { + success: false, + error: result.data.error, + errorDescription: result.data.error_description || result.data.message, + pending: result.data.error === 'authorization_pending' + }; + } else { + // Actual error + return { + success: false, + error: result.data.error || 'no_access_token', + errorDescription: result.data.error_description || result.data.message || 'No access token received' + }; + } + } + } + + return { success: false, error: result.data.error, errorDescription: result.data.error_description }; +} + diff --git a/src/lib/oauth/services/antigravity.js b/src/lib/oauth/services/antigravity.js new file mode 100644 index 00000000..d85def01 --- /dev/null +++ b/src/lib/oauth/services/antigravity.js @@ -0,0 +1,239 @@ +import crypto from "crypto"; +import open from "open"; +import { ANTIGRAVITY_CONFIG } from "../constants/oauth.js"; +import { getServerCredentials } from "../config/index.js"; +import { startLocalServer } from "../utils/server.js"; +import { spinner as createSpinner } from "../utils/ui.js"; + +/** + * Antigravity OAuth Service + * Uses standard OAuth2 Authorization Code flow (similar to Gemini) + */ +export class AntigravityService { + constructor() { + this.config = ANTIGRAVITY_CONFIG; + } + + /** + * Build Antigravity authorization URL + */ + buildAuthUrl(redirectUri, state) { + const params = new URLSearchParams({ + client_id: this.config.clientId, + response_type: "code", + redirect_uri: redirectUri, + scope: this.config.scopes.join(" "), + state: state, + access_type: "offline", + prompt: "consent", + }); + + return `${this.config.authorizeUrl}?${params.toString()}`; + } + + /** + * Exchange authorization code for tokens + */ + async exchangeCode(code, redirectUri) { + const response = await fetch(this.config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: this.config.clientId, + client_secret: this.config.clientSecret, + code: code, + redirect_uri: redirectUri, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + } + + /** + * Get user info from Google + */ + async getUserInfo(accessToken) { + const response = await fetch(`${this.config.userInfoUrl}?alt=json`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get user info: ${error}`); + } + + return await response.json(); + } + + /** + * Fetch Project ID from loadCodeAssist API + */ + async fetchProjectId(accessToken) { + const loadReqBody = { + metadata: { + ideType: "IDE_UNSPECIFIED", + platform: "PLATFORM_UNSPECIFIED", + pluginType: "GEMINI", + }, + }; + + const response = await fetch(this.config.loadCodeAssistEndpoint, { + method: "POST", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + "User-Agent": this.config.loadCodeAssistUserAgent, + "X-Goog-Api-Client": this.config.loadCodeAssistApiClient, + "Client-Metadata": this.config.loadCodeAssistClientMetadata, + }, + body: JSON.stringify(loadReqBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to fetch project ID: ${errorText}`); + } + + const loadResp = await response.json(); + let projectId = loadResp.cloudaicompanionProject; + + if (typeof projectId === 'object' && projectId !== null && projectId.id) { + projectId = projectId.id; + } + + if (!projectId) { + throw new Error("No cloudaicompanionProject found in response"); + } + + return projectId; + } + + /** + * Save Antigravity tokens to server + */ + async saveTokens(tokens, userInfo, projectId) { + const { server, token, userId } = getServerCredentials(); + + const response = await fetch(`${server}/api/cli/providers/antigravity`, { + 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, + scope: tokens.scope, + email: userInfo.email, + projectId: projectId, // Send projectId to server + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to save tokens"); + } + + return await response.json(); + } + + /** + * Complete Antigravity OAuth flow + */ + async connect() { + const spinner = createSpinner("Starting Antigravity OAuth...").start(); + + try { + spinner.text = "Starting local server..."; + + // Start local server for callback + let callbackParams = null; + const { port, close } = await startLocalServer((params) => { + callbackParams = params; + }); + + const redirectUri = `http://localhost:${port}/callback`; + spinner.succeed(`Local server started on port ${port}`); + + // Generate state + const state = crypto.randomBytes(32).toString("base64url"); + + // Build authorization URL + const authUrl = this.buildAuthUrl(redirectUri, state); + + console.log("\nOpening browser for Antigravity authentication..."); + console.log(`If browser doesn't open, visit:\n${authUrl}\n`); + + // Open browser + await open(authUrl); + + // Wait for callback + spinner.start("Waiting for Antigravity 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 + const tokens = await this.exchangeCode(callbackParams.code, redirectUri); + + spinner.text = "Fetching user info..."; + + // Get user info + const userInfo = await this.getUserInfo(tokens.access_token); + + spinner.text = "Fetching Google Cloud Project ID..."; + + // Fetch Project ID + const projectId = await this.fetchProjectId(tokens.access_token); + + spinner.text = "Saving tokens to server..."; + + // Save tokens to server + await this.saveTokens(tokens, userInfo, projectId); + + spinner.succeed(`Antigravity connected successfully! (${userInfo.email}, Project: ${projectId})`); + return true; + } catch (error) { + spinner.fail(`Failed: ${error.message}`); + throw error; + } + } +} + diff --git a/src/lib/oauth/services/claude.js b/src/lib/oauth/services/claude.js new file mode 100644 index 00000000..de713c2e --- /dev/null +++ b/src/lib/oauth/services/claude.js @@ -0,0 +1,136 @@ +import { OAuthService } from "./oauth.js"; +import { CLAUDE_CONFIG } from "../constants/oauth.js"; +import { getServerCredentials } from "../config/index.js"; +import { spinner as createSpinner } from "../utils/ui.js"; + +/** + * Claude OAuth Service + */ +export class ClaudeService extends OAuthService { + constructor() { + super(CLAUDE_CONFIG); + } + + /** + * Build Claude authorization URL + */ + buildClaudeAuthUrl(redirectUri, state, codeChallenge) { + const scopeStr = CLAUDE_CONFIG.scopes.join(" "); + const params = new URLSearchParams({ + code: "true", + client_id: CLAUDE_CONFIG.clientId, + response_type: "code", + redirect_uri: redirectUri, + scope: scopeStr, + code_challenge: codeChallenge, + code_challenge_method: CLAUDE_CONFIG.codeChallengeMethod, + state: state, + }); + + return `${CLAUDE_CONFIG.authorizeUrl}?${params.toString()}`; + } + + /** + * Exchange Claude authorization code (with special handling) + */ + async exchangeClaudeCode(code, redirectUri, codeVerifier, state) { + // Parse code - may contain state after # + let authCode = code; + let codeState = ""; + if (authCode.includes("#")) { + const parts = authCode.split("#"); + authCode = parts[0]; + codeState = parts[1] || ""; + } + + // Claude uses JSON format (not form-urlencoded) + const tokenPayload = { + code: authCode, + state: codeState || state, + grant_type: "authorization_code", + client_id: CLAUDE_CONFIG.clientId, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }; + + const response = await fetch(CLAUDE_CONFIG.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(tokenPayload), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + } + + /** + * Save Claude tokens to server + */ + async saveTokens(tokens) { + const { server, token, userId } = getServerCredentials(); + + // Server will auto-generate displayName based on existing account count + const response = await fetch(`${server}/api/cli/providers/claude`, { + 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, + scope: tokens.scope, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to save tokens"); + } + + return await response.json(); + } + + /** + * Complete Claude OAuth flow + */ + async connect() { + const spinner = createSpinner("Starting Claude OAuth...").start(); + + try { + spinner.text = "Starting local server..."; + + // Authenticate and get authorization code + const { code, state, codeVerifier, redirectUri } = await this.authenticate( + "Claude", + this.buildClaudeAuthUrl.bind(this) + ); + + spinner.start("Exchanging code for tokens..."); + + // Exchange code for tokens + const tokens = await this.exchangeClaudeCode(code, redirectUri, codeVerifier, state); + + spinner.text = "Saving tokens to server..."; + + // Save tokens to server + await this.saveTokens(tokens); + + spinner.succeed("Claude connected successfully!"); + return true; + } catch (error) { + spinner.fail(`Failed: ${error.message}`); + throw error; + } + } +} + diff --git a/src/lib/oauth/services/codex.js b/src/lib/oauth/services/codex.js new file mode 100644 index 00000000..1edce836 --- /dev/null +++ b/src/lib/oauth/services/codex.js @@ -0,0 +1,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, + idToken: tokens.id_token, + expiresIn: tokens.expires_in, + }), + }); + + 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; + } + } +} + diff --git a/src/lib/oauth/services/gemini.js b/src/lib/oauth/services/gemini.js new file mode 100644 index 00000000..1830c94a --- /dev/null +++ b/src/lib/oauth/services/gemini.js @@ -0,0 +1,247 @@ +import crypto from "crypto"; +import open from "open"; +import { GEMINI_CONFIG } from "../constants/oauth.js"; +import { getServerCredentials } from "../config/index.js"; +import { startLocalServer } from "../utils/server.js"; +import { spinner as createSpinner } from "../utils/ui.js"; + +/** + * Gemini CLI (Google Cloud Code Assist) OAuth Service + * Uses standard OAuth2 Authorization Code flow (no PKCE) + */ +export class GeminiCLIService { + constructor() { + this.config = GEMINI_CONFIG; + } + + /** + * Build Gemini CLI authorization URL + */ + buildAuthUrl(redirectUri, state) { + const params = new URLSearchParams({ + client_id: this.config.clientId, + response_type: "code", + redirect_uri: redirectUri, + scope: this.config.scopes.join(" "), + state: state, + access_type: "offline", + prompt: "consent", + }); + + return `${this.config.authorizeUrl}?${params.toString()}`; + } + + /** + * Exchange authorization code for tokens + */ + async exchangeCode(code, redirectUri) { + const response = await fetch(this.config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: this.config.clientId, + client_secret: this.config.clientSecret, + code: code, + redirect_uri: redirectUri, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + } + + /** + * Fetch project ID from Google Cloud Code Assist + */ + async fetchProjectId(accessToken) { + const response = await fetch( + "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", + { + method: "POST", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + "User-Agent": "google-api-nodejs-client/9.15.1", + "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1", + "Client-Metadata": JSON.stringify({ + ideType: "IDE_UNSPECIFIED", + platform: "PLATFORM_UNSPECIFIED", + pluginType: "GEMINI" + }) + }, + body: JSON.stringify({ + metadata: { + ideType: "IDE_UNSPECIFIED", + platform: "PLATFORM_UNSPECIFIED", + pluginType: "GEMINI" + } + }) + } + ); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to fetch project ID: ${error}`); + } + + const data = await response.json(); + + // Extract project ID + let projectId = ""; + if (typeof data.cloudaicompanionProject === "string") { + projectId = data.cloudaicompanionProject.trim(); + } else if (data.cloudaicompanionProject?.id) { + projectId = data.cloudaicompanionProject.id.trim(); + } + + if (!projectId) { + throw new Error("No project ID found in response"); + } + + return projectId; + } + + /** + * Get user info from Google + */ + async getUserInfo(accessToken) { + const response = await fetch(`${this.config.userInfoUrl}?alt=json`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get user info: ${error}`); + } + + return await response.json(); + } + + /** + * Save Gemini CLI tokens to server + */ + async saveTokens(tokens, userInfo, projectId) { + const { server, token, userId } = getServerCredentials(); + + const response = await fetch(`${server}/api/cli/providers/gemini-cli`, { + 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, + scope: tokens.scope, + email: userInfo.email, + projectId: projectId, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to save tokens"); + } + + return await response.json(); + } + + /** + * Complete Gemini OAuth flow + */ + async connect() { + const spinner = createSpinner("Starting Gemini OAuth...").start(); + + try { + spinner.text = "Starting local server..."; + + // Start local server for callback + let callbackParams = null; + const { port, close } = await startLocalServer((params) => { + callbackParams = params; + }); + + const redirectUri = `http://localhost:${port}/callback`; + spinner.succeed(`Local server started on port ${port}`); + + // Generate state + const state = crypto.randomBytes(32).toString("base64url"); + + // Build authorization URL + const authUrl = this.buildAuthUrl(redirectUri, state); + + console.log("\nOpening browser for Google authentication..."); + console.log(`If browser doesn't open, visit:\n${authUrl}\n`); + + // Open browser + await open(authUrl); + + // Wait for callback + spinner.start("Waiting for Google 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 + const tokens = await this.exchangeCode(callbackParams.code, redirectUri); + + spinner.text = "Fetching user info..."; + + // Get user info + const userInfo = await this.getUserInfo(tokens.access_token); + + spinner.text = "Fetching project ID..."; + + // Fetch project ID + const projectId = await this.fetchProjectId(tokens.access_token); + + spinner.text = "Saving tokens to server..."; + + // Save tokens to server + await this.saveTokens(tokens, userInfo, projectId); + + spinner.succeed(`Gemini CLI connected successfully! (${userInfo.email}, Project: ${projectId})`); + return true; + } catch (error) { + spinner.fail(`Failed: ${error.message}`); + throw error; + } + } +} + diff --git a/src/lib/oauth/services/github.js b/src/lib/oauth/services/github.js new file mode 100644 index 00000000..79f1e31e --- /dev/null +++ b/src/lib/oauth/services/github.js @@ -0,0 +1,225 @@ +import { OAuthService } from "./oauth.js"; +import { GITHUB_CONFIG } from "../constants/oauth.js"; +import { spinner as createSpinner } from "../utils/ui.js"; + +/** + * GitHub Copilot OAuth Service + * Uses Device Code Flow for authentication + */ +export class GitHubService extends OAuthService { + constructor() { + super(GITHUB_CONFIG); + } + + /** + * Get device code for GitHub authentication + */ + async getDeviceCode() { + const response = await fetch(`${GITHUB_CONFIG.deviceCodeUrl}`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + client_id: GITHUB_CONFIG.clientId, + scope: GITHUB_CONFIG.scopes, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get device code: ${error}`); + } + + return await response.json(); + } + + /** + * Poll for access token using device code + */ + async pollAccessToken(deviceCode, verificationUri, userCode, interval = 5000) { + const spinner = createSpinner("Waiting for GitHub authentication...").start(); + + // Show user code and verification URL + console.log(`\nPlease visit: ${verificationUri}`); + console.log(`Enter code: ${userCode}\n`); + + // Open browser automatically + try { + const open = (await import("open")).default; + await open(verificationUri); + } catch (error) { + console.log("Could not open browser automatically. Please visit the URL above manually."); + } + + // Poll for access token + while (true) { + await new Promise(resolve => setTimeout(resolve, interval)); + + const response = await fetch(`${GITHUB_CONFIG.tokenUrl}`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + client_id: GITHUB_CONFIG.clientId, + device_code: deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }), + }); + + const data = await response.json(); + + if (data.access_token) { + spinner.succeed("GitHub authentication successful!"); + return { + access_token: data.access_token, + token_type: data.token_type, + scope: data.scope, + }; + } else if (data.error === "authorization_pending") { + // Continue polling + continue; + } else if (data.error === "slow_down") { + // Increase polling interval + interval += 5000; + continue; + } else if (data.error === "expired_token") { + spinner.fail("Device code expired. Please try again."); + throw new Error("Device code expired"); + } else if (data.error === "access_denied") { + spinner.fail("Access denied by user."); + throw new Error("Access denied"); + } else { + spinner.fail("Failed to get access token."); + throw new Error(data.error_description || data.error); + } + } + } + + /** + * Get Copilot token using GitHub access token + */ + async getCopilotToken(accessToken) { + const response = await fetch(`${GITHUB_CONFIG.copilotTokenUrl}`, { + headers: { + Authorization: `Bearer ${accessToken}`, // GitHub API typically uses Bearer + Accept: "application/json", + "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, + "User-Agent": GITHUB_CONFIG.userAgent, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get Copilot token: ${error}`); + } + + return await response.json(); + } + + /** + * Get user info using GitHub access token + */ + async getUserInfo(accessToken) { + const response = await fetch(`${GITHUB_CONFIG.userInfoUrl}`, { + headers: { + Authorization: `Bearer ${accessToken}`, // GitHub API typically uses Bearer + Accept: "application/json", + "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, + "User-Agent": GITHUB_CONFIG.userAgent, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get user info: ${error}`); + } + + return await response.json(); + } + + /** + * Complete GitHub Copilot authentication flow + */ + async authenticate() { + try { + // Get device code + const deviceResponse = await this.getDeviceCode(); + + // Poll for access token + const tokenResponse = await this.pollAccessToken( + deviceResponse.device_code, + deviceResponse.verification_uri, + deviceResponse.user_code + ); + + // Get Copilot token + const copilotToken = await this.getCopilotToken(tokenResponse.access_token); + + // Get user info + const userInfo = await this.getUserInfo(tokenResponse.access_token); + + console.log(`\n✅ Successfully authenticated as ${userInfo.login}`); + + return { + accessToken: tokenResponse.access_token, + copilotToken: copilotToken.token, + refreshToken: null, // GitHub device flow doesn't return refresh token + expiresIn: copilotToken.expires_at, + userInfo: { + id: userInfo.id, + login: userInfo.login, + name: userInfo.name, + email: userInfo.email, + }, + copilotTokenInfo: copilotToken, + }; + } catch (error) { + throw new Error(`GitHub authentication failed: ${error.message}`); + } + } + + /** + * Connect to server with GitHub credentials + */ + async connect() { + try { + // Authenticate with GitHub + const authResult = await this.authenticate(); + + // Send credentials to server + const { server, token, userId } = await import("../config/index.js").then(m => m.getServerCredentials()); + const spinner = (await import("../utils/ui.js")).spinner("Connecting to server...").start(); + + const response = await fetch(`${server}/api/cli/providers/github`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-User-Id": userId, + }, + body: JSON.stringify({ + accessToken: authResult.accessToken, + copilotToken: authResult.copilotToken, + userInfo: authResult.userInfo, + copilotTokenInfo: authResult.copilotTokenInfo, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to connect to server"); + } + + spinner.succeed("GitHub Copilot connected successfully!"); + console.log(`\nConnected as: ${authResult.userInfo.login}`); + } catch (error) { + const { error: showError } = await import("../utils/ui.js"); + showError(`GitHub connection failed: ${error.message}`); + throw error; + } + } +} diff --git a/src/lib/oauth/services/iflow.js b/src/lib/oauth/services/iflow.js new file mode 100644 index 00000000..307522e0 --- /dev/null +++ b/src/lib/oauth/services/iflow.js @@ -0,0 +1,202 @@ +import crypto from "crypto"; +import open from "open"; +import { IFLOW_CONFIG } from "../constants/oauth.js"; +import { getServerCredentials } from "../config/index.js"; +import { startLocalServer } from "../utils/server.js"; +import { spinner as createSpinner } from "../utils/ui.js"; + +/** + * iFlow OAuth Service + * Uses Authorization Code flow with Basic Auth + */ +export class IFlowService { + constructor() { + this.config = IFLOW_CONFIG; + } + + /** + * Build iFlow authorization URL + */ + buildAuthUrl(redirectUri, state) { + const params = new URLSearchParams({ + loginMethod: this.config.extraParams.loginMethod, + type: this.config.extraParams.type, + redirect: redirectUri, + state: state, + client_id: this.config.clientId, + }); + + return `${this.config.authorizeUrl}?${params.toString()}`; + } + + /** + * Exchange authorization code for tokens + */ + async exchangeCode(code, redirectUri) { + // Create Basic Auth header + const basicAuth = Buffer.from( + `${this.config.clientId}:${this.config.clientSecret}` + ).toString("base64"); + + const response = await fetch(this.config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + Authorization: `Basic ${basicAuth}`, + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code: code, + redirect_uri: redirectUri, + client_id: this.config.clientId, + client_secret: this.config.clientSecret, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + } + + /** + * Get user info from iFlow + */ + async getUserInfo(accessToken) { + const response = await fetch( + `${this.config.userInfoUrl}?accessToken=${encodeURIComponent(accessToken)}`, + { + headers: { + Accept: "application/json", + }, + } + ); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get user info: ${error}`); + } + + const result = await response.json(); + + if (!result.success) { + throw new Error("Failed to get user info"); + } + + return result.data; + } + + /** + * Save iFlow tokens to server + */ + async saveTokens(tokens, userInfo) { + const { server, token, userId } = getServerCredentials(); + + const response = await fetch(`${server}/api/cli/providers/iflow`, { + 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, + apiKey: userInfo.apiKey, + email: userInfo.email || userInfo.phone, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to save tokens"); + } + + return await response.json(); + } + + /** + * Complete iFlow OAuth flow + */ + async connect() { + const spinner = createSpinner("Starting iFlow OAuth...").start(); + + try { + spinner.text = "Starting local server..."; + + // Start local server for callback + let callbackParams = null; + const { port, close } = await startLocalServer((params) => { + callbackParams = params; + }); + + const redirectUri = `http://localhost:${port}/callback`; + spinner.succeed(`Local server started on port ${port}`); + + // Generate state + const state = crypto.randomBytes(32).toString("base64url"); + + // Build authorization URL + const authUrl = this.buildAuthUrl(redirectUri, state); + + console.log("\nOpening browser for iFlow authentication..."); + console.log(`If browser doesn't open, visit:\n${authUrl}\n`); + + // Open browser + await open(authUrl); + + // Wait for callback + spinner.start("Waiting for iFlow 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 + const tokens = await this.exchangeCode(callbackParams.code, redirectUri); + + spinner.text = "Fetching user info..."; + + // Get user info (includes API key) + const userInfo = await this.getUserInfo(tokens.access_token); + + spinner.text = "Saving tokens to server..."; + + // Save tokens to server + await this.saveTokens(tokens, userInfo); + + spinner.succeed(`iFlow connected successfully! (${userInfo.email || userInfo.phone})`); + return true; + } catch (error) { + spinner.fail(`Failed: ${error.message}`); + throw error; + } + } +} + diff --git a/src/lib/oauth/services/index.js b/src/lib/oauth/services/index.js new file mode 100644 index 00000000..b3d82842 --- /dev/null +++ b/src/lib/oauth/services/index.js @@ -0,0 +1,14 @@ +/** + * Export all services + */ + +export { OAuthService } from "./oauth.js"; +export { ClaudeService } from "./claude.js"; +export { CodexService } from "./codex.js"; +export { GeminiCLIService } from "./gemini.js"; +export { QwenService } from "./qwen.js"; +export { IFlowService } from "./iflow.js"; +export { AntigravityService } from "./antigravity.js"; +export { OpenAIService } from "./openai.js"; +export { GitHubService } from "./github.js"; + diff --git a/src/lib/oauth/services/oauth.js b/src/lib/oauth/services/oauth.js new file mode 100644 index 00000000..f056171d --- /dev/null +++ b/src/lib/oauth/services/oauth.js @@ -0,0 +1,157 @@ +import open from "open"; +import { startLocalServer } from "../utils/server.js"; +import { generatePKCE } from "../utils/pkce.js"; +import { spinner as createSpinner } from "../utils/ui.js"; +import { OAUTH_TIMEOUT } from "../constants/oauth.js"; + +/** + * Generic OAuth Authorization Code Flow with PKCE + */ +export class OAuthService { + constructor(config) { + this.config = config; + } + + /** + * Build authorization URL + */ + buildAuthUrl(redirectUri, state, codeChallenge, extraParams = {}) { + const params = new URLSearchParams({ + client_id: this.config.clientId, + response_type: "code", + redirect_uri: redirectUri, + state: state, + code_challenge: codeChallenge, + code_challenge_method: this.config.codeChallengeMethod, + ...extraParams, + }); + + return `${this.config.authorizeUrl}?${params.toString()}`; + } + + /** + * Start local server and wait for callback + */ + async startAuthFlow(authUrl, providerName) { + const spinner = createSpinner("Starting local server...").start(); + + // Start local server for callback + let callbackParams = null; + const { port, close } = await startLocalServer((params) => { + callbackParams = params; + }); + + const redirectUri = `http://localhost:${port}/callback`; + spinner.succeed(`Local server started on port ${port}`); + + return { + redirectUri, + port, + close, + waitForCallback: async () => { + spinner.start(`Waiting for ${providerName} authorization...`); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Authentication timeout (5 minutes)")); + }, OAUTH_TIMEOUT); + + const checkInterval = setInterval(() => { + if (callbackParams) { + clearInterval(checkInterval); + clearTimeout(timeout); + resolve(); + } + }, 100); + }); + + spinner.stop(); + close(); + + if (callbackParams.error) { + throw new Error(callbackParams.error_description || callbackParams.error); + } + + if (!callbackParams.code) { + throw new Error("No authorization code received"); + } + + return callbackParams; + }, + }; + } + + /** + * Exchange authorization code for tokens + */ + async exchangeCode(code, redirectUri, codeVerifier, contentType = "application/x-www-form-urlencoded") { + const body = + contentType === "application/json" + ? JSON.stringify({ + grant_type: "authorization_code", + client_id: this.config.clientId, + code: code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }) + : new URLSearchParams({ + grant_type: "authorization_code", + client_id: this.config.clientId, + code: code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }); + + const response = await fetch(this.config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": contentType, + Accept: "application/json", + }, + body: body, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + } + + /** + * Complete OAuth flow + */ + async authenticate(providerName, buildAuthUrlFn) { + // Generate PKCE + const { codeVerifier, codeChallenge, state } = generatePKCE(); + + // Start local server and get redirect URI + const { redirectUri, waitForCallback } = await this.startAuthFlow(null, providerName); + + // Build authorization URL + const authUrl = buildAuthUrlFn(redirectUri, state, codeChallenge); + + console.log(`\nOpening browser for ${providerName} authentication...`); + console.log(`If browser doesn't open, visit:\n${authUrl}\n`); + + // Open browser + await open(authUrl); + + // Wait for callback + const callbackParams = await waitForCallback(); + + // Validate state + if (callbackParams.state !== state) { + throw new Error("Invalid state parameter"); + } + + return { + code: callbackParams.code, + state: callbackParams.state, + codeVerifier, + redirectUri, + }; + } +} + diff --git a/src/lib/oauth/services/openai.js b/src/lib/oauth/services/openai.js new file mode 100644 index 00000000..851260a9 --- /dev/null +++ b/src/lib/oauth/services/openai.js @@ -0,0 +1,123 @@ +import { OAuthService } from "./oauth.js"; +import { OPENAI_CONFIG } from "../constants/oauth.js"; +import { getServerCredentials } from "../config/index.js"; +import { spinner as createSpinner } from "../utils/ui.js"; + +/** + * OpenAI OAuth Service (Native) + * Uses Authorization Code Flow with PKCE (similar to Codex) + */ +export class OpenAIService extends OAuthService { + constructor() { + super(OPENAI_CONFIG); + } + + /** + * Build OpenAI authorization URL + */ + buildOpenAIAuthUrl(redirectUri, state, codeChallenge) { + const params = new URLSearchParams({ + client_id: OPENAI_CONFIG.clientId, + response_type: "code", + redirect_uri: redirectUri, + scope: OPENAI_CONFIG.scope, + state: state, + code_challenge: codeChallenge, + code_challenge_method: OPENAI_CONFIG.codeChallengeMethod, + ...OPENAI_CONFIG.extraParams, + }); + + return `${OPENAI_CONFIG.authorizeUrl}?${params.toString()}`; + } + + /** + * Exchange OpenAI authorization code for tokens + */ + async exchangeOpenAICode(code, redirectUri, codeVerifier) { + const response = await fetch(OPENAI_CONFIG.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: OPENAI_CONFIG.clientId, + code: code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return await response.json(); + } + + /** + * Save OpenAI tokens to server + */ + async saveTokens(tokens) { + const { server, token, userId } = getServerCredentials(); + + const response = await fetch(`${server}/api/cli/providers/openai`, { + 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, + idToken: tokens.id_token, + scope: tokens.scope, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to save tokens"); + } + + return await response.json(); + } + + /** + * Complete OpenAI OAuth flow + */ + async connect() { + const spinner = createSpinner("Starting OpenAI OAuth...").start(); + + try { + spinner.text = "Starting local server..."; + + // Authenticate and get authorization code + const { code, codeVerifier, redirectUri } = await this.authenticate( + "OpenAI", + this.buildOpenAIAuthUrl.bind(this) + ); + + spinner.start("Exchanging code for tokens..."); + + // Exchange code for tokens + const tokens = await this.exchangeOpenAICode(code, redirectUri, codeVerifier); + + spinner.text = "Saving tokens to server..."; + + // Save tokens to server + await this.saveTokens(tokens); + + spinner.succeed("OpenAI connected successfully!"); + return true; + } catch (error) { + spinner.fail(`Failed: ${error.message}`); + throw error; + } + } +} + diff --git a/src/lib/oauth/services/qwen.js b/src/lib/oauth/services/qwen.js new file mode 100644 index 00000000..ee4489ea --- /dev/null +++ b/src/lib/oauth/services/qwen.js @@ -0,0 +1,170 @@ +import open from "open"; +import { QWEN_CONFIG } from "../constants/oauth.js"; +import { getServerCredentials } from "../config/index.js"; +import { generatePKCE } from "../utils/pkce.js"; +import { spinner as createSpinner } from "../utils/ui.js"; + +/** + * Qwen OAuth Service + * Uses Device Code Flow with PKCE + */ +export class QwenService { + constructor() { + this.config = QWEN_CONFIG; + } + + /** + * Request device code + */ + async requestDeviceCode(codeChallenge) { + const response = await fetch(this.config.deviceCodeUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + client_id: this.config.clientId, + scope: this.config.scope, + code_challenge: codeChallenge, + code_challenge_method: this.config.codeChallengeMethod, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Device code request failed: ${error}`); + } + + return await response.json(); + } + + /** + * Poll for token + */ + async pollForToken(deviceCode, codeVerifier, interval = 5) { + const maxAttempts = 60; // 5 minutes + const pollInterval = interval * 1000; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + await new Promise((r) => setTimeout(r, pollInterval)); + + const response = await fetch(this.config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + client_id: this.config.clientId, + device_code: deviceCode, + code_verifier: codeVerifier, + }), + }); + + if (response.ok) { + return await response.json(); + } + + const error = await response.json(); + + if (error.error === "authorization_pending") { + continue; + } else if (error.error === "slow_down") { + await new Promise((r) => setTimeout(r, 5000)); + continue; + } else if (error.error === "expired_token") { + throw new Error("Device code expired"); + } else if (error.error === "access_denied") { + throw new Error("Access denied"); + } else { + throw new Error(error.error_description || error.error); + } + } + + throw new Error("Authorization timeout"); + } + + /** + * Save Qwen tokens to server + */ + async saveTokens(tokens) { + const { server, token, userId } = getServerCredentials(); + + const response = await fetch(`${server}/api/cli/providers/qwen`, { + 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, + resourceUrl: tokens.resource_url, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to save tokens"); + } + + return await response.json(); + } + + /** + * Complete Qwen OAuth flow + */ + async connect() { + const spinner = createSpinner("Starting Qwen OAuth...").start(); + + try { + spinner.text = "Generating PKCE..."; + + // Generate PKCE + const { codeVerifier, codeChallenge } = generatePKCE(); + + spinner.text = "Requesting device code..."; + + // Request device code + const deviceData = await this.requestDeviceCode(codeChallenge); + + spinner.stop(); + + console.log("\n📋 Please visit the following URL and enter the code:\n"); + console.log(` ${deviceData.verification_uri}\n`); + console.log(` Code: ${deviceData.user_code}\n`); + + // Open browser + if (deviceData.verification_uri_complete) { + await open(deviceData.verification_uri_complete); + } else { + await open(deviceData.verification_uri); + } + + spinner.start("Waiting for authorization..."); + + // Poll for token + const tokens = await this.pollForToken( + deviceData.device_code, + codeVerifier, + deviceData.interval || 5 + ); + + spinner.text = "Saving tokens to server..."; + + // Save tokens to server + await this.saveTokens(tokens); + + spinner.succeed("Qwen connected successfully!"); + return true; + } catch (error) { + spinner.fail(`Failed: ${error.message}`); + throw error; + } + } +} + diff --git a/src/lib/oauth/utils/banner.js b/src/lib/oauth/utils/banner.js new file mode 100644 index 00000000..a5743e2d --- /dev/null +++ b/src/lib/oauth/utils/banner.js @@ -0,0 +1,63 @@ +import figlet from "figlet"; +import gradient from "gradient-string"; +import chalkAnimation from "chalk-animation"; + +/** + * Display banner + */ +export function showBanner() { + const banner = figlet.textSync("LLM Proxy", { + font: "ANSI Shadow", + horizontalLayout: "default", + verticalLayout: "default", + }); + + console.log("\n" + gradient.pastel.multiline(banner)); + console.log(gradient.cristal(" 🚀 OAuth CLI for AI Providers\n")); +} + +/** + * Display simple banner (no animation) + */ +export function showSimpleBanner() { + const banner = figlet.textSync("EP CLI", { + font: "Standard", + horizontalLayout: "default", + }); + console.log(gradient.pastel.multiline(banner)); + console.log(gradient.cristal(" OAuth CLI for AI Providers\n")); +} + +/** + * Display success animation + */ +export async function showSuccess(message) { + return new Promise((resolve) => { + const animation = chalkAnimation.rainbow(`\n✨ ${message}\n`); + setTimeout(() => { + animation.stop(); + resolve(); + }, 1000); + }); +} + +/** + * Display loading animation + */ +export function showLoading(text) { + const frames = ["⠋", "⠙", "â š", "â ¸", "â ŧ", "â ´", "â Ļ", "â §", "⠇", "⠏"]; + let i = 0; + + const interval = setInterval(() => { + process.stdout.write(`\r${frames[i]} ${text}`); + i = (i + 1) % frames.length; + }, 80); + + return { + stop: () => { + clearInterval(interval); + process.stdout.write("\r"); + }, + }; +} + diff --git a/src/lib/oauth/utils/pkce.js b/src/lib/oauth/utils/pkce.js new file mode 100644 index 00000000..260096c7 --- /dev/null +++ b/src/lib/oauth/utils/pkce.js @@ -0,0 +1,38 @@ +import crypto from "crypto"; + +/** + * Generate PKCE code verifier (43-128 characters) + */ +export function generateCodeVerifier() { + return crypto.randomBytes(32).toString("base64url"); +} + +/** + * Generate PKCE code challenge from verifier (S256 method) + */ +export function generateCodeChallenge(verifier) { + return crypto.createHash("sha256").update(verifier).digest("base64url"); +} + +/** + * Generate random state for CSRF protection + */ +export function generateState() { + return crypto.randomBytes(32).toString("base64url"); +} + +/** + * Generate complete PKCE pair + */ +export function generatePKCE() { + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + const state = generateState(); + + return { + codeVerifier, + codeChallenge, + state, + }; +} + diff --git a/src/lib/oauth/utils/server.js b/src/lib/oauth/utils/server.js new file mode 100644 index 00000000..e3013fc6 --- /dev/null +++ b/src/lib/oauth/utils/server.js @@ -0,0 +1,116 @@ +import http from "http"; +import { URL } from "url"; + +/** + * Start a local HTTP server to receive OAuth callback + * @param {Function} onCallback - Called with query params when callback received + * @param {number} fixedPort - Optional fixed port number (default: random) + * @returns {Promise<{server: http.Server, port: number, close: Function}>} + */ +export function startLocalServer(onCallback, fixedPort = null) { + return new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + const url = new URL(req.url, `http://localhost`); + + if (url.pathname === "/callback" || url.pathname === "/auth/callback") { + const params = Object.fromEntries(url.searchParams); + + // Send success response to browser with auto-close attempt + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); + res.end(` + + + + Authentication Successful + + + +
+
+

Authentication Successful

+

Closing in 3 seconds...

+
+ + +`); + + // Call callback with params + onCallback(params); + } else { + res.writeHead(404); + res.end("Not found"); + } + }); + + // Listen on fixed port or find available port + const portToUse = fixedPort || 0; + server.listen(portToUse, "127.0.0.1", () => { + const { port } = server.address(); + resolve({ + server, + port, + close: () => server.close(), + }); + }); + + server.on("error", (err) => { + if (err.code === "EADDRINUSE" && fixedPort) { + reject(new Error(`Port ${fixedPort} is already in use. Please close other applications using this port.`)); + } else { + reject(err); + } + }); + }); +} + +/** + * Wait for callback with timeout + * @param {number} timeoutMs - Timeout in milliseconds + * @returns {Promise} - Callback params + */ +export function waitForCallback(timeoutMs = 300000) { + return new Promise((resolve, reject) => { + let resolved = false; + + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + reject(new Error("Authentication timeout")); + } + }, timeoutMs); + + const onCallback = (params) => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + resolve(params); + } + }; + + // Return the callback function + resolve.__onCallback = onCallback; + }); +} + diff --git a/src/lib/oauth/utils/ui.js b/src/lib/oauth/utils/ui.js new file mode 100644 index 00000000..a81ff48e --- /dev/null +++ b/src/lib/oauth/utils/ui.js @@ -0,0 +1,48 @@ +import chalk from "chalk"; +import ora from "ora"; + +/** + * UI Helper Functions + */ + +export function success(message) { + console.log(chalk.green(`\n✓ ${message}\n`)); +} + +export function error(message) { + console.log(chalk.red(`\n✗ ${message}\n`)); +} + +export function info(message) { + console.log(chalk.blue(`\n${message}\n`)); +} + +export function warn(message) { + console.log(chalk.yellow(`\n⚠ ${message}\n`)); +} + +export function gray(message) { + console.log(chalk.gray(message)); +} + +export function spinner(text) { + return ora(text); +} + +export function printSection(title) { + console.log(chalk.blue(`\n${title}\n`)); +} + +export function printKeyValue(key, value, isSuccess = false) { + const color = isSuccess ? chalk.green : chalk.gray; + console.log(color(` ${key}: ${value}`)); +} + +export function printList(items, isSuccess = false) { + const symbol = isSuccess ? "✓" : "✗"; + const color = isSuccess ? chalk.green : chalk.gray; + items.forEach((item) => { + console.log(color(` ${symbol} ${item}`)); + }); +} + diff --git a/src/lib/usage/fetcher.js b/src/lib/usage/fetcher.js new file mode 100644 index 00000000..683bceca --- /dev/null +++ b/src/lib/usage/fetcher.js @@ -0,0 +1,202 @@ +/** + * Usage Fetcher - Get usage data from provider APIs + */ + +import { GITHUB_CONFIG, GEMINI_CONFIG, ANTIGRAVITY_CONFIG } from "@/lib/oauth/constants/oauth"; + +/** + * Get usage data for a provider connection + * @param {Object} connection - Provider connection with accessToken + * @returns {Object} Usage data with quotas + */ +export async function getUsageForProvider(connection) { + const { provider, accessToken, providerSpecificData } = connection; + + switch (provider) { + case "github": + return await getGitHubUsage(accessToken, providerSpecificData); + case "gemini-cli": + return await getGeminiUsage(accessToken); + case "antigravity": + return await getAntigravityUsage(accessToken); + case "claude": + return await getClaudeUsage(accessToken); + case "codex": + return await getCodexUsage(accessToken); + case "qwen": + return await getQwenUsage(accessToken, providerSpecificData); + case "iflow": + return await getIflowUsage(accessToken); + default: + return { message: `Usage API not implemented for ${provider}` }; + } +} + +/** + * GitHub Copilot Usage + */ +async function getGitHubUsage(accessToken, providerSpecificData) { + try { + const response = await fetch("https://api.github.com/copilot_internal/user", { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, + "User-Agent": GITHUB_CONFIG.userAgent, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`GitHub API error: ${error}`); + } + + const data = await response.json(); + + // Handle different response formats (paid vs free) + if (data.quota_snapshots) { + // Paid plan format + const snapshots = data.quota_snapshots; + return { + plan: data.copilot_plan, + resetDate: data.quota_reset_date, + quotas: { + chat: formatGitHubQuotaSnapshot(snapshots.chat), + completions: formatGitHubQuotaSnapshot(snapshots.completions), + premium_interactions: formatGitHubQuotaSnapshot(snapshots.premium_interactions), + }, + }; + } else if (data.monthly_quotas || data.limited_user_quotas) { + // Free/limited plan format + const monthlyQuotas = data.monthly_quotas || {}; + const usedQuotas = data.limited_user_quotas || {}; + + return { + plan: data.copilot_plan || data.access_type_sku, + resetDate: data.limited_user_reset_date, + quotas: { + chat: { + used: usedQuotas.chat || 0, + total: monthlyQuotas.chat || 0, + unlimited: false, + }, + completions: { + used: usedQuotas.completions || 0, + total: monthlyQuotas.completions || 0, + unlimited: false, + }, + }, + }; + } + + return { message: "GitHub Copilot connected. Unable to parse quota data." }; + } catch (error) { + throw new Error(`Failed to fetch GitHub usage: ${error.message}`); + } +} + +function formatGitHubQuotaSnapshot(quota) { + if (!quota) return { used: 0, total: 0, unlimited: true }; + + return { + used: quota.entitlement - quota.remaining, + total: quota.entitlement, + remaining: quota.remaining, + unlimited: quota.unlimited || false, + }; +} + +/** + * Gemini CLI Usage (Google Cloud) + */ +async function getGeminiUsage(accessToken) { + try { + // Gemini CLI uses Google Cloud quotas + // Try to get quota info from Cloud Resource Manager + const response = await fetch( + "https://cloudresourcemanager.googleapis.com/v1/projects?filter=lifecycleState:ACTIVE", + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + } + ); + + if (!response.ok) { + // Quota API may not be accessible, return generic message + return { message: "Gemini CLI uses Google Cloud quotas. Check Google Cloud Console for details." }; + } + + return { message: "Gemini CLI connected. Usage tracked via Google Cloud Console." }; + } catch (error) { + return { message: "Unable to fetch Gemini usage. Check Google Cloud Console." }; + } +} + +/** + * Antigravity Usage + */ +async function getAntigravityUsage(accessToken) { + try { + // Similar to Gemini, uses Google Cloud + return { message: "Antigravity connected. Usage tracked via Google Cloud Console." }; + } catch (error) { + return { message: "Unable to fetch Antigravity usage." }; + } +} + +/** + * Claude Usage + */ +async function getClaudeUsage(accessToken) { + try { + // Claude OAuth doesn't expose usage API directly + // Could potentially check via inference endpoint + return { message: "Claude connected. Usage tracked per request." }; + } catch (error) { + return { message: "Unable to fetch Claude usage." }; + } +} + +/** + * Codex (OpenAI) Usage + */ +async function getCodexUsage(accessToken) { + try { + // OpenAI usage requires organization API access + return { message: "Codex connected. Check OpenAI dashboard for usage." }; + } catch (error) { + return { message: "Unable to fetch Codex usage." }; + } +} + +/** + * Qwen Usage + */ +async function getQwenUsage(accessToken, providerSpecificData) { + try { + const resourceUrl = providerSpecificData?.resourceUrl; + if (!resourceUrl) { + return { message: "Qwen connected. No resource URL available." }; + } + + // Qwen may have usage endpoint at resource URL + return { message: "Qwen connected. Usage tracked per request." }; + } catch (error) { + return { message: "Unable to fetch Qwen usage." }; + } +} + +/** + * iFlow Usage + */ +async function getIflowUsage(accessToken) { + try { + // iFlow may have usage endpoint + return { message: "iFlow connected. Usage tracked per request." }; + } catch (error) { + return { message: "Unable to fetch iFlow usage." }; + } +} + diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 00000000..4319e157 --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,16 @@ +// Database Models - Export all from localDb +export { + getProviderConnections, + getProviderConnectionById, + createProviderConnection, + updateProviderConnection, + deleteProviderConnection, + getModelAliases, + setModelAlias, + deleteModelAlias, + getApiKeys, + createApiKey, + deleteApiKey, + validateApiKey, + isCloudEnabled, +} from "@/lib/localDb"; diff --git a/src/server-init.js b/src/server-init.js new file mode 100644 index 00000000..4c46a548 --- /dev/null +++ b/src/server-init.js @@ -0,0 +1,21 @@ +// Server startup script +import initializeCloudSync from "./shared/services/initializeCloudSync.js"; + +async function startServer() { + console.log("Starting server with cloud sync..."); + + try { + // Initialize cloud sync + await initializeCloudSync(); + console.log("Server started with cloud sync initialized"); + } catch (error) { + console.log("Error initializing cloud sync:", error); + process.exit(1); + } +} + +// Start the server initialization +startServer().catch(console.log); + +// Export for use as module if needed +export default startServer; diff --git a/src/shared/components/Avatar.js b/src/shared/components/Avatar.js new file mode 100644 index 00000000..ae0a7552 --- /dev/null +++ b/src/shared/components/Avatar.js @@ -0,0 +1,88 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +export default function Avatar({ + src, + alt = "Avatar", + name, + size = "md", + className, +}) { + const sizes = { + xs: "size-6 text-xs", + sm: "size-8 text-sm", + md: "size-10 text-base", + lg: "size-12 text-lg", + xl: "size-16 text-xl", + }; + + // Get initials from name + const getInitials = (name) => { + if (!name) return "?"; + const parts = name.split(" "); + if (parts.length >= 2) { + return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); + } + return name.substring(0, 2).toUpperCase(); + }; + + // Generate color from name + const getColorFromName = (name) => { + if (!name) return "bg-primary"; + const colors = [ + "bg-red-500", + "bg-orange-500", + "bg-amber-500", + "bg-yellow-500", + "bg-lime-500", + "bg-green-500", + "bg-emerald-500", + "bg-teal-500", + "bg-cyan-500", + "bg-sky-500", + "bg-blue-500", + "bg-indigo-500", + "bg-violet-500", + "bg-purple-500", + "bg-fuchsia-500", + "bg-pink-500", + "bg-rose-500", + ]; + const index = name.charCodeAt(0) % colors.length; + return colors[index]; + }; + + if (src) { + return ( +
+ ); + } + + return ( +
+ {getInitials(name)} +
+ ); +} + diff --git a/src/shared/components/Badge.js b/src/shared/components/Badge.js new file mode 100644 index 00000000..1a412a74 --- /dev/null +++ b/src/shared/components/Badge.js @@ -0,0 +1,55 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +const variants = { + default: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300", + primary: "bg-primary/10 text-primary", + success: "bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border border-green-100 dark:border-green-800/30", + warning: "bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-500 border border-yellow-100 dark:border-yellow-800/30", + error: "bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border border-red-100 dark:border-red-800/30", + info: "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border border-blue-100 dark:border-blue-800/30", +}; + +const sizes = { + sm: "px-2 py-0.5 text-[10px]", + md: "px-2.5 py-1 text-xs", + lg: "px-3 py-1.5 text-sm", +}; + +export default function Badge({ + children, + variant = "default", + size = "md", + dot = false, + icon, + className, +}) { + return ( + + {dot && ( + + )} + {icon && {icon}} + {children} + + ); +} + diff --git a/src/shared/components/Button.js b/src/shared/components/Button.js new file mode 100644 index 00000000..bb5e8ce3 --- /dev/null +++ b/src/shared/components/Button.js @@ -0,0 +1,56 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +const variants = { + primary: "bg-primary text-white hover:bg-primary-hover shadow-warm", + secondary: "bg-surface border border-border text-text-main hover:bg-black/5 shadow-sm", + outline: "border border-border text-text-main hover:bg-black/5", + ghost: "text-text-muted hover:bg-black/5 hover:text-text-main", + danger: "bg-red-500 text-white hover:bg-red-600 shadow-sm", +}; + +const sizes = { + sm: "h-8 px-3 text-xs rounded-md", + md: "h-10 px-5 text-sm rounded-lg", + lg: "h-12 px-8 text-base rounded-xl", +}; + +export default function Button({ + children, + variant = "primary", + size = "md", + icon, + iconRight, + disabled = false, + loading = false, + fullWidth = false, + className, + ...props +}) { + return ( + + ); +} + diff --git a/src/shared/components/Card.js b/src/shared/components/Card.js new file mode 100644 index 00000000..e77858f3 --- /dev/null +++ b/src/shared/components/Card.js @@ -0,0 +1,92 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +export default function Card({ + children, + title, + subtitle, + icon, + action, + padding = "md", + hover = false, + className, + ...props +}) { + const paddings = { + none: "", + sm: "p-4", + md: "p-6", + lg: "p-8", + }; + + return ( +
+ {(title || action) && ( +
+
+ {icon && ( +
+ {icon} +
+ )} +
+ {title && ( +

{title}

+ )} + {subtitle && ( +

{subtitle}

+ )} +
+
+ {action} +
+ )} + {children} +
+ ); +} + +// Sub-component: Bordered section inside Card +Card.Section = function CardSection({ children, className, ...props }) { + return ( +
+ {children} +
+ ); +}; + +// Sub-component: Hoverable row inside Card +Card.Row = function CardRow({ children, className, ...props }) { + return ( +
+ {children} +
+ ); +}; + diff --git a/src/shared/components/Footer.js b/src/shared/components/Footer.js new file mode 100644 index 00000000..641ebdb0 --- /dev/null +++ b/src/shared/components/Footer.js @@ -0,0 +1,132 @@ +"use client"; + +import Link from "next/link"; +import { APP_CONFIG } from "@/shared/constants/config"; + +const footerLinks = { + product: [ + { label: "Features", href: "#features" }, + { label: "Pricing", href: "#pricing" }, + { label: "Changelog", href: "#" }, + ], + resources: [ + { label: "Documentation", href: "#" }, + { label: "API Reference", href: "#" }, + { label: "Help Center", href: "#" }, + ], + company: [ + { label: "About", href: "#" }, + { label: "Blog", href: "#" }, + { label: "Contact", href: "#" }, + ], +}; + +export default function Footer() { + return ( +
+
+
+ {/* Brand */} +
+
+
+ + + +
+ + {APP_CONFIG.name} + +
+

+ The unified interface for modern AI infrastructure. Secure, observable, and scalable. +

+ {/* Social links */} + +
+ + {/* Product */} +
+

Product

+
    + {footerLinks.product.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ + {/* Resources */} +
+

Resources

+
    + {footerLinks.resources.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ + {/* Company */} +
+

Company

+
    + {footerLinks.company.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+
+ + {/* Bottom */} +
+

+ Š {new Date().getFullYear()} {APP_CONFIG.name} Inc. All rights reserved. +

+
+ + Privacy Policy + + + Terms of Service + +
+
+
+
+ ); +} + diff --git a/src/shared/components/Header.js b/src/shared/components/Header.js new file mode 100644 index 00000000..7376e255 --- /dev/null +++ b/src/shared/components/Header.js @@ -0,0 +1,109 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Link from "next/link"; +import { ThemeToggle } from "@/shared/components"; +import { APP_CONFIG, OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; + +const getPageInfo = (pathname) => { + if (!pathname) return { title: "", description: "", breadcrumbs: [] }; + + // Provider detail page: /dashboard/providers/[id] + const providerMatch = pathname.match(/\/providers\/([^/]+)$/); + if (providerMatch) { + const providerId = providerMatch[1]; + const providerInfo = OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId]; + if (providerInfo) { + return { + title: providerInfo.name, + description: "", + breadcrumbs: [ + { label: "Providers", href: "/dashboard/providers" }, + { label: providerInfo.name, image: `/providers/${providerInfo.id}.png` } + ] + }; + } + } + + if (pathname.includes("/providers")) return { title: "Providers", description: "Manage your AI provider connections", breadcrumbs: [] }; + if (pathname.includes("/combos")) return { title: "Combos", description: "Model combos with fallback", breadcrumbs: [] }; + if (pathname.includes("/cli-tools")) return { title: "CLI Tools", description: "Configure CLI tools", breadcrumbs: [] }; + if (pathname.includes("/endpoint")) return { title: "Endpoint", description: "API endpoint configuration", breadcrumbs: [] }; + if (pathname.includes("/profile")) return { title: "Settings", description: "Manage your preferences", breadcrumbs: [] }; + if (pathname === "/dashboard") return { title: "Endpoint", description: "API endpoint configuration", breadcrumbs: [] }; + return { title: "", description: "", breadcrumbs: [] }; +}; + +export default function Header({ onMenuClick, showMenuButton = true }) { + const pathname = usePathname(); + const { title, description, breadcrumbs } = getPageInfo(pathname); + + return ( +
+ {/* Mobile menu button */} +
+ {showMenuButton && ( + + )} +
+ + {/* Page title with breadcrumbs - desktop */} +
+ {breadcrumbs.length > 0 ? ( +
+ {breadcrumbs.map((crumb, index) => ( +
+ {index > 0 && ( + + chevron_right + + )} + {crumb.href ? ( + + {crumb.label} + + ) : ( +
+ {crumb.image && ( + {crumb.label} { e.target.style.display = "none"; }} + /> + )} +

+ {crumb.label} +

+
+ )} +
+ ))} +
+ ) : title ? ( +
+

{title}

+ {description && ( +

{description}

+ )} +
+ ) : null} +
+ + {/* Right actions */} +
+ {/* Theme toggle */} + +
+
+ ); +} + diff --git a/src/shared/components/Input.js b/src/shared/components/Input.js new file mode 100644 index 00000000..85f4588d --- /dev/null +++ b/src/shared/components/Input.js @@ -0,0 +1,69 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +export default function Input({ + label, + type = "text", + placeholder, + value, + onChange, + error, + hint, + icon, + disabled = false, + required = false, + className, + inputClassName, + ...props +}) { + return ( +
+ {label && ( + + )} +
+ {icon && ( +
+ {icon} +
+ )} + +
+ {error && ( +

+ error + {error} +

+ )} + {hint && !error && ( +

{hint}

+ )} +
+ ); +} + diff --git a/src/shared/components/Loading.js b/src/shared/components/Loading.js new file mode 100644 index 00000000..cd08935b --- /dev/null +++ b/src/shared/components/Loading.js @@ -0,0 +1,77 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +// Spinner loading +export function Spinner({ size = "md", className }) { + const sizes = { + sm: "size-4", + md: "size-6", + lg: "size-8", + xl: "size-12", + }; + + return ( + + progress_activity + + ); +} + +// Full page loading +export function PageLoading({ message = "Loading..." }) { + return ( +
+ +

{message}

+
+ ); +} + +// Skeleton loading +export function Skeleton({ className, ...props }) { + return ( +
+ ); +} + +// Card skeleton +export function CardSkeleton() { + return ( +
+
+ + +
+ + +
+ ); +} + +// Default export +export default function Loading({ type = "spinner", ...props }) { + switch (type) { + case "page": + return ; + case "skeleton": + return ; + case "card": + return ; + default: + return ; + } +} + diff --git a/src/shared/components/Modal.js b/src/shared/components/Modal.js new file mode 100644 index 00000000..7413cb46 --- /dev/null +++ b/src/shared/components/Modal.js @@ -0,0 +1,136 @@ +"use client"; + +import { useEffect } from "react"; +import { cn } from "@/shared/utils/cn"; +import Button from "./Button"; + +export default function Modal({ + isOpen, + onClose, + title, + children, + footer, + size = "md", + closeOnOverlay = true, + showCloseButton = true, + className, +}) { + const sizes = { + sm: "max-w-sm", + md: "max-w-md", + lg: "max-w-lg", + xl: "max-w-xl", + full: "max-w-4xl", + }; + + // Lock body scroll when modal is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [isOpen]); + + // Handle escape key + useEffect(() => { + const handleEscape = (e) => { + if (e.key === "Escape" && isOpen) { + onClose(); + } + }; + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+ {/* Overlay */} +
+ + {/* Modal content */} +
+ {/* Header */} + {(title || showCloseButton) && ( +
+ {title && ( +

+ {title} +

+ )} + {showCloseButton && ( + + )} +
+ )} + + {/* Body */} +
{children}
+ + {/* Footer */} + {footer && ( +
+ {footer} +
+ )} +
+
+ ); +} + +// Confirm Modal helper +export function ConfirmModal({ + isOpen, + onClose, + onConfirm, + title = "Confirm", + message, + confirmText = "Confirm", + cancelText = "Cancel", + variant = "danger", + loading = false, +}) { + return ( + + + + + } + > +

{message}

+
+ ); +} + diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js new file mode 100644 index 00000000..25e06daf --- /dev/null +++ b/src/shared/components/ModelSelectModal.js @@ -0,0 +1,183 @@ +"use client"; + +import { useState, useMemo } from "react"; +import Modal from "./Modal"; +import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; +import { AI_PROVIDERS } from "@/shared/constants/providers"; + +export default function ModelSelectModal({ + isOpen, + onClose, + onSelect, + selectedModel, + activeProviders = [], + title = "Select Model", + modelAliases = {}, +}) { + const [searchQuery, setSearchQuery] = useState(""); + + // Group models by provider + const groupedModels = useMemo(() => { + const groups = {}; + + // Get active provider IDs + const activeProviderIds = activeProviders.length > 0 + ? activeProviders.map(p => p.provider) + : Object.keys(AI_PROVIDERS); + + activeProviderIds.forEach((providerId) => { + const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId; + const providerInfo = AI_PROVIDERS[providerId] || { name: providerId, color: "#666" }; + + // For passthrough providers, get models from aliases + if (providerInfo.passthroughModels) { + const aliasModels = Object.entries(modelAliases) + .filter(([, fullModel]) => fullModel.startsWith(`${alias}/`)) + .map(([aliasName, fullModel]) => ({ + id: fullModel.replace(`${alias}/`, ""), + name: aliasName, + value: fullModel, + })); + + if (aliasModels.length > 0) { + groups[providerId] = { + name: providerInfo.name, + alias: alias, + color: providerInfo.color, + models: aliasModels, + }; + } + } else { + const models = getModelsByProviderId(providerId); + if (models.length > 0) { + groups[providerId] = { + name: providerInfo.name, + alias: alias, + color: providerInfo.color, + models: models.map((m) => ({ + id: m.id, + name: m.name, + value: `${alias}/${m.id}`, + })), + }; + } + } + }); + + return groups; + }, [activeProviders, modelAliases]); + + // Filter models by search query + const filteredGroups = useMemo(() => { + if (!searchQuery.trim()) return groupedModels; + + const query = searchQuery.toLowerCase(); + const filtered = {}; + + Object.entries(groupedModels).forEach(([providerId, group]) => { + const matchedModels = group.models.filter( + (m) => + m.name.toLowerCase().includes(query) || + m.id.toLowerCase().includes(query) || + group.name.toLowerCase().includes(query) + ); + + if (matchedModels.length > 0) { + filtered[providerId] = { + ...group, + models: matchedModels, + }; + } + }); + + return filtered; + }, [groupedModels, searchQuery]); + + const handleSelect = (model) => { + onSelect(model); + onClose(); + setSearchQuery(""); + }; + + return ( + { + onClose(); + setSearchQuery(""); + }} + title={title} + size="md" + className="!p-4" + > + {/* Search - compact */} +
+
+ + search + + setSearchQuery(e.target.value)} + className="w-full pl-8 pr-3 py-1.5 bg-surface border border-border rounded text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" + /> +
+
+ + {/* Models grouped by provider - compact */} +
+ {Object.entries(filteredGroups).map(([providerId, group]) => ( +
+ {/* Provider header */} +
+
+ + {group.name} + + + ({group.models.length}) + +
+ + {/* Models as wrap chips - compact */} +
+ {group.models.map((model) => { + const isSelected = selectedModel === model.value; + return ( + + ); + })} +
+
+ ))} + + {Object.keys(filteredGroups).length === 0 && ( +
+ + search_off + +

No models found

+
+ )} +
+ + ); +} + diff --git a/src/shared/components/OAuthModal.js b/src/shared/components/OAuthModal.js new file mode 100644 index 00000000..81be4e54 --- /dev/null +++ b/src/shared/components/OAuthModal.js @@ -0,0 +1,419 @@ +"use client"; + +import { useState, useEffect, useRef, useCallback } from "react"; +import { Modal, Button, Input } from "@/shared/components"; +import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; + +/** + * OAuth Modal Component + * - Localhost: Auto callback via popup message + * - Remote: Manual paste callback URL + */ +export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, onClose }) { + const [step, setStep] = useState("waiting"); // waiting | input | success | error + const [authData, setAuthData] = useState(null); + const [callbackUrl, setCallbackUrl] = useState(""); + const [error, setError] = useState(null); + const [isDeviceCode, setIsDeviceCode] = useState(false); + const [deviceData, setDeviceData] = useState(null); + const [polling, setPolling] = useState(false); + const popupRef = useRef(null); + const { copied, copy } = useCopyToClipboard(); + + // Detect if running on localhost + const isLocalhost = typeof window !== "undefined" && + (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"); + + // Reset state and start OAuth when modal opens + useEffect(() => { + if (isOpen && provider) { + setAuthData(null); + setCallbackUrl(""); + setError(null); + setIsDeviceCode(false); + setDeviceData(null); + setPolling(false); + // Auto start OAuth + startOAuthFlow(); + } + }, [isOpen, provider]); + + // Listen for OAuth callback via multiple methods + const callbackProcessedRef = useRef(false); + + useEffect(() => { + if (!authData) return; + callbackProcessedRef.current = false; // Reset when authData changes + + // Handler for callback data - only process once + const handleCallback = async (data) => { + if (callbackProcessedRef.current) return; // Already processed + + const { code, state, error: callbackError, errorDescription } = data; + + if (callbackError) { + callbackProcessedRef.current = true; + setError(errorDescription || callbackError); + setStep("error"); + return; + } + + if (code) { + callbackProcessedRef.current = true; + await exchangeTokens(code, state); + } + }; + + // Method 1: postMessage from popup + const handleMessage = (event) => { + if (event.origin !== window.location.origin) return; + if (event.data?.type === "oauth_callback") { + handleCallback(event.data.data); + } + }; + window.addEventListener("message", handleMessage); + + // Method 2: BroadcastChannel + let channel; + try { + channel = new BroadcastChannel("oauth_callback"); + channel.onmessage = (event) => handleCallback(event.data); + } catch (e) { + console.log("BroadcastChannel not supported"); + } + + // Method 3: localStorage event + const handleStorage = (event) => { + if (event.key === "oauth_callback" && event.newValue) { + try { + const data = JSON.parse(event.newValue); + handleCallback(data); + localStorage.removeItem("oauth_callback"); + } catch (e) { + console.log("Failed to parse localStorage data"); + } + } + }; + window.addEventListener("storage", handleStorage); + + // Also check localStorage on mount (in case callback already happened) + try { + const stored = localStorage.getItem("oauth_callback"); + if (stored) { + const data = JSON.parse(stored); + // Only use if recent (within 30 seconds) + if (data.timestamp && Date.now() - data.timestamp < 30000) { + handleCallback(data); + localStorage.removeItem("oauth_callback"); + } + } + } catch (e) {} + + return () => { + window.removeEventListener("message", handleMessage); + window.removeEventListener("storage", handleStorage); + if (channel) channel.close(); + }; + }, [authData]); + + // Exchange tokens + const exchangeTokens = async (code, state) => { + try { + const res = await fetch(`/api/oauth/${provider}/exchange`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + code, + redirectUri: authData.redirectUri, + codeVerifier: authData.codeVerifier, + state, + }), + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + setStep("success"); + onSuccess?.(); + } catch (err) { + setError(err.message); + setStep("error"); + } + }; + + // Start OAuth flow + const startOAuthFlow = async () => { + if (!provider) return; + try { + setError(null); + + // Device code flow (GitHub, Qwen) + if (provider === "github" || provider === "qwen") { + setIsDeviceCode(true); + setStep("waiting"); + + const res = await fetch(`/api/oauth/${provider}/device-code`); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + setDeviceData(data); + + // Open verification URL + const verifyUrl = data.verification_uri_complete || data.verification_uri; + if (verifyUrl) window.open(verifyUrl, "_blank"); + + // Start polling + startPolling(data.device_code, data.codeVerifier, data.interval || 5); + return; + } + + // Authorization code flow - always use localhost with current port (except Codex) + let redirectUri; + if (provider === "codex") { + // Codex requires fixed port 1455 + redirectUri = "http://localhost:1455/auth/callback"; + } else { + // Always use localhost with current port for OAuth callback + const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80"); + redirectUri = `http://localhost:${port}/callback`; + } + + const res = await fetch(`/api/oauth/${provider}/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + setAuthData({ ...data, redirectUri }); + + // For Codex, always use manual input since it requires fixed port 1455 + if (provider === "codex") { + setStep("input"); + window.open(data.authUrl, "_blank"); + } else if (isLocalhost) { + // Other providers on localhost: Open popup and wait for message + setStep("waiting"); + popupRef.current = window.open(data.authUrl, "oauth_popup", "width=600,height=700"); + + // Check if popup was blocked + if (!popupRef.current) { + setStep("input"); + } + } else { + // Remote: Show manual input + setStep("input"); + window.open(data.authUrl, "_blank"); + } + } catch (err) { + setError(err.message); + setStep("error"); + } + }; + + // Poll for device code token + const startPolling = async (deviceCode, codeVerifier, interval) => { + setPolling(true); + const maxAttempts = 60; + + for (let i = 0; i < maxAttempts; i++) { + await new Promise((r) => setTimeout(r, interval * 1000)); + + try { + const res = await fetch(`/api/oauth/${provider}/poll`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ deviceCode, codeVerifier }), + }); + + const data = await res.json(); + + if (data.success) { + setStep("success"); + setPolling(false); + onSuccess?.(); + return; + } + + if (data.error === "expired_token" || data.error === "access_denied") { + throw new Error(data.errorDescription || data.error); + } + + if (data.error === "slow_down") { + interval = Math.min(interval + 5, 30); + } + } catch (err) { + setError(err.message); + setStep("error"); + setPolling(false); + return; + } + } + + setError("Authorization timeout"); + setStep("error"); + setPolling(false); + }; + + // Handle manual URL input + const handleManualSubmit = async () => { + try { + setError(null); + const url = new URL(callbackUrl); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + const errorParam = url.searchParams.get("error"); + + if (errorParam) { + throw new Error(url.searchParams.get("error_description") || errorParam); + } + + if (!code) { + throw new Error("No authorization code found in URL"); + } + + await exchangeTokens(code, state); + } catch (err) { + setError(err.message); + setStep("error"); + } + }; + + if (!provider || !providerInfo) return null; + + return ( + +
+ {/* Waiting Step (Localhost - popup mode) */} + {step === "waiting" && !isDeviceCode && ( +
+
+ + progress_activity + +
+

Waiting for Authorization

+

+ Complete the authorization in the popup window. +

+ +
+ )} + + {/* Device Code Flow - Waiting */} + {step === "waiting" && isDeviceCode && deviceData && ( + <> +
+

+ Visit the URL below and enter the code: +

+
+

Verification URL

+
+ {deviceData.verification_uri} +
+
+
+

Your Code

+
+

{deviceData.user_code}

+
+
+
+ {polling && ( +
+ progress_activity + Waiting for authorization... +
+ )} + + )} + + {/* Manual Input Step */} + {step === "input" && !isDeviceCode && ( + <> +
+
+

Step 1: Open this URL in your browser

+
+ + +
+
+ +
+

Step 2: Paste the callback URL here

+

+ After authorization, copy the full URL from your browser. +

+ setCallbackUrl(e.target.value)} + placeholder={`${window.location.origin}/callback?code=...`} + className="font-mono text-xs" + /> +
+
+ +
+ + +
+ + )} + + {/* Success Step */} + {step === "success" && ( +
+
+ check_circle +
+

Connected Successfully!

+

+ Your {providerInfo.name} account has been connected. +

+ +
+ )} + + {/* Error Step */} + {step === "error" && ( +
+
+ error +
+

Connection Failed

+

{error}

+
+ + +
+
+ )} +
+
+ ); +} diff --git a/src/shared/components/Select.js b/src/shared/components/Select.js new file mode 100644 index 00000000..066b805a --- /dev/null +++ b/src/shared/components/Select.js @@ -0,0 +1,70 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +export default function Select({ + label, + options = [], + value, + onChange, + placeholder = "Select an option", + error, + hint, + disabled = false, + required = false, + className, + selectClassName, + ...props +}) { + return ( +
+ {label && ( + + )} +
+ +
+ expand_more +
+
+ {error && ( +

+ error + {error} +

+ )} + {hint && !error && ( +

{hint}

+ )} +
+ ); +} + diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js new file mode 100644 index 00000000..445c8e5f --- /dev/null +++ b/src/shared/components/Sidebar.js @@ -0,0 +1,171 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/shared/utils/cn"; +import { APP_CONFIG } from "@/shared/constants/config"; +import Button from "./Button"; +import { ConfirmModal } from "./Modal"; + +const navItems = [ + { href: "/dashboard/endpoint", label: "Endpoint", icon: "api" }, + { href: "/dashboard/providers", label: "Providers", icon: "dns" }, + { href: "/dashboard/combos", label: "Combos", icon: "layers" }, + { href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" }, +]; + +const systemItems = [ + { href: "/dashboard/profile", label: "Settings", icon: "settings" }, +]; + +export default function Sidebar({ onClose }) { + const pathname = usePathname(); + const [showShutdownModal, setShowShutdownModal] = useState(false); + const [isShuttingDown, setIsShuttingDown] = useState(false); + const [isDisconnected, setIsDisconnected] = useState(false); + + const isActive = (href) => { + if (href === "/dashboard/endpoint") { + return pathname === "/dashboard" || pathname.startsWith("/dashboard/endpoint"); + } + return pathname.startsWith(href); + }; + + const handleShutdown = async () => { + setIsShuttingDown(true); + try { + await fetch("/api/shutdown", { method: "POST" }); + } catch (e) { + // Expected to fail as server shuts down + } + setIsShuttingDown(false); + setShowShutdownModal(false); + setIsDisconnected(true); + }; + + return ( + <> + + + {/* Shutdown Confirmation Modal */} + setShowShutdownModal(false)} + onConfirm={handleShutdown} + title="Close Proxy" + message="Are you sure you want to close the proxy server?" + confirmText="Close" + cancelText="Cancel" + variant="danger" + loading={isShuttingDown} + /> + + {/* Disconnected Overlay */} + {isDisconnected && ( +
+
+
+ power_off +
+

Server Disconnected

+

The proxy server has been stopped.

+ +
+
+ )} + + ); +} diff --git a/src/shared/components/ThemeProvider.js b/src/shared/components/ThemeProvider.js new file mode 100644 index 00000000..a4745f4c --- /dev/null +++ b/src/shared/components/ThemeProvider.js @@ -0,0 +1,15 @@ +"use client"; + +import { useEffect } from "react"; +import useThemeStore from "@/store/themeStore"; + +export function ThemeProvider({ children }) { + const { initTheme } = useThemeStore(); + + useEffect(() => { + initTheme(); + }, [initTheme]); + + return <>{children}; +} + diff --git a/src/shared/components/ThemeToggle.js b/src/shared/components/ThemeToggle.js new file mode 100644 index 00000000..02b61bec --- /dev/null +++ b/src/shared/components/ThemeToggle.js @@ -0,0 +1,47 @@ +"use client"; + +import { useTheme } from "@/shared/hooks/useTheme"; +import { cn } from "@/shared/utils/cn"; + +export default function ThemeToggle({ className, variant = "default" }) { + const { theme, toggleTheme, isDark } = useTheme(); + + const variants = { + default: cn( + "flex items-center justify-center size-10 rounded-full", + "text-text-muted", + "hover:bg-black/5", + "hover:text-text-main", + "transition-colors" + ), + card: cn( + "flex items-center justify-center size-11 rounded-full", + "bg-surface/60", + "hover:bg-surface", + "border border-border", + "backdrop-blur-md shadow-sm hover:shadow-md", + "text-text-muted-light hover:text-primary", + "hover:text-primary", + "transition-all group" + ), + }; + + return ( + + ); +} + diff --git a/src/shared/components/Toggle.js b/src/shared/components/Toggle.js new file mode 100644 index 00000000..61b9539f --- /dev/null +++ b/src/shared/components/Toggle.js @@ -0,0 +1,91 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +export default function Toggle({ + checked = false, + onChange, + label, + description, + disabled = false, + size = "md", + className, +}) { + const sizes = { + sm: { + track: "w-8 h-4", + thumb: "size-3", + translate: "translate-x-4", + }, + md: { + track: "w-11 h-6", + thumb: "size-5", + translate: "translate-x-5", + }, + lg: { + track: "w-14 h-7", + thumb: "size-6", + translate: "translate-x-7", + }, + }; + + const handleClick = () => { + if (!disabled && onChange) { + onChange(!checked); + } + }; + + return ( +
+ + {(label || description) && ( +
+ {label && ( + + {label} + + )} + {description && ( + + {description} + + )} +
+ )} +
+ ); +} + diff --git a/src/shared/components/index.js b/src/shared/components/index.js new file mode 100644 index 00000000..a3a25b2a --- /dev/null +++ b/src/shared/components/index.js @@ -0,0 +1,21 @@ +// Shared Components - Export all +export { default as Button } from "./Button"; +export { default as Input } from "./Input"; +export { default as Select } from "./Select"; +export { default as Card } from "./Card"; +export { default as Modal, ConfirmModal } from "./Modal"; +export { default as Loading, Spinner, PageLoading, Skeleton, CardSkeleton } from "./Loading"; +export { default as Avatar } from "./Avatar"; +export { default as Badge } from "./Badge"; +export { default as Toggle } from "./Toggle"; +export { default as ThemeToggle } from "./ThemeToggle"; +export { ThemeProvider } from "./ThemeProvider"; +export { default as Sidebar } from "./Sidebar"; +export { default as Header } from "./Header"; +export { default as Footer } from "./Footer"; +export { default as OAuthModal } from "./OAuthModal"; +export { default as ModelSelectModal } from "./ModelSelectModal"; + +// Layouts +export * from "./layouts"; + diff --git a/src/shared/components/layouts/AuthLayout.js b/src/shared/components/layouts/AuthLayout.js new file mode 100644 index 00000000..8e6d71b5 --- /dev/null +++ b/src/shared/components/layouts/AuthLayout.js @@ -0,0 +1,24 @@ +"use client"; + +import { ThemeToggle } from "@/shared/components"; + +export default function AuthLayout({ children }) { + return ( +
+ {/* Background effects */} +
+
+ + {/* Theme toggle */} +
+ +
+ + {/* Content */} +
+ {children} +
+
+ ); +} + diff --git a/src/shared/components/layouts/DashboardLayout.js b/src/shared/components/layouts/DashboardLayout.js new file mode 100644 index 00000000..30bad750 --- /dev/null +++ b/src/shared/components/layouts/DashboardLayout.js @@ -0,0 +1,43 @@ +"use client"; + +import { useState } from "react"; +import Sidebar from "../Sidebar"; +import Header from "../Header"; + +export default function DashboardLayout({ children }) { + const [sidebarOpen, setSidebarOpen] = useState(false); + + return ( +
+ {/* Mobile sidebar overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar - Desktop */} +
+ +
+ + {/* Sidebar - Mobile */} +
+ setSidebarOpen(false)} /> +
+ + {/* Main content */} +
+
setSidebarOpen(true)} /> +
+
{children}
+
+
+
+ ); +} diff --git a/src/shared/components/layouts/index.js b/src/shared/components/layouts/index.js new file mode 100644 index 00000000..4b6ac5fc --- /dev/null +++ b/src/shared/components/layouts/index.js @@ -0,0 +1,4 @@ +// Layout Components - Export all +export { default as DashboardLayout } from "./DashboardLayout"; +export { default as AuthLayout } from "./AuthLayout"; + diff --git a/src/shared/constants/cliTools.js b/src/shared/constants/cliTools.js new file mode 100644 index 00000000..0e8b7f1c --- /dev/null +++ b/src/shared/constants/cliTools.js @@ -0,0 +1,142 @@ +// CLI Tools configuration +export const CLI_TOOLS = { + claude: { + id: "claude", + name: "Claude Code", + icon: "terminal", + color: "#D97757", + description: "Anthropic Claude Code CLI", + configType: "env", + envVars: { + baseUrl: "ANTHROPIC_BASE_URL", + model: "ANTHROPIC_MODEL", + opusModel: "ANTHROPIC_DEFAULT_OPUS_MODEL", + sonnetModel: "ANTHROPIC_DEFAULT_SONNET_MODEL", + haikuModel: "ANTHROPIC_DEFAULT_HAIKU_MODEL", + }, + modelAliases: ["default", "sonnet", "opus", "haiku", "opusplan"], + settingsFile: "~/.claude/settings.json", + defaultModels: [ + { id: "opus", name: "Claude Opus", alias: "opus", envKey: "ANTHROPIC_DEFAULT_OPUS_MODEL", defaultValue: "cc/claude-opus-4-5-20251101" }, + { id: "sonnet", name: "Claude Sonnet", alias: "sonnet", envKey: "ANTHROPIC_DEFAULT_SONNET_MODEL", defaultValue: "cc/claude-sonnet-4-5-20250929" }, + { id: "haiku", name: "Claude Haiku", alias: "haiku", envKey: "ANTHROPIC_DEFAULT_HAIKU_MODEL", defaultValue: "cc/claude-haiku-4-5-20251001" }, + ], + }, + codex: { + id: "codex", + name: "OpenAI Codex CLI", + image: "/providers/codex.png", + color: "#10A37F", + description: "OpenAI Codex CLI", + configType: "custom", + }, + cursor: { + id: "cursor", + name: "Cursor", + image: "/providers/cursor.png", + color: "#000000", + description: "Cursor AI Code Editor", + configType: "guide", + requiresCloud: true, + notes: [ + { type: "warning", text: "Requires Cursor Pro account to use this feature." }, + { type: "cloudCheck", text: "Cursor routes requests through its own server, so local endpoint is not supported. Please enable Cloud Endpoint in Settings." }, + ], + guideSteps: [ + { step: 1, title: "Open Settings", desc: "Go to Settings → Models" }, + { step: 2, title: "Enable OpenAI API", desc: "Enable \"OpenAI API key\" option" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}/v1", copyable: true }, + { step: 4, title: "API Key", type: "apiKeySelector" }, + { step: 5, title: "Add Custom Model", desc: "Click \"View All Model\" → \"Add Custom Model\"" }, + { step: 6, title: "Select Model", type: "modelSelector" }, + ], + }, + cline: { + id: "cline", + name: "CLINE", + image: "/providers/cline.png", + color: "#00D1B2", + description: "CLINE AI Assistant", + configType: "guide", + guideSteps: [ + { step: 1, title: "Open Settings", desc: "Go to CLINE Settings panel" }, + { step: 2, title: "Select Provider", desc: "Choose API Provider → Ollama" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "API Key", type: "apiKeySelector" }, + { step: 5, title: "Select Model", type: "modelSelector" }, + ], + }, + roo: { + id: "roo", + name: "Roo", + image: "/providers/roo.png", + color: "#FF6B6B", + description: "Roo AI Assistant", + configType: "guide", + guideSteps: [ + { step: 1, title: "Open Settings", desc: "Go to Roo Settings panel" }, + { step: 2, title: "Select Provider", desc: "Choose API Provider → Ollama" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "API Key", type: "apiKeySelector" }, + { step: 5, title: "Select Model", type: "modelSelector" }, + ], + }, + continue: { + id: "continue", + name: "Continue", + image: "/providers/continue.png", + color: "#7C3AED", + description: "Continue AI Assistant", + configType: "guide", + guideSteps: [ + { step: 1, title: "Open Config", desc: "Open Continue configuration file" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Select Model", type: "modelSelector" }, + { step: 4, title: "Add Model Config", desc: "Add the following configuration to your models array:" }, + ], + codeBlock: { + language: "json", + code: `{ + "apiBase": "{{baseUrl}}", + "title": "{{model}}", + "model": "{{model}}", + "provider": "openai", + "apiKey": "{{apiKey}}" +}`, + }, + }, + // HIDDEN: gemini-cli + // "gemini-cli": { + // id: "gemini-cli", + // name: "Gemini CLI", + // icon: "terminal", + // color: "#4285F4", + // description: "Google Gemini CLI", + // configType: "env", + // envVars: { + // baseUrl: "GEMINI_API_BASE_URL", + // model: "GEMINI_MODEL", + // }, + // defaultModels: [ + // { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", alias: "pro" }, + // { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", alias: "flash" }, + // ], + // }, +}; + +// Get all provider models for mapping dropdown +export const getProviderModelsForMapping = (providers) => { + const result = []; + providers.forEach(conn => { + if (conn.isActive && (conn.testStatus === "active" || conn.testStatus === "success")) { + result.push({ + connectionId: conn.id, + provider: conn.provider, + name: conn.name, + models: conn.models || [], + }); + } + }); + return result; +}; + diff --git a/src/shared/constants/colors.js b/src/shared/constants/colors.js new file mode 100644 index 00000000..a4bb99f3 --- /dev/null +++ b/src/shared/constants/colors.js @@ -0,0 +1,77 @@ +// Claude-inspired color palette for Endpoint Proxy +// Light theme: Warm beige/cream tones +// Dark theme: Deep charcoal/brown tones + +export const COLORS = { + // Primary - Warm Coral/Terracotta (Claude-like) + primary: { + DEFAULT: "#D97757", + hover: "#C56243", + light: "#E8A58C", + dark: "#B0664D", + }, + + // Light theme backgrounds + light: { + bg: "#FBF9F6", + bgAlt: "#F5F1ED", + surface: "#FFFFFF", + sidebar: "#F0EFEC", + border: "#E6E4DD", + textMain: "#383733", + textMuted: "#75736E", + }, + + // Dark theme backgrounds + dark: { + bg: "#191918", + bgAlt: "#1F1F1E", + surface: "#242423", + sidebar: "#1F1F1E", + border: "#333331", + textMain: "#ECEBE8", + textMuted: "#9E9D99", + }, + + // Status colors + status: { + success: "#22C55E", + successLight: "#DCFCE7", + successDark: "#166534", + warning: "#F59E0B", + warningLight: "#FEF3C7", + warningDark: "#92400E", + error: "#EF4444", + errorLight: "#FEE2E2", + errorDark: "#991B1B", + info: "#3B82F6", + infoLight: "#DBEAFE", + infoDark: "#1E40AF", + }, +}; + +// CSS Variables mapping for Tailwind +export const CSS_VARIABLES = { + light: { + "--color-primary": COLORS.primary.DEFAULT, + "--color-primary-hover": COLORS.primary.hover, + "--color-bg": COLORS.light.bg, + "--color-bg-alt": COLORS.light.bgAlt, + "--color-surface": COLORS.light.surface, + "--color-sidebar": COLORS.light.sidebar, + "--color-border": COLORS.light.border, + "--color-text-main": COLORS.light.textMain, + "--color-text-muted": COLORS.light.textMuted, + }, + dark: { + "--color-primary": COLORS.primary.DEFAULT, + "--color-primary-hover": COLORS.primary.hover, + "--color-bg": COLORS.dark.bg, + "--color-bg-alt": COLORS.dark.bgAlt, + "--color-surface": COLORS.dark.surface, + "--color-sidebar": COLORS.dark.sidebar, + "--color-border": COLORS.dark.border, + "--color-text-main": COLORS.dark.textMain, + "--color-text-muted": COLORS.dark.textMuted, + }, +}; diff --git a/src/shared/constants/config.js b/src/shared/constants/config.js new file mode 100644 index 00000000..a4381512 --- /dev/null +++ b/src/shared/constants/config.js @@ -0,0 +1,53 @@ +// App configuration +export const APP_CONFIG = { + name: "Endpoint Proxy", + description: "AI Infrastructure Management", + version: "1.0.0", +}; + +// Theme configuration +export const THEME_CONFIG = { + storageKey: "theme", + defaultTheme: "system", // "light" | "dark" | "system" +}; + +// Subscription +export const SUBSCRIPTION_CONFIG = { + price: 1.0, + currency: "USD", + interval: "month", + planName: "Pro Plan", +}; + +// API endpoints +export const API_ENDPOINTS = { + users: "/api/users", + providers: "/api/providers", + payments: "/api/payments", + auth: "/api/auth", +}; + +// Provider API endpoints (for display only) +export const PROVIDER_ENDPOINTS = { + openrouter: "https://openrouter.ai/api/v1/chat/completions", + glm: "https://api.z.ai/api/anthropic/v1/messages", + kimi: "https://api.kimi.com/coding/v1/messages", + minimax: "https://api.minimax.io/anthropic/v1/messages", + openai: "https://api.openai.com/v1/chat/completions", + anthropic: "https://api.anthropic.com/v1/messages", + gemini: "https://generativelanguage.googleapis.com/v1beta/models", +}; + +// Re-export from providers.js for backward compatibility +export { + OAUTH_PROVIDERS, + APIKEY_PROVIDERS, + AI_PROVIDERS, + AUTH_METHODS, +} from "./providers.js"; + +// Re-export from models.js for backward compatibility +export { + PROVIDER_MODELS, + AI_MODELS, +} from "./models.js"; diff --git a/src/shared/constants/index.js b/src/shared/constants/index.js new file mode 100644 index 00000000..820029c5 --- /dev/null +++ b/src/shared/constants/index.js @@ -0,0 +1,4 @@ +// Shared Constants - Export all +export * from "./colors"; +export * from "./config"; + diff --git a/src/shared/constants/models.js b/src/shared/constants/models.js new file mode 100644 index 00000000..30becdf1 --- /dev/null +++ b/src/shared/constants/models.js @@ -0,0 +1,34 @@ +// Re-export from open-sse (single source of truth) +export { + PROVIDER_MODELS, + getProviderModels, + getDefaultModel, + isValidModel as isValidModelCore, + findModelName, + getModelTargetFormat, + PROVIDER_ID_TO_ALIAS, + getModelsByProviderId +} from "open-sse"; + +import { AI_PROVIDERS } from "./providers.js"; +import { PROVIDER_MODELS as MODELS } from "open-sse"; + +// Providers that accept any model (passthrough) +const PASSTHROUGH_PROVIDERS = new Set( + Object.entries(AI_PROVIDERS) + .filter(([, p]) => p.passthroughModels) + .map(([key]) => key) +); + +// Wrap isValidModel with passthrough providers +export function isValidModel(aliasOrId, modelId) { + if (PASSTHROUGH_PROVIDERS.has(aliasOrId)) return true; + const models = MODELS[aliasOrId]; + if (!models) return false; + return models.some(m => m.id === modelId); +} + +// Legacy AI_MODELS for backward compatibility +export const AI_MODELS = Object.entries(MODELS).flatMap(([alias, models]) => + models.map(m => ({ provider: alias, model: m.id, name: m.name })) +); diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js new file mode 100644 index 00000000..7f5efa09 --- /dev/null +++ b/src/shared/constants/providers.js @@ -0,0 +1,65 @@ +// Provider definitions + +// OAuth Providers +export const OAUTH_PROVIDERS = { + claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757" }, + antigravity: { id: "antigravity", alias: "ag", name: "Antigravity", icon: "rocket_launch", color: "#F59E0B" }, + codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6" }, + iflow: { id: "iflow", alias: "if", name: "iFlow AI", icon: "water_drop", color: "#6366F1" }, + qwen: { id: "qwen", alias: "qw", name: "Qwen Code", icon: "psychology", color: "#10B981" }, + "gemini-cli": { id: "gemini-cli", alias: "gc", name: "Gemini CLI", icon: "terminal", color: "#4285F4" }, + github: { id: "github", alias: "gh", name: "GitHub Copilot", icon: "code", color: "#333333" }, +}; + +export const APIKEY_PROVIDERS = { + openrouter: { id: "openrouter", alias: "openrouter", name: "OpenRouter", icon: "router", color: "#6366F1", textIcon: "OR" , passthroughModels: true }, + glm: { id: "glm", alias: "glm", name: "GLM Coding", icon: "code", color: "#2563EB", textIcon: "GL" }, + kimi: { id: "kimi", alias: "kimi", name: "Kimi Coding", icon: "psychology", color: "#1E3A8A", textIcon: "KM" }, + minimax: { id: "minimax", alias: "minimax", name: "Minimax Coding", icon: "memory", color: "#7C3AED", textIcon: "MM" }, + openai: { id: "openai", alias: "openai", name: "OpenAI", icon: "auto_awesome", color: "#10A37F", textIcon: "OA" }, + anthropic: { id: "anthropic", alias: "anthropic", name: "Anthropic", icon: "smart_toy", color: "#D97757", textIcon: "AN" }, + gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE" }, +}; + +// All providers (combined) +export const AI_PROVIDERS = { ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS }; + +// Auth methods +export const AUTH_METHODS = { + oauth: { id: "oauth", name: "OAuth", icon: "lock" }, + apikey: { id: "apikey", name: "API Key", icon: "key" }, +}; + +// Helper: Get provider by alias +export function getProviderByAlias(alias) { + for (const provider of Object.values(AI_PROVIDERS)) { + if (provider.alias === alias || provider.id === alias) { + return provider; + } + } + return null; +} + +// Helper: Get provider ID from alias +export function resolveProviderId(aliasOrId) { + const provider = getProviderByAlias(aliasOrId); + return provider?.id || aliasOrId; +} + +// Helper: Get alias from provider ID +export function getProviderAlias(providerId) { + const provider = AI_PROVIDERS[providerId]; + return provider?.alias || providerId; +} + +// Alias to ID mapping (for quick lookup) +export const ALIAS_TO_ID = Object.values(AI_PROVIDERS).reduce((acc, p) => { + acc[p.alias] = p.id; + return acc; +}, {}); + +// ID to Alias mapping +export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce((acc, p) => { + acc[p.id] = p.alias; + return acc; +}, {}); diff --git a/src/shared/hooks/index.js b/src/shared/hooks/index.js new file mode 100644 index 00000000..68c81c2d --- /dev/null +++ b/src/shared/hooks/index.js @@ -0,0 +1,2 @@ +// Shared Hooks - Export all +export { useTheme } from "./useTheme"; diff --git a/src/shared/hooks/useCopyToClipboard.js b/src/shared/hooks/useCopyToClipboard.js new file mode 100644 index 00000000..558aad89 --- /dev/null +++ b/src/shared/hooks/useCopyToClipboard.js @@ -0,0 +1,29 @@ +"use client"; + +import { useState, useCallback, useRef } from "react"; + +/** + * Hook for copy to clipboard with feedback + * @param {number} resetDelay - Time in ms before resetting copied state (default: 2000) + * @returns {{ copied: string|null, copy: (text: string, id?: string) => void }} + */ +export function useCopyToClipboard(resetDelay = 2000) { + const [copied, setCopied] = useState(null); + const timeoutRef = useRef(null); + + const copy = useCallback((text, id = "default") => { + navigator.clipboard.writeText(text); + setCopied(id); + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + setCopied(null); + }, resetDelay); + }, [resetDelay]); + + return { copied, copy }; +} + diff --git a/src/shared/hooks/useTheme.js b/src/shared/hooks/useTheme.js new file mode 100644 index 00000000..029be308 --- /dev/null +++ b/src/shared/hooks/useTheme.js @@ -0,0 +1,31 @@ +"use client"; + +import { useEffect } from "react"; +import useThemeStore from "@/store/themeStore"; + +export function useTheme() { + const { theme, setTheme, toggleTheme, initTheme } = useThemeStore(); + + useEffect(() => { + initTheme(); + + // Listen for system theme changes + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = () => { + if (theme === "system") { + initTheme(); + } + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, [theme, initTheme]); + + return { + theme, + setTheme, + toggleTheme, + isDark: theme === "dark" || (theme === "system" && typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches), + }; +} + diff --git a/src/shared/services/cloudSyncScheduler.js b/src/shared/services/cloudSyncScheduler.js new file mode 100644 index 00000000..59848f5b --- /dev/null +++ b/src/shared/services/cloudSyncScheduler.js @@ -0,0 +1,117 @@ +import { getConsistentMachineId } from "@/shared/utils/machineId"; +import { isCloudEnabled } from "@/lib/localDb"; + +/** + * Cloud sync scheduler + */ +export class CloudSyncScheduler { + constructor(machineId = null, intervalMinutes = 15) { + this.machineId = machineId; + this.intervalMinutes = intervalMinutes; + this.intervalId = null; + } + + /** + * Initialize machine ID if not provided + */ + async initializeMachineId() { + if (!this.machineId) { + this.machineId = await getConsistentMachineId(); + } + } + + /** + * Start periodic sync (delays first sync to allow server to be ready) + */ + async start() { + if (this.intervalId) { + return; + } + + await this.initializeMachineId(); + + // Delay first sync by 30 seconds to ensure server is ready + setTimeout(() => { + this.syncWithRetry().catch(() => {}); + }, 30000); + + // Then sync periodically + this.intervalId = setInterval(() => { + this.syncWithRetry().catch(() => {}); + }, this.intervalMinutes * 60 * 1000); + } + + /** + * Stop periodic sync + */ + stop() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + /** + * Sync with retry logic (exponential backoff) + */ + async syncWithRetry(maxRetries = 1) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const result = await this.sync(); + return result; + } catch (error) { + if (attempt === maxRetries) { + return null; + } + + const delay = Math.min(1000 * Math.pow(2, attempt), 10000); // Max 10s + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + /** + * Perform sync via internal API route (handles token update to db.json) + */ + async sync() { + // Check if cloud is enabled + const enabled = await isCloudEnabled(); + if (!enabled) { + return null; + } + + await this.initializeMachineId(); + + // Call internal API route which handles both sync and token update + const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"}/api/sync/cloud`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ machineId: this.machineId, action: "sync" }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || "Sync failed"); + } + + const result = await response.json(); + return result; + } + + /** + * Check if scheduler is running + */ + isRunning() { + return this.intervalId !== null; + } +} + +// Export a singleton instance if needed +let cloudSyncScheduler = null; + +export async function getCloudSyncScheduler(machineId = null, intervalMinutes = 15) { + if (!cloudSyncScheduler) { + cloudSyncScheduler = new CloudSyncScheduler(machineId, intervalMinutes); + } + return cloudSyncScheduler; +} diff --git a/src/shared/services/initializeCloudSync.js b/src/shared/services/initializeCloudSync.js new file mode 100644 index 00000000..4191eaaf --- /dev/null +++ b/src/shared/services/initializeCloudSync.js @@ -0,0 +1,32 @@ +import { getCloudSyncScheduler } from "@/shared/services/cloudSyncScheduler"; +import { isCloudEnabled, cleanupProviderConnections } from "@/lib/localDb"; + +/** + * Initialize cloud sync scheduler + * This should be called when the application starts + */ +export async function initializeCloudSync() { + try { + // Cleanup null fields from existing data + await cleanupProviderConnections(); + + // Create scheduler instance with default 15-minute interval + const scheduler = await getCloudSyncScheduler(null, 15); + + // Start the scheduler + await scheduler.start(); + + return scheduler; + } catch (error) { + console.error("[CloudSync] Error initializing scheduler:", error); + throw error; + } +} + +// For development/testing purposes +if (typeof require !== "undefined" && require.main === module) { + initializeCloudSync().catch(console.log); +} + +export default initializeCloudSync; + diff --git a/src/shared/utils/api.js b/src/shared/utils/api.js new file mode 100644 index 00000000..2e0ad905 --- /dev/null +++ b/src/shared/utils/api.js @@ -0,0 +1,92 @@ +/** + * API utility functions for making HTTP requests + */ + +const DEFAULT_HEADERS = { + "Content-Type": "application/json", +}; + +/** + * Make a GET request + * @param {string} url - API endpoint + * @param {object} options - Fetch options + * @returns {Promise} + */ +export async function get(url, options = {}) { + const response = await fetch(url, { + method: "GET", + headers: { ...DEFAULT_HEADERS, ...options.headers }, + ...options, + }); + return handleResponse(response); +} + +/** + * Make a POST request + * @param {string} url - API endpoint + * @param {object} data - Request body + * @param {object} options - Fetch options + * @returns {Promise} + */ +export async function post(url, data, options = {}) { + const response = await fetch(url, { + method: "POST", + headers: { ...DEFAULT_HEADERS, ...options.headers }, + body: JSON.stringify(data), + ...options, + }); + return handleResponse(response); +} + +/** + * Make a PUT request + * @param {string} url - API endpoint + * @param {object} data - Request body + * @param {object} options - Fetch options + * @returns {Promise} + */ +export async function put(url, data, options = {}) { + const response = await fetch(url, { + method: "PUT", + headers: { ...DEFAULT_HEADERS, ...options.headers }, + body: JSON.stringify(data), + ...options, + }); + return handleResponse(response); +} + +/** + * Make a DELETE request + * @param {string} url - API endpoint + * @param {object} options - Fetch options + * @returns {Promise} + */ +export async function del(url, options = {}) { + const response = await fetch(url, { + method: "DELETE", + headers: { ...DEFAULT_HEADERS, ...options.headers }, + ...options, + }); + return handleResponse(response); +} + +/** + * Handle API response + * @param {Response} response - Fetch response + * @returns {Promise} + */ +async function handleResponse(response) { + const data = await response.json(); + + if (!response.ok) { + const error = new Error(data.error || "An error occurred"); + error.status = response.status; + error.data = data; + throw error; + } + + return data; +} + +export default { get, post, put, del }; + diff --git a/src/shared/utils/apiKey.js b/src/shared/utils/apiKey.js new file mode 100644 index 00000000..046abceb --- /dev/null +++ b/src/shared/utils/apiKey.js @@ -0,0 +1,98 @@ +import crypto from "crypto"; + +const API_KEY_SECRET = process.env.API_KEY_SECRET || "endpoint-proxy-api-key-secret"; + +/** + * Generate 6-char random keyId + */ +function generateKeyId() { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < 6; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +/** + * Generate CRC (8-char HMAC) + */ +function generateCrc(machineId, keyId) { + return crypto + .createHmac("sha256", API_KEY_SECRET) + .update(machineId + keyId) + .digest("hex") + .slice(0, 8); +} + +/** + * Generate API key with machineId embedded + * Format: sk-{machineId}-{keyId}-{crc8} + * @param {string} machineId - 16-char machine ID + * @returns {{ key: string, keyId: string }} + */ +export function generateApiKeyWithMachine(machineId) { + const keyId = generateKeyId(); + const crc = generateCrc(machineId, keyId); + const key = `sk-${machineId}-${keyId}-${crc}`; + return { key, keyId }; +} + +/** + * Parse API key and extract machineId + keyId + * Supports both formats: + * - New: sk-{machineId}-{keyId}-{crc8} + * - Old: sk-{random8} + * @param {string} apiKey + * @returns {{ machineId: string, keyId: string, isNewFormat: boolean } | null} + */ +export function parseApiKey(apiKey) { + if (!apiKey || !apiKey.startsWith("sk-")) return null; + + const parts = apiKey.split("-"); + + // New format: sk-{machineId}-{keyId}-{crc8} = 4 parts + if (parts.length === 4) { + const [, machineId, keyId, crc] = parts; + + // Validate CRC + const expectedCrc = generateCrc(machineId, keyId); + if (crc !== expectedCrc) return null; + + return { machineId, keyId, isNewFormat: true }; + } + + // Old format: sk-{random8} = 2 parts + if (parts.length === 2) { + return { machineId: null, keyId: parts[1], isNewFormat: false }; + } + + return null; +} + +/** + * Verify API key CRC (only for new format) + * @param {string} apiKey + * @returns {boolean} + */ +export function verifyApiKeyCrc(apiKey) { + const parsed = parseApiKey(apiKey); + if (!parsed) return false; + + // Old format doesn't have CRC, always valid if parsed + if (!parsed.isNewFormat) return true; + + // New format already verified in parseApiKey + return true; +} + +/** + * Check if API key is new format (contains machineId) + * @param {string} apiKey + * @returns {boolean} + */ +export function isNewFormatKey(apiKey) { + const parsed = parseApiKey(apiKey); + return parsed?.isNewFormat === true; +} + diff --git a/src/shared/utils/cloud.js b/src/shared/utils/cloud.js new file mode 100644 index 00000000..02c8ff07 --- /dev/null +++ b/src/shared/utils/cloud.js @@ -0,0 +1,40 @@ +import { getMachineId } from "@/shared/utils/machine"; + +// Function to get cloud URL with machine ID +export function getCloudUrl(machineId) { + // Get from environment or default to localhost:8787 + const cloudUrl = process.env.NEXT_PUBLIC_CLOUD_URL || "http://localhost:8787"; + return `${cloudUrl}/${machineId}/v1/chat/completions`; +} + +// Function to call cloud with machine ID +export async function callCloudWithMachineId(request) { + const machineId = await getMachineId(); + if (!machineId) { + throw new Error("Could not get machine ID"); + } + + const cloudUrl = getCloudUrl(machineId); + + // Get the original request body and headers + const body = await request.json(); + const headers = new Headers(request.headers); + + // Remove authorization header since cloud won't need it (uses machineId instead) + headers.delete("authorization"); + + // Call the cloud with machine ID + const response = await fetch(cloudUrl, { + method: "POST", + headers: headers, + body: JSON.stringify(body) + }); + + return response; +} + +// Function to periodically sync provider data to cloud (now a no-op) +export function startProviderSync(cloudUrl, intervalMs = 900000) { // Default 15 minutes + console.log("Frontend sync is disabled. Use backend sync instead."); + return null; +} diff --git a/src/shared/utils/cn.js b/src/shared/utils/cn.js new file mode 100644 index 00000000..cbcfe066 --- /dev/null +++ b/src/shared/utils/cn.js @@ -0,0 +1,11 @@ +// Utility function to merge class names +// Handles conditional classes and removes duplicates + +export function cn(...classes) { + return classes + .filter(Boolean) + .join(" ") + .replace(/\s+/g, " ") + .trim(); +} + diff --git a/src/shared/utils/index.js b/src/shared/utils/index.js new file mode 100644 index 00000000..d7db9d95 --- /dev/null +++ b/src/shared/utils/index.js @@ -0,0 +1,32 @@ +// Shared Utils - Export all +export { cn } from "./cn"; +export * as api from "./api"; + +/** + * Extract error code from error message (401, 429, 503...) + * @param {string} lastError - Error message + * @returns {string|null} Error code or null + */ +export function getErrorCode(lastError) { + if (!lastError) return null; + const match = lastError.match(/\b([45]\d{2})\b/); + return match ? match[1] : "ERR"; +} + +/** + * Get relative time string (e.g. "5 min ago") + * @param {string} isoDate - ISO date string + * @returns {string} Relative time + */ +export function getRelativeTime(isoDate) { + if (!isoDate) return ""; + const diff = Date.now() - new Date(isoDate).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + diff --git a/src/shared/utils/machine.js b/src/shared/utils/machine.js new file mode 100644 index 00000000..c2038a93 --- /dev/null +++ b/src/shared/utils/machine.js @@ -0,0 +1,18 @@ +import { getConsistentMachineId } from './machineId'; + +// Get machine ID using node-machine-id with salt +export async function getMachineId() { + return await getConsistentMachineId(); +} + +// Keep sync functions for backward compatibility but make them no-ops +// (Frontend sync is disabled - use backend sync instead) +export async function syncProviderDataToCloud(cloudUrl) { + console.log("Frontend sync is disabled. Use backend sync instead."); + return Promise.resolve(true); +} + +export async function getProvidersNeedingRefresh() { + console.log("Frontend sync is disabled. Use backend sync instead."); + return Promise.resolve([]); +} diff --git a/src/shared/utils/machineId.js b/src/shared/utils/machineId.js new file mode 100644 index 00000000..7e632417 --- /dev/null +++ b/src/shared/utils/machineId.js @@ -0,0 +1,58 @@ +import { machineIdSync } from 'node-machine-id'; + +/** + * Get consistent machine ID using node-machine-id with salt + * This ensures the same physical machine gets the same ID across runs + * + * @param {string} salt - Optional salt to use (defaults to environment variable) + * @returns {Promise} Machine ID (16-character base32) + */ +export async function getConsistentMachineId(salt = null) { + // For server-side, use node-machine-id with salt + const saltValue = salt || process.env.MACHINE_ID_SALT || 'endpoint-proxy-salt'; + try { + const rawMachineId = machineIdSync(); + // Create consistent ID using salt + const crypto = await import('crypto'); + const hashedMachineId = crypto.createHash('sha256').update(rawMachineId + saltValue).digest('hex'); + // Return only first 16 characters for brevity + return hashedMachineId.substring(0, 16); + } catch (error) { + console.log('Error getting machine ID:', error); + // Fallback to random ID if node-machine-id fails + return crypto.randomUUID ? crypto.randomUUID() : + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} + +/** + * Get raw machine ID without hashing (for debugging purposes) + * @returns {Promise} Raw machine ID + */ +export async function getRawMachineId() { + // For server-side, use raw node-machine-id + try { + return machineIdSync(); + } catch (error) { + console.log('Error getting raw machine ID:', error); + // Fallback to random ID if node-machine-id fails + return crypto.randomUUID ? crypto.randomUUID() : + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} + +/** + * Check if we're running in browser or server environment + * @returns {boolean} True if in browser, false if in server + */ +export function isBrowser() { + return typeof window !== 'undefined'; +} diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js new file mode 100644 index 00000000..ad01d03e --- /dev/null +++ b/src/sse/handlers/chat.js @@ -0,0 +1,134 @@ +import { getProviderCredentials, markAccountUnavailable, clearAccountError } from "../services/auth.js"; +import { getModelInfo, getComboModels } from "../services/model.js"; +import { handleChatCore } from "open-sse/handlers/chatCore.js"; +import { errorResponse } from "open-sse/utils/error.js"; +import { checkFallbackError } from "open-sse/services/accountFallback.js"; +import { handleComboChat } from "open-sse/services/combo.js"; +import * as log from "../utils/logger.js"; +import { updateProviderCredentials, checkAndRefreshToken } from "../services/tokenRefresh.js"; + +/** + * Handle chat completion request + * Supports: OpenAI, Claude, Gemini, OpenAI Responses API formats + * Format detection and translation handled by translator + */ +export async function handleChat(request, clientRawRequest = null) { + let body; + try { + body = await request.json(); + } catch { + log.warn("CHAT", "Invalid JSON body"); + return errorResponse(400, "Invalid JSON body"); + } + + // Build clientRawRequest for logging (if not provided) + if (!clientRawRequest) { + const url = new URL(request.url); + clientRawRequest = { + endpoint: url.pathname, + body, + headers: Object.fromEntries(request.headers.entries()) + }; + } + + // Count messages (support both messages[] and input[] formats) + const msgCount = body.messages?.length || body.input?.length || 0; + const toolCount = body.tools?.length || 0; + log.request("POST", `${body.model} | ${msgCount} msgs${toolCount ? ` | ${toolCount} tools` : ""}`); + + const modelStr = body.model; + if (!modelStr) { + log.warn("CHAT", "Missing model"); + return errorResponse(400, "Missing model"); + } + + // Check if model is a combo (has multiple models with fallback) + const comboModels = await getComboModels(modelStr); + if (comboModels) { + log.info("CHAT", `Combo "${modelStr}" with ${comboModels.length} models`); + return handleComboChat({ + body, + models: comboModels, + handleSingleModel: (b, m) => handleSingleModelChat(b, m, clientRawRequest), + log + }); + } + + // Single model request + return handleSingleModelChat(body, modelStr, clientRawRequest); +} + +/** + * Handle single model chat request + */ +async function handleSingleModelChat(body, modelStr, clientRawRequest = null) { + const modelInfo = await getModelInfo(modelStr); + if (!modelInfo.provider) { + log.warn("CHAT", "Invalid model format", { model: modelStr }); + return errorResponse(400, "Invalid model format"); + } + + const { provider, model } = modelInfo; + + // Try with available accounts (fallback on errors) + let excludeConnectionId = null; + let lastError = null; + + while (true) { + const credentials = await getProviderCredentials(provider, excludeConnectionId); + if (!credentials) { + if (!excludeConnectionId) { + return errorResponse(400, `No credentials for provider: ${provider}`); + } + log.warn("CHAT", "No more accounts available", { provider }); + return new Response( + JSON.stringify({ error: lastError || "All accounts unavailable" }), + { status: 503, headers: { "Content-Type": "application/json" } } + ); + } + + log.debug("CHAT", `Using account ${credentials.connectionId} for ${provider}`); + + const refreshedCredentials = await checkAndRefreshToken(provider, credentials); + + // Use shared chatCore + const result = await handleChatCore({ + body: { ...body, model: `${provider}/${model}` }, + modelInfo: { provider, model }, + credentials: refreshedCredentials, + log, + clientRawRequest, + onCredentialsRefreshed: async (newCreds) => { + await updateProviderCredentials(credentials.connectionId, { + accessToken: newCreds.accessToken, + refreshToken: newCreds.refreshToken, + providerSpecificData: newCreds.providerSpecificData, + testStatus: "active" + }); + }, + onRequestSuccess: async () => { + // Clear error status only if currently has error (optimization) + await clearAccountError(credentials.connectionId, credentials); + } + }); + + if (result.success) return result.response; + + // Check if should fallback to next account + const { shouldFallback, cooldownMs } = checkFallbackError(result.status, result.error); + + if (shouldFallback) { + log.warn("CHAT", "Account unavailable, trying next", { + provider, + connectionId: credentials.connectionId, + status: result.status + }); + await markAccountUnavailable(credentials.connectionId, cooldownMs, result.error?.slice(0, 100), result.status, provider); + excludeConnectionId = credentials.connectionId; + lastError = result.error; + continue; + } + + return result.response; + } +} diff --git a/src/sse/services/auth.js b/src/sse/services/auth.js new file mode 100644 index 00000000..1520042a --- /dev/null +++ b/src/sse/services/auth.js @@ -0,0 +1,106 @@ +import { getProviderConnections, validateApiKey, updateProviderConnection } from "@/lib/localDb"; +import { isAccountUnavailable, getUnavailableUntil } from "open-sse/services/accountFallback.js"; +import * as log from "../utils/logger.js"; + +/** + * Get provider credentials from localDb + * Filters out unavailable accounts and returns the highest priority available account + * @param {string} provider - Provider name + * @param {string|null} excludeConnectionId - Connection ID to exclude (for retry with next account) + */ +export async function getProviderCredentials(provider, excludeConnectionId = null) { + const connections = await getProviderConnections({ provider, isActive: true }); + + if (connections.length === 0) { + log.warn("AUTH", `No credentials for ${provider}`); + return null; + } + + // Filter out unavailable accounts and excluded connection + const availableConnections = connections.filter(c => { + if (excludeConnectionId && c.id === excludeConnectionId) return false; + if (isAccountUnavailable(c.rateLimitedUntil)) return false; + return true; + }); + + if (availableConnections.length === 0) { + log.warn("AUTH", `All ${connections.length} accounts for ${provider} unavailable`); + return null; + } + + const connection = availableConnections[0]; + + return { + apiKey: connection.apiKey, + accessToken: connection.accessToken, + refreshToken: connection.refreshToken, + projectId: connection.projectId, + copilotToken: connection.providerSpecificData?.copilotToken, + providerSpecificData: connection.providerSpecificData, + connectionId: connection.id, + // Include current status for optimization check + testStatus: connection.testStatus, + lastError: connection.lastError, + rateLimitedUntil: connection.rateLimitedUntil + }; +} + +/** + * Mark account as unavailable with cooldown + */ +export async function markAccountUnavailable(connectionId, cooldownMs, reason = "Provider error", errorCode = null, provider = null) { + const rateLimitedUntil = getUnavailableUntil(cooldownMs); + await updateProviderConnection(connectionId, { + rateLimitedUntil, + testStatus: "unavailable", + lastError: reason, + errorCode, + lastErrorAt: new Date().toISOString() + }); + // log.warn("AUTH", `Account ${connectionId.slice(0,8)} unavailable until ${rateLimitedUntil}`); + + // Log to stderr for CLI to display + if (provider && errorCode && reason) { + console.error(`❌ ${provider} [${errorCode}]: ${reason}`); + } +} + +/** + * Clear account error status (only if currently has error) + * Optimized to avoid unnecessary DB updates + */ +export async function clearAccountError(connectionId, currentConnection) { + // Only update if currently has error status + const hasError = currentConnection.testStatus === "unavailable" || + currentConnection.lastError || + currentConnection.rateLimitedUntil; + + if (!hasError) return; // Skip if already clean + + await updateProviderConnection(connectionId, { + testStatus: "active", + lastError: null, + lastErrorAt: null, + rateLimitedUntil: null + }); + log.info("AUTH", `Account ${connectionId.slice(0,8)} error cleared`); +} + +/** + * Extract API key from request headers + */ +export function extractApiKey(request) { + const authHeader = request.headers.get("Authorization"); + if (authHeader?.startsWith("Bearer ")) { + return authHeader.slice(7); + } + return null; +} + +/** + * Validate API key (optional - for local use can skip) + */ +export async function isValidApiKey(apiKey) { + if (!apiKey) return false; + return await validateApiKey(apiKey); +} diff --git a/src/sse/services/model.js b/src/sse/services/model.js new file mode 100644 index 00000000..e20b6892 --- /dev/null +++ b/src/sse/services/model.js @@ -0,0 +1,35 @@ +// Re-export from open-sse with localDb integration +import { getModelAliases, getComboByName } from "@/lib/localDb"; +import { parseModel, resolveModelAliasFromMap, getModelInfoCore } from "open-sse/services/model.js"; + +export { parseModel }; + +/** + * Resolve model alias from localDb + */ +export async function resolveModelAlias(alias) { + const aliases = await getModelAliases(); + return resolveModelAliasFromMap(alias, aliases); +} + +/** + * Get full model info (parse or resolve) + */ +export async function getModelInfo(modelStr) { + return getModelInfoCore(modelStr, getModelAliases); +} + +/** + * Check if model is a combo and get models list + * @returns {Promise} Array of models or null if not a combo + */ +export async function getComboModels(modelStr) { + // Only check if it's not in provider/model format + if (modelStr.includes("/")) return null; + + const combo = await getComboByName(modelStr); + if (combo && combo.models && combo.models.length > 0) { + return combo.models; + } + return null; +} diff --git a/src/sse/services/tokenRefresh.js b/src/sse/services/tokenRefresh.js new file mode 100644 index 00000000..716db9b9 --- /dev/null +++ b/src/sse/services/tokenRefresh.js @@ -0,0 +1,173 @@ +// Re-export from open-sse with local logger +import * as log from "../utils/logger.js"; +import { updateProviderConnection } from "../../lib/localDb.js"; +import { + TOKEN_EXPIRY_BUFFER_MS as BUFFER_MS, + refreshAccessToken as _refreshAccessToken, + refreshClaudeOAuthToken as _refreshClaudeOAuthToken, + refreshGoogleToken as _refreshGoogleToken, + refreshQwenToken as _refreshQwenToken, + refreshCodexToken as _refreshCodexToken, + refreshIflowToken as _refreshIflowToken, + refreshGitHubToken as _refreshGitHubToken, + refreshCopilotToken as _refreshCopilotToken, + getAccessToken as _getAccessToken, + refreshTokenByProvider as _refreshTokenByProvider, + formatProviderCredentials as _formatProviderCredentials, + getAllAccessTokens as _getAllAccessTokens +} from "open-sse/services/tokenRefresh.js"; + +export const TOKEN_EXPIRY_BUFFER_MS = BUFFER_MS; + +// Wrap functions with local logger +export const refreshAccessToken = (provider, refreshToken, credentials) => + _refreshAccessToken(provider, refreshToken, credentials, log); + +export const refreshClaudeOAuthToken = (refreshToken) => + _refreshClaudeOAuthToken(refreshToken, log); + +export const refreshGoogleToken = (refreshToken, clientId, clientSecret) => + _refreshGoogleToken(refreshToken, clientId, clientSecret, log); + +export const refreshQwenToken = (refreshToken) => + _refreshQwenToken(refreshToken, log); + +export const refreshCodexToken = (refreshToken) => + _refreshCodexToken(refreshToken, log); + +export const refreshIflowToken = (refreshToken) => + _refreshIflowToken(refreshToken, log); + +export const refreshGitHubToken = (refreshToken) => + _refreshGitHubToken(refreshToken, log); + +export const refreshCopilotToken = (githubAccessToken) => + _refreshCopilotToken(githubAccessToken, log); + +export const getAccessToken = (provider, credentials) => + _getAccessToken(provider, credentials, log); + +export const refreshTokenByProvider = (provider, credentials) => + _refreshTokenByProvider(provider, credentials, log); + +export const formatProviderCredentials = (provider, credentials) => + _formatProviderCredentials(provider, credentials, log); + +export const getAllAccessTokens = (userInfo) => + _getAllAccessTokens(userInfo, log); + +// Local-specific: Update credentials in localDb +export async function updateProviderCredentials(connectionId, newCredentials) { + try { + const updates = {}; + + if (newCredentials.accessToken) { + updates.accessToken = newCredentials.accessToken; + } + if (newCredentials.refreshToken) { + updates.refreshToken = newCredentials.refreshToken; + } + if (newCredentials.expiresIn) { + updates.expiresAt = new Date(Date.now() + newCredentials.expiresIn * 1000).toISOString(); + updates.expiresIn = newCredentials.expiresIn; + } + if (newCredentials.providerSpecificData) { + updates.providerSpecificData = newCredentials.providerSpecificData; + } + + const result = await updateProviderConnection(connectionId, updates); + log.info("TOKEN_REFRESH", "Credentials updated in localDb", { + connectionId, + success: !!result + }); + return !!result; + } catch (error) { + log.error("TOKEN_REFRESH", "Error updating credentials in localDb", { + connectionId, + error: error.message, + }); + return false; + } +} + +// Local-specific: Check and refresh token proactively +export async function checkAndRefreshToken(provider, credentials) { + let updatedCredentials = { ...credentials }; + + // Check regular token expiry + if (updatedCredentials.expiresAt) { + const expiresAt = new Date(updatedCredentials.expiresAt).getTime(); + const now = Date.now(); + + if (expiresAt - now < TOKEN_EXPIRY_BUFFER_MS) { + log.info("TOKEN_REFRESH", "Token expiring soon, refreshing proactively", { + provider, + expiresIn: Math.round((expiresAt - now) / 1000) + }); + + const newCredentials = await getAccessToken(provider, updatedCredentials); + if (newCredentials && newCredentials.accessToken) { + await updateProviderCredentials(updatedCredentials.connectionId, newCredentials); + + updatedCredentials = { + ...updatedCredentials, + accessToken: newCredentials.accessToken, + refreshToken: newCredentials.refreshToken || updatedCredentials.refreshToken, + expiresAt: newCredentials.expiresIn + ? new Date(Date.now() + newCredentials.expiresIn * 1000).toISOString() + : updatedCredentials.expiresAt + }; + } + } + } + + // Check GitHub copilot token expiry + if (provider === "github" && updatedCredentials.providerSpecificData?.copilotTokenExpiresAt) { + const copilotExpiresAt = updatedCredentials.providerSpecificData.copilotTokenExpiresAt * 1000; + const now = Date.now(); + + if (copilotExpiresAt - now < TOKEN_EXPIRY_BUFFER_MS) { + log.info("TOKEN_REFRESH", "Copilot token expiring soon, refreshing proactively", { + provider, + expiresIn: Math.round((copilotExpiresAt - now) / 1000) + }); + + const copilotToken = await refreshCopilotToken(updatedCredentials.accessToken); + if (copilotToken) { + await updateProviderCredentials(updatedCredentials.connectionId, { + providerSpecificData: { + ...updatedCredentials.providerSpecificData, + copilotToken: copilotToken.token, + copilotTokenExpiresAt: copilotToken.expiresAt + } + }); + + updatedCredentials.providerSpecificData = { + ...updatedCredentials.providerSpecificData, + copilotToken: copilotToken.token, + copilotTokenExpiresAt: copilotToken.expiresAt + }; + } + } + } + + return updatedCredentials; +} + +// Local-specific: Refresh GitHub and Copilot tokens together +export async function refreshGitHubAndCopilotTokens(credentials) { + const newGitHubCredentials = await refreshGitHubToken(credentials.refreshToken); + if (newGitHubCredentials?.accessToken) { + const copilotToken = await refreshCopilotToken(newGitHubCredentials.accessToken); + if (copilotToken) { + return { + ...newGitHubCredentials, + providerSpecificData: { + copilotToken: copilotToken.token, + copilotTokenExpiresAt: copilotToken.expiresAt + } + }; + } + } + return newGitHubCredentials; +} diff --git a/src/sse/utils/logger.js b/src/sse/utils/logger.js new file mode 100644 index 00000000..d8127086 --- /dev/null +++ b/src/sse/utils/logger.js @@ -0,0 +1,75 @@ +// Logger utility for cloud + +const LOG_LEVELS = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3 +}; + +const LEVEL = LOG_LEVELS.DEBUG; + +function formatTime() { + return new Date().toLocaleTimeString("en-US", { hour12: false }); +} + +function formatData(data) { + if (!data) return ""; + if (typeof data === "string") return data; + try { + return JSON.stringify(data); + } catch { + return String(data); + } +} + +export function debug(tag, message, data) { + if (LEVEL <= LOG_LEVELS.DEBUG) { + const dataStr = data ? ` ${formatData(data)}` : ""; + console.log(`[${formatTime()}] 🔍 [${tag}] ${message}${dataStr}`); + } +} + +export function info(tag, message, data) { + if (LEVEL <= LOG_LEVELS.INFO) { + const dataStr = data ? ` ${formatData(data)}` : ""; + console.log(`[${formatTime()}] â„šī¸ [${tag}] ${message}${dataStr}`); + } +} + +export function warn(tag, message, data) { + if (LEVEL <= LOG_LEVELS.WARN) { + const dataStr = data ? ` ${formatData(data)}` : ""; + // console.warn(`[${formatTime()}] âš ī¸ [${tag}] ${message}${dataStr}`); + } +} + +export function error(tag, message, data) { + if (LEVEL <= LOG_LEVELS.ERROR) { + const dataStr = data ? ` ${formatData(data)}` : ""; + console.log(`[${formatTime()}] ❌ [${tag}] ${message}${dataStr}`); + } +} + +export function request(method, path, extra) { + const dataStr = extra ? ` ${formatData(extra)}` : ""; + console.log(`\x1b[36m[${formatTime()}] đŸ“Ĩ ${method} ${path}${dataStr}\x1b[0m`); +} + +export function response(status, duration, extra) { + const icon = status < 400 ? "📤" : "đŸ’Ĩ"; + const dataStr = extra ? ` ${formatData(extra)}` : ""; + console.log(`[${formatTime()}] ${icon} ${status} (${duration}ms)${dataStr}`); +} + +export function stream(event, data) { + const dataStr = data ? ` ${formatData(data)}` : ""; + console.log(`[${formatTime()}] 🌊 [STREAM] ${event}${dataStr}`); +} + +// Mask sensitive data +export function maskKey(key) { + if (!key || key.length < 8) return "***"; + return `${key.slice(0, 4)}...${key.slice(-4)}`; +} + diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 00000000..de81f82e --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,5 @@ +// Zustand Stores - Export all +export { default as useThemeStore } from "./themeStore"; +export { default as useUserStore } from "./userStore"; +export { default as useProviderStore } from "./providerStore"; + diff --git a/src/store/providerStore.js b/src/store/providerStore.js new file mode 100644 index 00000000..a67badfa --- /dev/null +++ b/src/store/providerStore.js @@ -0,0 +1,48 @@ +"use client"; + +import { create } from "zustand"; + +const useProviderStore = create((set, get) => ({ + providers: [], + loading: false, + error: null, + + setProviders: (providers) => set({ providers }), + + addProvider: (provider) => + set((state) => ({ providers: [provider, ...state.providers] })), + + updateProvider: (id, updates) => + set((state) => ({ + providers: state.providers.map((p) => + p._id === id ? { ...p, ...updates } : p + ), + })), + + removeProvider: (id) => + set((state) => ({ + providers: state.providers.filter((p) => p._id !== id), + })), + + setLoading: (loading) => set({ loading }), + + setError: (error) => set({ error }), + + fetchProviders: async () => { + set({ loading: true, error: null }); + try { + const response = await fetch("/api/providers"); + const data = await response.json(); + if (response.ok) { + set({ providers: data.providers, loading: false }); + } else { + set({ error: data.error, loading: false }); + } + } catch (error) { + set({ error: "Failed to fetch providers", loading: false }); + } + }, +})); + +export default useProviderStore; + diff --git a/src/store/themeStore.js b/src/store/themeStore.js new file mode 100644 index 00000000..ecd9a092 --- /dev/null +++ b/src/store/themeStore.js @@ -0,0 +1,54 @@ +"use client"; + +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { THEME_CONFIG } from "@/shared/constants/config"; + +const useThemeStore = create( + persist( + (set, get) => ({ + theme: THEME_CONFIG.defaultTheme, + + setTheme: (theme) => { + set({ theme }); + applyTheme(theme); + }, + + toggleTheme: () => { + const currentTheme = get().theme; + const newTheme = currentTheme === "dark" ? "light" : "dark"; + set({ theme: newTheme }); + applyTheme(newTheme); + }, + + initTheme: () => { + const theme = get().theme; + applyTheme(theme); + }, + }), + { + name: THEME_CONFIG.storageKey, + } + ) +); + +// Apply theme to document +function applyTheme(theme) { + if (typeof window === "undefined") return; + + const root = document.documentElement; + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + + const effectiveTheme = theme === "system" ? systemTheme : theme; + + if (effectiveTheme === "dark") { + root.classList.add("dark"); + } else { + root.classList.remove("dark"); + } +} + +export default useThemeStore; + diff --git a/src/store/userStore.js b/src/store/userStore.js new file mode 100644 index 00000000..a82e1449 --- /dev/null +++ b/src/store/userStore.js @@ -0,0 +1,20 @@ +"use client"; + +import { create } from "zustand"; + +const useUserStore = create((set) => ({ + user: null, + loading: false, + error: null, + + setUser: (user) => set({ user }), + + clearUser: () => set({ user: null }), + + setLoading: (loading) => set({ loading }), + + setError: (error) => set({ error }), +})); + +export default useUserStore; +