commit b05d49ffc8c06524d260c220479b1a0c1c96f332 Author: romgX <156889089+romgX@users.noreply.github.com> Date: Sat Mar 7 21:22:22 2026 +0800 Initial release v0.8.3 29 providers (7 IDE + 22 direct API), Anthropic + OpenAI API compatibility, IDE RPC proxy servers, Web dashboard, Free/Pro licensing. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f77eb4f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## [0.8.3] - 2026-03-07 + +### Initial Release + +**Providers (29 total)** +- 7 IDE providers: Claude Desktop, Claude Code, Kiro (AWS Q), Windsurf, Antigravity, OpenCode, VS Code Copilot +- 22 direct API providers: Groq, Cerebras, OpenRouter, SambaNova, DashScope, DeepSeek, Gemini, Mistral, xAI, SiliconFlow, Zhipu, Together, Fireworks, NVIDIA NIM, GitHub Models, Volcengine, Moonshot, Baichuan, Stepfun, MiniMax, Hunyuan, Ollama + +**API Compatibility** +- Anthropic Messages API (streaming + non-streaming) +- OpenAI Chat Completions API (streaming + non-streaming) +- Azure OpenAI compatible endpoint +- Bidirectional format translation with full tool/function calling support + +**Consumer Access Methods** +- IDE RPC proxy servers: Windsurf (18766), Antigravity (18767), Cursor (18780), VS Code Copilot (18769) +- Shell environment functions for CLI tools (Claude Code, OpenCode, Aider, Goose, Amp) +- API key auth: `sk-or-{provider}-{hex}` format for SDK/curl access +- Custom model groups with round-robin routing + +**Platform** +- macOS (ARM64 / x64) + Windows (x64) +- Single-file binary (Node.js SEA) — no runtime dependencies +- Web management dashboard (bilingual EN/ZH) +- Auto-detection of installed AI apps and credentials +- License system: Free (30 req/day) / Pro (unlimited) diff --git a/COMMERCIAL-LICENSE.txt b/COMMERCIAL-LICENSE.txt new file mode 100644 index 0000000..72b0eb9 --- /dev/null +++ b/COMMERCIAL-LICENSE.txt @@ -0,0 +1,69 @@ +OpenRelay Commercial License / 商业许可协议 + +Copyright (c) 2025-2026 OpenRelay Contributors + +================================================================================ +ENGLISH +================================================================================ + +This software is distributed under a dual-license model: + +1. OPEN SOURCE FRAMEWORK (MIT License) + The framework code (routing, proxy infrastructure, format translation, and + configuration) is open source under the MIT License. See the LICENSE file + for full terms. + +2. PRO FEATURES (Commercial License) + The following features require a valid Pro license for unlimited use: + + - Unlimited request volume (Free tier: 30 requests/day) + - Custom model groups with round-robin routing across multiple providers + - Priority support + + All other features — including all 29 providers, IDE RPC proxy servers, + Web dashboard, and API format translation — are available in both + Free and Pro tiers. + +3. RESTRICTIONS + - Decompilation, reverse engineering, and redistribution of obfuscated or + compiled Pro feature code are prohibited without written permission. + - Circumvention of license verification or usage limits is prohibited. + - Redistribution of the complete package (including Pro features) requires + a commercial redistribution agreement. + +4. FREE TIER + All providers and core features are available with a daily request limit. + No Pro license is required for use within this limit. + +For licensing inquiries: https://github.com/romgX/openrelay/issues + +================================================================================ +中文 +================================================================================ + +本软件采用双重许可模式分发: + +1. 开源框架(MIT 许可证) + 框架代码(路由、代理基础设施、格式转换和配置)以 MIT 许可证开源。 + 完整条款见 LICENSE 文件。 + +2. Pro 功能(商业许可) + 以下功能需要有效的 Pro 许可证方可无限制使用: + + - 无限请求量(免费版:每日 30 次) + - 自定义模型组(跨多个 Provider 轮询路由) + - 优先支持 + + 其他所有功能——包括全部 29 个提供商、IDE RPC 代理服务器、 + Web 管理面板和 API 格式转换——在免费版和 Pro 版中均可使用。 + +3. 限制条款 + - 未经书面许可,禁止反编译、逆向工程和再分发经混淆或编译的 Pro 功能代码。 + - 禁止规避许可证验证或使用限制。 + - 再分发完整软件包(包含 Pro 功能)需要签订商业再分发协议。 + +4. 免费版 + 所有提供商和核心功能均可在每日请求限额内使用。 + 在此限额内无需 Pro 许可证。 + +许可咨询:https://github.com/romgX/openrelay/issues diff --git a/DISCLAIMER.md b/DISCLAIMER.md new file mode 100644 index 0000000..9534769 --- /dev/null +++ b/DISCLAIMER.md @@ -0,0 +1,45 @@ +# Disclaimer + +## Nature of the Software + +OpenRelay is a **local network proxy tool** intended for personal learning, research, and development purposes. It runs entirely on your own machine and is functionally equivalent to well-known local proxy tools such as Charles Proxy, Fiddler, mitmproxy, and Surge — it intercepts and forwards network requests on localhost. + +OpenRelay is **not** an AI service provider. It does not generate, host, or distribute any AI content. It is a local utility that translates and forwards API requests between tools you already use. + +## What OpenRelay Does + +- Reads **your own** locally stored authentication credentials (cookies, tokens, API keys) from AI desktop applications **you have already installed and logged into** +- Exposes a unified HTTP proxy on your local machine +- Translates between API formats (Anthropic Messages API, OpenAI Chat Completions API) so that different AI clients can connect to different AI backends through a single endpoint + +## What OpenRelay Does NOT Do + +- **Does not generate AI content** — it only relays requests and responses between your tools and AI backends +- **Does not bypass paywalls or access controls** — it only uses credentials you already possess +- **Does not share accounts** — all credentials stay on your local machine +- **Does not transmit credentials externally** — authentication data never leaves your device +- **Does not modify or redistribute any third-party software** +- **Does not intercept traffic from other users or devices** (unless you explicitly configure LAN access) + +## No Affiliation + +OpenRelay is an independent project. It is **not** affiliated with, endorsed by, or associated with Anthropic, Amazon (AWS), Google, Microsoft, Cursor, Codeium (Windsurf), or any other AI service provider mentioned in this project. + +All product names, trademarks, and registered trademarks are the property of their respective owners. + +## User Responsibility + +This software is provided for **learning, research, and personal development use**. By using OpenRelay, you acknowledge and agree that: + +1. **You bear full responsibility** for all consequences arising from your use of this software +2. You are responsible for complying with the Terms of Service of all AI services you access through this tool +3. You must only use credentials for accounts you own or are authorized to use +4. You must comply with all applicable laws and regulations in your jurisdiction +5. The authors and contributors of OpenRelay assume **no liability** for any direct, indirect, incidental, or consequential damages resulting from the use or misuse of this software +6. This software is provided "as is" without warranty of any kind, express or implied + +## Legal Notice + +Local proxy tools that read the user's own credentials from their own machine operate in a well-established category of developer utilities. Users should independently evaluate the legal implications of using such tools in their specific jurisdiction and context. + +For questions or concerns, please open an issue on GitHub. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ee91819 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-2026 OpenRelay Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..ed0fc15 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,56 @@ +# Privacy Policy + +## Overview + +OpenRelay is a local proxy tool that runs entirely on your machine. Your privacy is fundamental to its design. + +## Credential Handling + +OpenRelay reads authentication credentials (cookies, tokens, API keys) from AI desktop applications you have already installed and logged into. These credentials are: + +- **Read-only** — OpenRelay reads but never modifies your stored credentials +- **In-memory only** — credentials are held in process memory during runtime, never written to OpenRelay's own files +- **Never transmitted** — credentials are only used to authenticate requests directly to the original AI service backends, never sent to OpenRelay servers or any third party + +## Data Handling + +### What stays on your machine + +- **All authentication credentials** — read from your local filesystem, used in-memory only +- **All AI conversations** — pass directly from your client to the AI backend; OpenRelay does not store, log, or inspect message content +- **Configuration data** — stored locally at `~/.openrelay/` + +### What is never collected + +- Message content is never logged, stored, or analyzed +- No usage telemetry, analytics, or behavioral data is collected +- Your credentials are never sent to OpenRelay servers + +## Network Connections + +OpenRelay makes the following network connections: + +1. **AI service backends** (e.g., api.anthropic.com, amazonaws.com, server.codeium.com) — to forward your requests using your own credentials +2. **License verification server** (`license.limitlessmeto.com`, hosted on Cloudflare) — periodic license status check + - **Sent**: anonymous device identifier and license token + - **NOT sent**: credentials, conversation content, usage details, or personal information + - **Purpose**: verify license tier (Free / Pro) and active device count +3. **GitHub Releases API** (optional) — to check for software updates + +## Local Logging + +- The `openrelay.log` file may contain request metadata (timestamps, model names, token counts) for debugging purposes +- Log files never contain message content, credentials, or personal information +- Logs are stored locally and are never transmitted + +## Data Deletion + +To completely remove all OpenRelay data: + +```bash +rm -rf ~/.openrelay/ +``` + +## Contact + +For privacy-related questions, please open an issue on GitHub. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ebd7934 --- /dev/null +++ b/README.md @@ -0,0 +1,277 @@ +# OpenRelay + +**Your AI subscriptions, everywhere.** Stop paying for AI tools you can't fully use. + +[中文说明](#中文) + +--- + +## The Problem + +How do you find more free AI model quotas? +How do you connect free or paid quotas to the tools you're already using? +How do you configure Claude Code, OpenCode, or any AI tool in one click? + +**OpenRelay solves all of this.** + +## What OpenRelay Actually Does + +### 1. Auto-discover all your AI quotas + +Launch OpenRelay and it instantly finds every AI subscription and free quota on your machine — Claude Desktop, Claude Code, Kiro, Windsurf, Antigravity, OpenCode, VS Code Copilot. All quotas at your command, zero manual configuration. + +Plus 22 direct API providers (Groq, Gemini, DeepSeek, Mistral, OpenRouter, etc.) — enter an API key once and it's available everywhere. + +**29 providers. One dashboard. One endpoint.** + +### 2. Use any quota in any tool + +Your Claude Pro subscription can now power Claude Code, Aider, Continue, Goose, Amp, or any tool that speaks Anthropic/OpenAI API: + +```bash +export ANTHROPIC_BASE_URL=http://localhost:18765 +export ANTHROPIC_API_KEY=unused +# That's it. Claude Code now uses your Claude Desktop quota. +``` + +Want to use Kiro's free Claude Sonnet quota in Aider? Just change the URL: +```bash +export ANTHROPIC_BASE_URL=http://localhost:18765/kiro +``` + +### 3. One-click setup for every CLI tool + +No more editing `.zshrc` or juggling environment variables. Open the Web dashboard, pick a provider for each tool, flip a switch: + +- **Claude Code** → route through Kiro (free Claude Sonnet) +- **Aider** → route through Groq (free, blazing fast) +- **Goose** → route through Gemini API (free 1M context) +- **OpenCode** → route through DeepSeek (cheapest coding model) + +Reopen your terminal. Done. Every tool is configured. + +### 4. Supercharge your IDE with external quotas + +Cursor quota burned through? Windsurf credits gone? Don't stop coding — seamlessly plug in any other quota source: + +| IDE | How it works | What you get | +|-----|-------------|--------------| +| **Cursor** | RPC proxy (ConnectRPC, HTTP/2) | Use Claude/Kiro/Groq/any provider inside Cursor | +| **Windsurf** | RPC proxy (ConnectRPC) | Replace Windsurf's built-in models | +| **VS Code Copilot** | Ollama BYOK bridge | Use any model as a Copilot backend | +| **Antigravity** | Gemini REST proxy | Route through any provider | + +Start the proxy from the dashboard. Your IDE doesn't know the difference. + +### 5. Combine quotas into unstoppable model groups + +This is the killer feature. Take quotas from multiple providers and merge them into a single virtual model: + +``` +"fast-group" = Groq (Llama 90B) + Cerebras (Llama 70B) + SambaNova (Llama 405B) +``` + +When Groq's free quota runs out → automatic failover to Cerebras → then SambaNova. **Your AI never stops.** Round-robin across providers ensures maximum uptime with zero manual switching. + +--- + +## Install + +### Binary (recommended, no Node.js needed) + +Download from [GitHub Releases](https://github.com/romgX/openrelay/releases): + +```bash +# macOS +curl -L -o openrelay https://github.com/romgX/openrelay/releases/latest/download/openrelay-macos-arm64 +chmod +x openrelay +xattr -d com.apple.quarantine openrelay # first time only +./openrelay +``` + +```powershell +# Windows +.\openrelay-windows-x64.exe +``` + +### npm (requires Node.js >= 18) + +```bash +npm install -g openrelay +openrelay +``` + +## Quick Start + +```bash +openrelay # Start the proxy +openrelay --setup # First time on macOS: authorize Keychain +openrelay --test # Test all provider connections +``` + +Open `http://localhost:18765` → everything is managed from the Web dashboard. + +## All 29 Providers + +### IDE Providers (auto-detect, zero config) + +| Provider | What you get | Auth | +|----------|-------------|------| +| Claude Desktop | Your Claude Pro/Free quota | Keychain cookie | +| Claude Code | Your Claude Code subscription | Keychain OAuth | +| Kiro (AWS Q) | Free Claude Sonnet + Haiku + more | `~/.aws/sso/cache/` | +| Windsurf (Codeium) | SWE-1, Kimi, and more | `state.vscdb` | +| Antigravity (Google) | Free Gemini 2.5 | Local Language Server | +| OpenCode | 35+ models including free GLM-4 | CLI subprocess | +| VS Code (Copilot) | Your Copilot subscription | GitHub OAuth | + +### Direct API Providers (bring your own key) + +Groq, Cerebras, OpenRouter, SambaNova, DashScope (Qwen), DeepSeek, Gemini, Mistral, xAI (Grok), SiliconFlow, Zhipu (GLM), Together, Fireworks, NVIDIA NIM, GitHub Models, Volcengine, Moonshot, Baichuan, Stepfun, MiniMax, Hunyuan, Ollama + +### IDE RPC Proxy Servers + +| IDE | Port | Protocol | Use case | +|-----|------|----------|----------| +| Windsurf | 18766 | ConnectRPC | Replace Windsurf's built-in models | +| Antigravity | 18767 | Gemini REST | Route Antigravity through any provider | +| Cursor | 18780 | ConnectRPC (HTTP/2+TLS) | Use any model inside Cursor | +| VS Code Copilot | 18769 | Ollama REST | Any model as Copilot backend | + +## API Endpoints + +``` +POST /v1/messages → Anthropic Messages API +POST /{provider}/v1/messages → Specific provider (Anthropic) +POST /v1/chat/completions → OpenAI Chat Completions API +POST /{provider}/v1/chat/completions → Specific provider (OpenAI) +GET /v1/models → All available models +GET /health → Provider status +GET / → Web dashboard +``` + +## Free vs Pro + +| | Free | Pro | +|---|---|---| +| Providers | All 29 | All 29 | +| Daily requests | 30 | Unlimited | +| IDE RPC proxy | Yes | Yes | +| Custom model groups | View only | Full access | +| Price | $0 | [See pricing](https://github.com/romgX/openrelay/releases) | + +## Security + +**Credentials never leave your machine.** All tokens, cookies, and API keys stay in local process memory. Nothing is uploaded anywhere. + +**Direct connections only.** Requests go straight from your machine to the AI provider. No relay server in between. + +**No request logging.** Message content is never logged, cached, or persisted. + +**Auditable.** The credential handling code ([cookie.ts](src/cookie.ts)) is open source for security review. + +See [DISCLAIMER.md](DISCLAIMER.md) and [PRIVACY.md](PRIVACY.md) for details. + +## License + +Open Core model: +- **Framework** (proxy, format translation, config): [MIT](LICENSE) +- **Pro features** (custom model groups, unlimited requests): [Commercial](COMMERCIAL-LICENSE.txt) + +--- + + +## 中文说明 + +**你的 AI 订阅,处处可用。** 别再为用不完的 AI 工具白白付费。 + +--- + +## 痛点 +怎么样找到更多的免费AI模型配额? +怎么样把免费或收费的配额接到你正在使用的工具中? +怎么样能一键配置你的claude code 、openclaw AI模型? + +**OpenRelay 一键解决。** + +## OpenRelay 能做什么 + +### 1. 自动发现你所有的 AI 配额 + +启动 OpenRelay,它立刻找到你机器上每一份 AI 订阅和免费配额 — Claude Desktop、Claude Code、Kiro、Windsurf、Antigravity、OpenCode、VS Code Copilot。所有配额任你调度,无需手动配置。 + +支持 22 个直连 API(Groq、Gemini、DeepSeek、Mistral、OpenRouter 等)— 输入一次 API Key,到处可用。 + +**29 个提供商。一个面板。一个端点。** + +### 2. 任意配额用在任意工具 + +你的 Claude Pro 订阅现在可以驱动 Claude Code、Aider、Continue、Goose、Amp,或任何支持 Anthropic/OpenAI API 的工具: + +```bash +export ANTHROPIC_BASE_URL=http://localhost:18765 +export ANTHROPIC_API_KEY=unused +# 搞定。Claude Code 现在使用你的 Claude Desktop 配额。 +``` + +想在 Aider 里用 Kiro 的免费 Claude Sonnet?改一下 URL: +```bash +export ANTHROPIC_BASE_URL=http://localhost:18765/kiro +``` + +### 3. 一键配置所有 CLI 工具 + +不再手动编辑 `.zshrc`,不再来回倒腾环境变量。打开 Web 面板,选择 Provider,点一下开关: + +- **Claude Code** → 走 Kiro(免费 Claude Sonnet) +- **Aider** → 走 Groq(免费,极速推理) +- **Goose** → 走 Gemini API(免费,100 万上下文) +- **OpenCode** → 走 DeepSeek(最便宜的编程模型) + +重开终端,完事。每个工具都配好了。 + +### 4. 给你的 IDE 无缝接入外部配额 + +Cursor 配额烧完了?Windsurf 额度用光了?别停下编码 — 无缝插入任何其他配额来源: + +| IDE | 接入方式 | 效果 | +|-----|---------|------| +| **Cursor** | RPC 代理 (ConnectRPC, HTTP/2) | 在 Cursor 里用 Claude/Kiro/Groq/任意 Provider | +| **Windsurf** | RPC 代理 (ConnectRPC) | 用任意 Provider 替换 Windsurf 内置模型 | +| **VS Code Copilot** | Ollama BYOK 桥接 | 用任意模型作为 Copilot 后端 | +| **Antigravity** | Gemini REST 代理 | 通过任意 Provider 路由 | + +在面板启动代理,IDE 无感切换。 + +### 5. 组合配额,AI 永不停机 + +把多个 Provider 的配额合并成一个虚拟模型: + +``` +"fast-group" = Groq (Llama 90B) + Cerebras (Llama 70B) + SambaNova (Llama 405B) +``` + +Groq 免费额度用完 → 自动切到 Cerebras → 再切 SambaNova。**你的 AI 永不停机。** 跨 Provider 轮询确保最大可用时间,零手动切换。 + +### 安装 + +```bash +# 下载二进制(无需 Node.js) +curl -L -o openrelay https://github.com/romgX/openrelay/releases/latest/download/openrelay-macos-arm64 +chmod +x openrelay +xattr -d com.apple.quarantine openrelay # 仅首次需要 +./openrelay + +# 或通过 npm +npm install -g openrelay && openrelay +``` + +浏览器打开 `http://localhost:18765` — 一切在 Web 面板中管理,支持中英双语。 + +### 安全 + +**凭据不离开本机** — 所有 token、cookie、API key 仅在本地内存中使用,不会上传到任何服务器。 + +**直连 API** — 请求从你的机器直接发送到 AI 后端,没有中转服务器。 + +**可审计** — 凭据处理代码([cookie.ts](src/cookie.ts))开源可审。 diff --git a/package.json b/package.json new file mode 100644 index 0000000..a19bd64 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "openrelay", + "version": "0.8.3", + "description": "Multi-provider AI local proxy — extracts auth from locally installed AI desktop apps and exposes a unified HTTP proxy", + "type": "module", + "bin": { + "openrelay": "./dist/index.js" + }, + "dependencies": { + "sql.js": "^1.14.0" + }, + "files": [ + "dist/", + "LICENSE", + "README.md", + "COMMERCIAL-LICENSE.txt" + ], + "engines": { + "node": ">=18" + }, + "license": "MIT" +} diff --git a/src/cookie.ts b/src/cookie.ts new file mode 100644 index 0000000..5702f52 --- /dev/null +++ b/src/cookie.ts @@ -0,0 +1,344 @@ +/** + * Claude Desktop Cookie decryption module + * + * macOS Chromium standard encryption scheme: + * 1. Read Safe Storage password from Keychain + * 2. PBKDF2(password, "saltysalt", 1003, 16, SHA1) → AES-128-CBC key + * 3. encrypted_value format: v10 (3 bytes) + ciphertext, IV = 16 bytes of 0x20 + * + * Windows Chromium encryption scheme: + * 1. Read master key from Local State (DPAPI-protected) + * 2. AES-256-GCM decrypt with v10 + nonce(12B) + ciphertext + tag(16B) + */ + +import * as crypto from 'node:crypto' +import * as fs from 'node:fs' +import * as path from 'node:path' +import * as os from 'node:os' +import { execFileSync } from 'node:child_process' +import { openReadonlyDb, queryAll } from './sqlite.js' + +export interface DesktopCookies { + sessionKey: string + organizationId: string + deviceId: string + cfClearance: string +} + +// ---- macOS-specific functions ---- + +/** + * Get Claude Safe Storage password from macOS Keychain + */ +function getKeychainPassword(): string { + try { + const result = execFileSync('security', [ + 'find-generic-password', '-w', + '-s', 'Claude Safe Storage', + '-a', 'Claude Key', + ], { timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }) + return result.toString().trim() + } catch (err: any) { + throw new Error( + `Failed to read Keychain: ${err?.message || 'unknown error'}\n` + + 'Please verify: 1) macOS system 2) Claude Desktop is installed and has been logged in' + ) + } +} + +/** + * Derive AES-128-CBC key + */ +function deriveKey(password: string): Buffer { + return crypto.pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1') +} + +/** + * Decrypt a single Chromium Cookie value (macOS: AES-128-CBC) + */ +function decryptValue(encryptedValue: Buffer, key: Buffer): string { + if (encryptedValue.length < 4) return '' + const prefix = encryptedValue.subarray(0, 3).toString('utf-8') + if (prefix !== 'v10') return '' + + const data = encryptedValue.subarray(3) + const iv = Buffer.alloc(16, 0x20) + try { + const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv) + const decrypted = Buffer.concat([decipher.update(data), decipher.final()]) + // Chromium has a 32-byte internal prefix before cookie value, skip it + if (decrypted.length <= 32) return '' + return decrypted.subarray(32).toString('utf-8') + } catch { + return '' + } +} + +/** + * Find Claude Desktop Cookies DB path (macOS) + */ +function findCookiesDbPathDarwin(): string | null { + const candidates = [ + path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Cookies'), + path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Default', 'Cookies'), + ] + for (const p of candidates) { + if (fs.existsSync(p)) return p + } + return null +} + +/** + * Read device ID from ant-did file (macOS) + */ +function findDeviceIdDarwin(): string { + const candidates = [ + path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'ant-did'), + path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Default', 'ant-did'), + ] + for (const p of candidates) { + try { + if (fs.existsSync(p)) return fs.readFileSync(p, 'utf-8').trim() + } catch { /* continue */ } + } + return '' +} + +/** + * Extract cookies on macOS (Keychain + AES-128-CBC) + */ +async function extractCookiesDarwin(): Promise { + const dbPath = findCookiesDbPathDarwin() + if (!dbPath) { + throw new Error( + 'Claude Desktop Cookies database not found\n' + + 'Please verify Claude Desktop is installed and has been logged in at least once' + ) + } + + const password = getKeychainPassword() + const key = deriveKey(password) + + let db: any + try { + db = await openReadonlyDb(dbPath) + } catch (err: any) { + throw new Error(`Failed to open Cookies DB: ${err?.message}`) + } + + const result: DesktopCookies = { + sessionKey: '', + organizationId: '', + deviceId: '', + cfClearance: '', + } + + try { + const rows = queryAll(db, + `SELECT name, encrypted_value, host_key FROM cookies WHERE host_key LIKE '%claude.ai%'`, + ) + + for (const row of rows) { + const encBuf = row.encrypted_value instanceof Uint8Array + ? Buffer.from(row.encrypted_value) + : Buffer.alloc(0) + const value = decryptValue(encBuf, key) + if (!value) continue + switch (row.name) { + case 'sessionKey': result.sessionKey = value; break + case 'lastActiveOrg': result.organizationId = value; break + case 'anthropic-device-id': result.deviceId = value; break + case 'cf_clearance': result.cfClearance = value; break + } + } + } finally { + db.close() + } + + if (!result.sessionKey) { + throw new Error('sessionKey not found. Please verify Claude Desktop is logged in (able to chat normally).') + } + if (!result.organizationId) { + throw new Error('lastActiveOrg not found. Please verify Claude Desktop is logged in.') + } + + if (!result.deviceId) { + result.deviceId = findDeviceIdDarwin() + } + + return result +} + +// ---- Windows-specific functions ---- + +/** + * Find Claude Desktop Cookies DB path (Windows) + * %APPDATA%\Claude\Cookies or %APPDATA%\Claude\Default\Cookies + */ +/** + * Find Claude Desktop data root directory on Windows. + * Checks both traditional Electron installer path (%APPDATA%\Claude) + * and Windows Store sandboxed path (%LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude). + * Returns the most recently modified one if both exist. + */ +function findClaudeDataRootWindows(): string | null { + const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') + const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local') + + const candidates: string[] = [ + path.join(appData, 'Claude'), + ] + + // Windows Store app: %LOCALAPPDATA%\Packages\Claude_{publisherHash}\LocalCache\Roaming\Claude + const packagesDir = path.join(localAppData, 'Packages') + try { + for (const entry of fs.readdirSync(packagesDir)) { + if (entry.startsWith('Claude_')) { + candidates.push(path.join(packagesDir, entry, 'LocalCache', 'Roaming', 'Claude')) + } + } + } catch { /* Packages dir not accessible */ } + + // Pick the one with the most recently modified Cookies file + let best: string | null = null + let bestMtime = 0 + for (const dir of candidates) { + for (const cookiePath of ['Network/Cookies', 'Cookies', 'Default/Cookies']) { + const full = path.join(dir, cookiePath) + try { + const mtime = fs.statSync(full).mtimeMs + if (mtime > bestMtime) { bestMtime = mtime; best = dir } + } catch { /* not found */ } + } + } + return best +} + +function findCookiesDbPathWindows(): string | null { + const root = findClaudeDataRootWindows() + if (!root) return null + for (const sub of ['Network/Cookies', 'Cookies', 'Default/Cookies']) { + const p = path.join(root, sub) + if (fs.existsSync(p)) return p + } + return null +} + +/** + * Find Local State file for Claude Desktop (Windows) + */ +function findLocalStatePath(): string | null { + const root = findClaudeDataRootWindows() + if (root) { + const p = path.join(root, 'Local State') + if (fs.existsSync(p)) return p + } + // Fallback: check %LOCALAPPDATA%\Claude directly + const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local') + const fallback = path.join(localAppData, 'Claude', 'Local State') + if (fs.existsSync(fallback)) return fallback + return null +} + +/** + * Read device ID from ant-did file (Windows) + */ +function findDeviceIdWindows(): string { + const root = findClaudeDataRootWindows() + const candidates = root + ? [path.join(root, 'ant-did'), path.join(root, 'Default', 'ant-did')] + : [] + for (const p of candidates) { + try { + if (fs.existsSync(p)) return fs.readFileSync(p, 'utf-8').trim() + } catch { /* continue */ } + } + return '' +} + +/** + * Extract cookies on Windows (DPAPI + AES-256-GCM) + */ +async function extractCookiesWindows(): Promise { + const { getChromiumKeyFromLocalState, decryptChromiumValueWindows } = await import('./dpapi.js') + + const dbPath = findCookiesDbPathWindows() + if (!dbPath) { + throw new Error( + 'Claude Desktop Cookies database not found\n' + + 'Please verify Claude Desktop is installed and has been logged in at least once' + ) + } + + const localStatePath = findLocalStatePath() + if (!localStatePath) { + throw new Error('Claude Desktop Local State file not found') + } + + const key = getChromiumKeyFromLocalState(localStatePath) + + let db: any + try { + db = await openReadonlyDb(dbPath) + } catch (err: any) { + throw new Error(`Failed to open Cookies DB: ${err?.message}`) + } + + const result: DesktopCookies = { + sessionKey: '', + organizationId: '', + deviceId: '', + cfClearance: '', + } + + try { + const rows = queryAll(db, + `SELECT name, encrypted_value, host_key FROM cookies WHERE host_key LIKE '%claude.ai%'`, + ) + + for (const row of rows) { + const encBuf = row.encrypted_value instanceof Uint8Array + ? Buffer.from(row.encrypted_value) + : Buffer.alloc(0) + const value = decryptChromiumValueWindows(encBuf, key) + if (!value) continue + switch (row.name) { + case 'sessionKey': result.sessionKey = value; break + case 'lastActiveOrg': result.organizationId = value; break + case 'anthropic-device-id': result.deviceId = value; break + case 'cf_clearance': result.cfClearance = value; break + } + } + } finally { + db.close() + } + + if (!result.sessionKey) { + throw new Error('sessionKey not found. Please verify Claude Desktop is logged in (able to chat normally).') + } + if (!result.organizationId) { + throw new Error('lastActiveOrg not found. Please verify Claude Desktop is logged in.') + } + + if (!result.deviceId) { + result.deviceId = findDeviceIdWindows() + } + + return result +} + +// ---- Public API ---- + +/** + * Extract and decrypt all required cookies from Claude Desktop Cookies DB. + * Dispatches to platform-specific implementation. + */ +export async function extractCookies(): Promise { + if (process.platform === 'win32') { + return extractCookiesWindows() + } + if (process.platform === 'darwin') { + return extractCookiesDarwin() + } + throw new Error(`Unsupported platform: ${process.platform}`) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3f882f8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "node", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": false + }, + "include": ["src/**/*"] +}