Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/ackness_covel.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
37 KiB
JSON

{"repo": "ackness/covel", "files": {".env.llm.example": {"448b5f90a6c58484d12f95b2e8bfc0c8f95ea459": {"t": 1785745121.4024448, "n": 1900, "h": "5c03a454d4674049", "c": "# LLM Provider Keys\n# Copy this file to .env.llm and fill in your keys.\n# These are loaded by the dev server and Docker container.\n#\n# NOTE: In production, API keys are passed per-request from the browser\n# via the X-Provider-Keys header (stored in browser localStorage).\n# Server-side env vars are only needed for:\n# 1. Local development auto-injection\n# 2. Langfuse tracing\n# 3. Server-initiated LLM calls (if any)\n\n# ── DeepSeek ─────────────────────────────────────────────────────\n# DEEPSEEK_API_KEY=sk-xxx\n# DEEPSEEK_BASE_URL=https://api.deepseek.com\n\n# ── DashScope (Alibaba Cloud / Qwen) ────────────────────────────\n# DASHSCOPE_API_KEY=sk-xxx\n# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1\n\n# ── OpenAI ───────────────────────────────────────────────────────\n# OPENAI_API_KEY=sk-xxx\n# OPENAI_BASE_URL=https://api.openai.com/v1\n\n# ── Anthropic ────────────────────────────────────────────────────\n# ANTHROPIC_API_KEY=sk-ant-xxx\n# ANTHROPIC_BASE_URL=https://api.anthropic.com\n\n# ── OpenRouter (used by embedding bench Phase 0) ─────────────────\n# OPENROUTER_API_KEY=sk-or-v1-xxx\n# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1\n\n# ── Groq ─────────────────────────────────────────────────────────\n# GROQ_API_KEY=gsk_xxx\n# GROQ_BASE_URL=https://api.groq.com/openai/v1\n\n# ── Cerebras ─────────────────────────────────────────────────────\n# CEREBRAS_API_KEY=csk-xxx\n# CEREBRAS_BASE_URL=https://api.cerebras.ai/v1\n\n# ── Packey / gateway-compatible providers ────────────────────────\n# PACKEY_API_KEY=sk-xxx\n# PACKEY_BASE_URL=https://api.packey.ai/v1\n\n# ── Local OpenAI-compatible endpoint ──────────────────────────────\n# LOCAL_API_KEY=local-placeholder\n# LOCAL_BASE_URL=http://localhost:1234/v1\n\n# ── Ollama (local, used as default embedding backend) ────────────\n# Run: ollama pull nomic-embed-text-v2-moe && ollama serve\n# OLLAMA_BASE_URL=http://localhost:11434\n"}, "d2ab5815a66b4bf4b4512dce7a6145e4da3010e0": {"t": 1785747962.975779, "n": 1900, "h": "5c03a454d4674049", "c": "# LLM Provider Keys\n# Copy this file to .env.llm and fill in your keys.\n# These are loaded by the dev server and Docker container.\n#\n# NOTE: In production, API keys are passed per-request from the browser\n# via the X-Provider-Keys header (stored in browser localStorage).\n# Server-side env vars are only needed for:\n# 1. Local development auto-injection\n# 2. Langfuse tracing\n# 3. Server-initiated LLM calls (if any)\n\n# ── DeepSeek ─────────────────────────────────────────────────────\n# DEEPSEEK_API_KEY=sk-xxx\n# DEEPSEEK_BASE_URL=https://api.deepseek.com\n\n# ── DashScope (Alibaba Cloud / Qwen) ────────────────────────────\n# DASHSCOPE_API_KEY=sk-xxx\n# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1\n\n# ── OpenAI ───────────────────────────────────────────────────────\n# OPENAI_API_KEY=sk-xxx\n# OPENAI_BASE_URL=https://api.openai.com/v1\n\n# ── Anthropic ────────────────────────────────────────────────────\n# ANTHROPIC_API_KEY=sk-ant-xxx\n# ANTHROPIC_BASE_URL=https://api.anthropic.com\n\n# ── OpenRouter (used by embedding bench Phase 0) ─────────────────\n# OPENROUTER_API_KEY=sk-or-v1-xxx\n# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1\n\n# ── Groq ─────────────────────────────────────────────────────────\n# GROQ_API_KEY=gsk_xxx\n# GROQ_BASE_URL=https://api.groq.com/openai/v1\n\n# ── Cerebras ─────────────────────────────────────────────────────\n# CEREBRAS_API_KEY=csk-xxx\n# CEREBRAS_BASE_URL=https://api.cerebras.ai/v1\n\n# ── Packey / gateway-compatible providers ────────────────────────\n# PACKEY_API_KEY=sk-xxx\n# PACKEY_BASE_URL=https://api.packey.ai/v1\n\n# ── Local OpenAI-compatible endpoint ──────────────────────────────\n# LOCAL_API_KEY=local-placeholder\n# LOCAL_BASE_URL=http://localhost:1234/v1\n\n# ── Ollama (local, used as default embedding backend) ────────────\n# Run: ollama pull nomic-embed-text-v2-moe && ollama serve\n# OLLAMA_BASE_URL=http://localhost:11434\n"}, "f80f5f2cdc771b14f96b586e85cb246fd4d329f9": {"t": 1785747964.0391517, "n": 1336, "h": "048356fa37764928", "c": "# LLM Provider Keys\n# Copy this file to .env.llm and fill in your keys.\n# These are loaded by the dev server and Docker container.\n#\n# NOTE: In production, API keys are passed per-request from the browser\n# via the X-Provider-Keys header (stored in browser localStorage).\n# Server-side env vars are only needed for:\n# 1. Local development auto-injection\n# 2. Langfuse tracing\n# 3. Server-initiated LLM calls (if any)\n\n# ── DeepSeek ─────────────────────────────────────────────────────\n# DEEPSEEK_API_KEY=sk-xxx\n# DEEPSEEK_BASE_URL=https://api.deepseek.com\n\n# ── DashScope (Alibaba Cloud / Qwen) ────────────────────────────\n# DASHSCOPE_API_KEY=sk-xxx\n# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1\n\n# ── OpenAI ───────────────────────────────────────────────────────\n# OPENAI_API_KEY=sk-xxx\n# OPENAI_BASE_URL=https://api.openai.com/v1\n\n# ── Anthropic ────────────────────────────────────────────────────\n# ANTHROPIC_API_KEY=sk-ant-xxx\n# ANTHROPIC_BASE_URL=https://api.anthropic.com\n\n# ── OpenRouter (used by embedding bench Phase 0) ─────────────────\n# OPENROUTER_API_KEY=sk-or-v1-xxx\n# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1\n\n# ── Ollama (local, used as default embedding backend) ────────────\n# Run: ollama pull nomic-embed-text-v2-moe && ollama serve\n# OLLAMA_BASE_URL=http://localhost:11434\n"}, "b05f099edcb853793698c5ee0cd9db61af19eec9": {"t": 1785747965.099939, "n": 1013, "h": "3110d21de4d1391f", "c": "# LLM Provider Keys\n# Copy this file to .env.llm and fill in your keys.\n# These are loaded by the dev server and Docker container.\n#\n# NOTE: In production, API keys are passed per-request from the browser\n# via the X-Provider-Keys header (stored in browser localStorage).\n# Server-side env vars are only needed for:\n# 1. Local development auto-injection\n# 2. Langfuse tracing\n# 3. Server-initiated LLM calls (if any)\n\n# ── DeepSeek ─────────────────────────────────────────────────────\n# DEEPSEEK_API_KEY=sk-xxx\n# DEEPSEEK_BASE_URL=https://api.deepseek.com\n\n# ── DashScope (Alibaba Cloud / Qwen) ────────────────────────────\n# DASHSCOPE_API_KEY=sk-xxx\n# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1\n\n# ── OpenAI ───────────────────────────────────────────────────────\n# OPENAI_API_KEY=sk-xxx\n# OPENAI_BASE_URL=https://api.openai.com/v1\n\n# ── Anthropic ────────────────────────────────────────────────────\n# ANTHROPIC_API_KEY=sk-ant-xxx\n# ANTHROPIC_BASE_URL=https://api.anthropic.com\n"}, "31fa35758231b354ff729e6765a930e81bfa4a62": {"t": 1785747966.1733854, "n": 321, "h": "cd743ee7def60cc0", "c": "# LLM Provider Keys (local development only)\n# Copy this file to .env.llm and fill in your keys\n# This file is loaded automatically by the dev server\n\n# DASHSCOPE_API_KEY=sk-xxx\n# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1\n\n# DEEPSEEK_API_KEY=sk-xxx\n# DEEPSEEK_BASE_URL=https://api.deepseek.com\n"}, "6d01f0bf4fab12917dad32a25a901f73885e66a9": {"t": 1786067286.0961494, "n": 1900, "h": "5c03a454d4674049", "c": "# LLM Provider Keys\n# Copy this file to .env.llm and fill in your keys.\n# These are loaded by the dev server and Docker container.\n#\n# NOTE: In production, API keys are passed per-request from the browser\n# via the X-Provider-Keys header (stored in browser localStorage).\n# Server-side env vars are only needed for:\n# 1. Local development auto-injection\n# 2. Langfuse tracing\n# 3. Server-initiated LLM calls (if any)\n\n# ── DeepSeek ─────────────────────────────────────────────────────\n# DEEPSEEK_API_KEY=sk-xxx\n# DEEPSEEK_BASE_URL=https://api.deepseek.com\n\n# ── DashScope (Alibaba Cloud / Qwen) ────────────────────────────\n# DASHSCOPE_API_KEY=sk-xxx\n# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1\n\n# ── OpenAI ───────────────────────────────────────────────────────\n# OPENAI_API_KEY=sk-xxx\n# OPENAI_BASE_URL=https://api.openai.com/v1\n\n# ── Anthropic ────────────────────────────────────────────────────\n# ANTHROPIC_API_KEY=sk-ant-xxx\n# ANTHROPIC_BASE_URL=https://api.anthropic.com\n\n# ── OpenRouter (used by embedding bench Phase 0) ─────────────────\n# OPENROUTER_API_KEY=sk-or-v1-xxx\n# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1\n\n# ── Groq ─────────────────────────────────────────────────────────\n# GROQ_API_KEY=gsk_xxx\n# GROQ_BASE_URL=https://api.groq.com/openai/v1\n\n# ── Cerebras ─────────────────────────────────────────────────────\n# CEREBRAS_API_KEY=csk-xxx\n# CEREBRAS_BASE_URL=https://api.cerebras.ai/v1\n\n# ── Packey / gateway-compatible providers ────────────────────────\n# PACKEY_API_KEY=sk-xxx\n# PACKEY_BASE_URL=https://api.packey.ai/v1\n\n# ── Local OpenAI-compatible endpoint ──────────────────────────────\n# LOCAL_API_KEY=local-placeholder\n# LOCAL_BASE_URL=http://localhost:1234/v1\n\n# ── Ollama (local, used as default embedding backend) ────────────\n# Run: ollama pull nomic-embed-text-v2-moe && ollama serve\n# OLLAMA_BASE_URL=http://localhost:11434\n"}}, "apps/desktop/src/env-files.ts": {"448b5f90a6c58484d12f95b2e8bfc0c8f95ea459": {"t": 1785745566.720062, "n": 2878, "h": "cd12eead196972d1", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { normalizeProviderKeyMap, toApiKeyEnvMap } from \"./provider-keys.js\";\n\nexport function loadEnvFiles(baseDir: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const name of [\".env\", \".env.llm\"]) {\n const filePath = path.join(baseDir, name);\n if (!fs.existsSync(filePath)) continue;\n parseEnvFileInto(filePath, result);\n }\n return result;\n}\n\n/**\n * Read `~/.covel/keys.env` into a provider-id keyed record for the renderer.\n * Missing file is fine (fresh install). Legacy bare keys like `deepseek=...`\n * are folded into the same shape as `DEEPSEEK_API_KEY=...`.\n */\nexport function loadKeysEnv(keysFile: string): Record<string, string> {\n const result: Record<string, string> = {};\n if (!fs.existsSync(keysFile)) return result;\n parseEnvFileInto(keysFile, result);\n return normalizeProviderKeyMap(result);\n}\n\nexport function loadKeysEnvForChild(keysFile: string): Record<string, string> {\n return toApiKeyEnvMap(loadKeysEnv(keysFile));\n}\n\nexport function saveKeysEnv(\n keysFile: string,\n keys: Record<string, string>,\n): void {\n const envKeys = toApiKeyEnvMap(keys);\n const body =\n `# Covel provider API keys. One KEY=VALUE per line.\\n` +\n `# Example:\\n# DEEPSEEK_API_KEY=sk-xxx\\n# OPENAI_API_KEY=sk-xxx\\n\\n` +\n Object.entries(envKeys)\n // audit M2: reject values with CR/LF — a newline would inject extra\n // `KEY=VALUE` lines and poison other providers' key parsing.\n .filter(([k, v]) => {\n if (!k || typeof v !== \"string\" || !v.trim()) return false;\n if (/[\\r\\n]/.test(v)) {\n console.warn(\n `[env-files] Skipping key \"${k}\": value contains a newline`,\n );\n return false;\n }\n return true;\n })\n .map(([k, v]) => `${k}=${v.trim()}`)\n .join(\"\\n\") +\n \"\\n\";\n fs.mkdirSync(path.dirname(keysFile), { recursive: true });\n // mode in writeFileSync only applies when creating a new file. Re-assert\n // 0600 after the write so an existing looser-permission file gets tightened\n // (audit M1). chmod is a no-op on Windows but does not throw.\n fs.writeFileSync(keysFile, body, { mode: 0o600 });\n fs.chmodSync(keysFile, 0o600);\n}\n\nfunction parseEnvFileInto(\n filePath: string,\n into: Record<string, string>,\n): void {\n const content = fs.readFileSync(filePath, \"utf-8\");\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIdx = trimmed.indexOf(\"=\");\n if (eqIdx < 0) continue;\n const key = trimmed.slice(0, eqIdx).trim();\n let val = trimmed.slice(eqIdx + 1).trim();\n if (\n (val.startsWith('\"') && val.endsWith('\"')) ||\n (val.startsWith(\"'\") && val.endsWith(\"'\"))\n ) {\n val = val.slice(1, -1);\n }\n into[key] = val;\n }\n}\n"}, "8b0612a5acc2a820d5dc2c60d9b3ce9b00247583": {"t": 1785747963.4429622, "n": 2878, "h": "cd12eead196972d1", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { normalizeProviderKeyMap, toApiKeyEnvMap } from \"./provider-keys.js\";\n\nexport function loadEnvFiles(baseDir: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const name of [\".env\", \".env.llm\"]) {\n const filePath = path.join(baseDir, name);\n if (!fs.existsSync(filePath)) continue;\n parseEnvFileInto(filePath, result);\n }\n return result;\n}\n\n/**\n * Read `~/.covel/keys.env` into a provider-id keyed record for the renderer.\n * Missing file is fine (fresh install). Legacy bare keys like `deepseek=...`\n * are folded into the same shape as `DEEPSEEK_API_KEY=...`.\n */\nexport function loadKeysEnv(keysFile: string): Record<string, string> {\n const result: Record<string, string> = {};\n if (!fs.existsSync(keysFile)) return result;\n parseEnvFileInto(keysFile, result);\n return normalizeProviderKeyMap(result);\n}\n\nexport function loadKeysEnvForChild(keysFile: string): Record<string, string> {\n return toApiKeyEnvMap(loadKeysEnv(keysFile));\n}\n\nexport function saveKeysEnv(\n keysFile: string,\n keys: Record<string, string>,\n): void {\n const envKeys = toApiKeyEnvMap(keys);\n const body =\n `# Covel provider API keys. One KEY=VALUE per line.\\n` +\n `# Example:\\n# DEEPSEEK_API_KEY=sk-xxx\\n# OPENAI_API_KEY=sk-xxx\\n\\n` +\n Object.entries(envKeys)\n // audit M2: reject values with CR/LF — a newline would inject extra\n // `KEY=VALUE` lines and poison other providers' key parsing.\n .filter(([k, v]) => {\n if (!k || typeof v !== \"string\" || !v.trim()) return false;\n if (/[\\r\\n]/.test(v)) {\n console.warn(\n `[env-files] Skipping key \"${k}\": value contains a newline`,\n );\n return false;\n }\n return true;\n })\n .map(([k, v]) => `${k}=${v.trim()}`)\n .join(\"\\n\") +\n \"\\n\";\n fs.mkdirSync(path.dirname(keysFile), { recursive: true });\n // mode in writeFileSync only applies when creating a new file. Re-assert\n // 0600 after the write so an existing looser-permission file gets tightened\n // (audit M1). chmod is a no-op on Windows but does not throw.\n fs.writeFileSync(keysFile, body, { mode: 0o600 });\n fs.chmodSync(keysFile, 0o600);\n}\n\nfunction parseEnvFileInto(\n filePath: string,\n into: Record<string, string>,\n): void {\n const content = fs.readFileSync(filePath, \"utf-8\");\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIdx = trimmed.indexOf(\"=\");\n if (eqIdx < 0) continue;\n const key = trimmed.slice(0, eqIdx).trim();\n let val = trimmed.slice(eqIdx + 1).trim();\n if (\n (val.startsWith('\"') && val.endsWith('\"')) ||\n (val.startsWith(\"'\") && val.endsWith(\"'\"))\n ) {\n val = val.slice(1, -1);\n }\n into[key] = val;\n }\n}\n"}, "010e1cf71682111c280c047f27be4b65bd35926e": {"t": 1785747965.5636823, "n": 2246, "h": "5faa7da9432191e0", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { normalizeProviderKeyMap, toApiKeyEnvMap } from \"./provider-keys.js\";\n\nexport function loadEnvFiles(baseDir: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const name of [\".env\", \".env.llm\"]) {\n const filePath = path.join(baseDir, name);\n if (!fs.existsSync(filePath)) continue;\n parseEnvFileInto(filePath, result);\n }\n return result;\n}\n\n/**\n * Read `~/.covel/keys.env` into a provider-id keyed record for the renderer.\n * Missing file is fine (fresh install). Legacy bare keys like `deepseek=...`\n * are folded into the same shape as `DEEPSEEK_API_KEY=...`.\n */\nexport function loadKeysEnv(keysFile: string): Record<string, string> {\n const result: Record<string, string> = {};\n if (!fs.existsSync(keysFile)) return result;\n parseEnvFileInto(keysFile, result);\n return normalizeProviderKeyMap(result);\n}\n\nexport function loadKeysEnvForChild(keysFile: string): Record<string, string> {\n return toApiKeyEnvMap(loadKeysEnv(keysFile));\n}\n\nexport function saveKeysEnv(\n keysFile: string,\n keys: Record<string, string>,\n): void {\n const envKeys = toApiKeyEnvMap(keys);\n const body =\n `# Covel provider API keys. One KEY=VALUE per line.\\n` +\n `# Example:\\n# DEEPSEEK_API_KEY=sk-xxx\\n# OPENAI_API_KEY=sk-xxx\\n\\n` +\n Object.entries(envKeys)\n .filter(([k, v]) => k && typeof v === \"string\" && v.trim())\n .map(([k, v]) => `${k}=${v.trim()}`)\n .join(\"\\n\") +\n \"\\n\";\n fs.mkdirSync(path.dirname(keysFile), { recursive: true });\n fs.writeFileSync(keysFile, body, { mode: 0o600 });\n}\n\nfunction parseEnvFileInto(\n filePath: string,\n into: Record<string, string>,\n): void {\n const content = fs.readFileSync(filePath, \"utf-8\");\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIdx = trimmed.indexOf(\"=\");\n if (eqIdx < 0) continue;\n const key = trimmed.slice(0, eqIdx).trim();\n let val = trimmed.slice(eqIdx + 1).trim();\n if (\n (val.startsWith('\"') && val.endsWith('\"')) ||\n (val.startsWith(\"'\") && val.endsWith(\"'\"))\n ) {\n val = val.slice(1, -1);\n }\n into[key] = val;\n }\n}\n"}, "6d01f0bf4fab12917dad32a25a901f73885e66a9": {"t": 1786067408.11729, "n": 2878, "h": "cd12eead196972d1", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { normalizeProviderKeyMap, toApiKeyEnvMap } from \"./provider-keys.js\";\n\nexport function loadEnvFiles(baseDir: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const name of [\".env\", \".env.llm\"]) {\n const filePath = path.join(baseDir, name);\n if (!fs.existsSync(filePath)) continue;\n parseEnvFileInto(filePath, result);\n }\n return result;\n}\n\n/**\n * Read `~/.covel/keys.env` into a provider-id keyed record for the renderer.\n * Missing file is fine (fresh install). Legacy bare keys like `deepseek=...`\n * are folded into the same shape as `DEEPSEEK_API_KEY=...`.\n */\nexport function loadKeysEnv(keysFile: string): Record<string, string> {\n const result: Record<string, string> = {};\n if (!fs.existsSync(keysFile)) return result;\n parseEnvFileInto(keysFile, result);\n return normalizeProviderKeyMap(result);\n}\n\nexport function loadKeysEnvForChild(keysFile: string): Record<string, string> {\n return toApiKeyEnvMap(loadKeysEnv(keysFile));\n}\n\nexport function saveKeysEnv(\n keysFile: string,\n keys: Record<string, string>,\n): void {\n const envKeys = toApiKeyEnvMap(keys);\n const body =\n `# Covel provider API keys. One KEY=VALUE per line.\\n` +\n `# Example:\\n# DEEPSEEK_API_KEY=sk-xxx\\n# OPENAI_API_KEY=sk-xxx\\n\\n` +\n Object.entries(envKeys)\n // audit M2: reject values with CR/LF — a newline would inject extra\n // `KEY=VALUE` lines and poison other providers' key parsing.\n .filter(([k, v]) => {\n if (!k || typeof v !== \"string\" || !v.trim()) return false;\n if (/[\\r\\n]/.test(v)) {\n console.warn(\n `[env-files] Skipping key \"${k}\": value contains a newline`,\n );\n return false;\n }\n return true;\n })\n .map(([k, v]) => `${k}=${v.trim()}`)\n .join(\"\\n\") +\n \"\\n\";\n fs.mkdirSync(path.dirname(keysFile), { recursive: true });\n // mode in writeFileSync only applies when creating a new file. Re-assert\n // 0600 after the write so an existing looser-permission file gets tightened\n // (audit M1). chmod is a no-op on Windows but does not throw.\n fs.writeFileSync(keysFile, body, { mode: 0o600 });\n fs.chmodSync(keysFile, 0o600);\n}\n\nfunction parseEnvFileInto(\n filePath: string,\n into: Record<string, string>,\n): void {\n const content = fs.readFileSync(filePath, \"utf-8\");\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIdx = trimmed.indexOf(\"=\");\n if (eqIdx < 0) continue;\n const key = trimmed.slice(0, eqIdx).trim();\n let val = trimmed.slice(eqIdx + 1).trim();\n if (\n (val.startsWith('\"') && val.endsWith('\"')) ||\n (val.startsWith(\"'\") && val.endsWith(\"'\"))\n ) {\n val = val.slice(1, -1);\n }\n into[key] = val;\n }\n}\n"}}, "apps/server/tests/api/config-api.test.ts": {"448b5f90a6c58484d12f95b2e8bfc0c8f95ea459": {"t": 1786068276.3252604, "n": 12409, "h": "e9efb42361c18b1d", "c": "import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { Hono } from \"hono\";\nimport { createConfigApiRoutes } from \"../../src/routes/config-api.js\";\n\nconst ENV_KEYS = [\n \"COVEL_HOME\",\n \"COVEL_DESKTOP_REST\",\n \"COVEL_DESKTOP_REST_TOKEN\",\n \"COVEL_DATA_ROOT\",\n \"SQLITE_PATH\",\n \"COVEL_LOGS_DIR\",\n \"COVEL_LLM_TOML\",\n \"COVEL_USER_PLUGINS_DIR\",\n \"COVEL_USER_WORLDS_DIR\",\n] as const;\n\nfunction buildApp(apiKeys: Record<string, string>): Hono {\n const app = new Hono();\n app.route(\"/\", createConfigApiRoutes({ apiKeys }));\n return app;\n}\n\ndescribe(\"config API env and file contracts\", () => {\n let tmpHome: string;\n let apiKeys: Record<string, string>;\n const savedEnv: Partial<\n Record<(typeof ENV_KEYS)[number], string | undefined>\n > = {};\n\n beforeEach(() => {\n for (const key of ENV_KEYS) {\n savedEnv[key] = process.env[key];\n delete process.env[key];\n }\n tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), \"covel-config-api-\"));\n apiKeys = {};\n });\n\n afterEach(() => {\n for (const key of ENV_KEYS) {\n if (savedEnv[key] === undefined) {\n delete process.env[key];\n } else {\n process.env[key] = savedEnv[key];\n }\n }\n fs.rmSync(tmpHome, { recursive: true, force: true });\n });\n\n it(\"reports non-desktop mode without exposing local config paths\", async () => {\n const app = buildApp(apiKeys);\n\n const res = await app.request(\"/api/config/info\");\n\n expect(res.status).toBe(200);\n await expect(res.json()).resolves.toMatchObject({\n isDesktop: false,\n covelHome: null,\n dataRoot: null,\n keysEnvPath: null,\n dbPath: \"./data/covel.db\",\n logsDir: null,\n llmTomlPath: null,\n pluginsDir: null,\n worldsDir: null,\n });\n });\n\n it(\"reports desktop paths from runtime env\", async () => {\n process.env.COVEL_HOME = tmpHome;\n process.env.COVEL_DATA_ROOT = path.join(tmpHome, \"data-root\");\n process.env.SQLITE_PATH = path.join(tmpHome, \"data\", \"covel.db\");\n process.env.COVEL_LOGS_DIR = path.join(tmpHome, \"logs\");\n process.env.COVEL_LLM_TOML = path.join(tmpHome, \"llm.toml\");\n process.env.COVEL_USER_PLUGINS_DIR = path.join(tmpHome, \"plugins\");\n process.env.COVEL_USER_WORLDS_DIR = path.join(tmpHome, \"worlds\");\n const app = buildApp(apiKeys);\n\n const body = (await (\n await app.request(\"/api/config/info\")\n ).json()) as Record<string, unknown>;\n\n expect(body).toMatchObject({\n isDesktop: true,\n covelHome: tmpHome,\n dataRoot: path.join(tmpHome, \"data-root\"),\n dbPath: path.join(tmpHome, \"data\", \"covel.db\"),\n logsDir: path.join(tmpHome, \"logs\"),\n llmTomlPath: path.join(tmpHome, \"llm.toml\"),\n keysEnvPath: path.join(tmpHome, \"keys.env\"),\n pluginsDir: path.join(tmpHome, \"plugins\"),\n worldsDir: path.join(tmpHome, \"worlds\"),\n });\n });\n\n it(\"keeps key persistence disabled outside desktop mode\", async () => {\n const app = buildApp(apiKeys);\n\n const listRes = await app.request(\"/api/config/keys\");\n expect(listRes.status).toBe(200);\n await expect(listRes.json()).resolves.toEqual({ providers: [] });\n\n const putRes = await app.request(\"/api/config/keys\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ deepseek: \"sk-test\" }),\n });\n expect(putRes.status).toBe(400);\n expect(apiKeys).toEqual({});\n });\n\n it(\"writes keys.env, normalizes provider ids and updates the live apiKeys map\", async () => {\n process.env.COVEL_HOME = tmpHome;\n const app = buildApp(apiKeys);\n\n const putRes = await app.request(\"/api/config/keys\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n deepseek: \" sk-deepseek \",\n OPEN_ROUTER_API_KEY: \"sk-open-router\",\n invalidEmpty: \"\",\n }),\n });\n\n expect(putRes.status).toBe(200);\n expect(apiKeys).toEqual({\n deepseek: \"sk-deepseek\",\n \"open-router\": \"sk-open-router\",\n });\n const file = fs.readFileSync(path.join(tmpHome, \"keys.env\"), \"utf-8\");\n expect(file).toContain(\"DEEPSEEK_API_KEY=sk-deepseek\");\n expect(file).toContain(\"OPEN_ROUTER_API_KEY=sk-open-router\");\n expect(file).not.toContain(\"invalidEmpty\");\n\n const listBody = (await (await app.request(\"/api/config/keys\")).json()) as {\n providers: string[];\n };\n expect(listBody.providers.sort()).toEqual([\"deepseek\", \"open-router\"]);\n expect(JSON.stringify(listBody)).not.toContain(\"sk-\");\n });\n\n it(\"removes keys from file and live apiKeys map when value is empty\", async () => {\n process.env.COVEL_HOME = tmpHome;\n fs.writeFileSync(\n path.join(tmpHome, \"keys.env\"),\n \"DEEPSEEK_API_KEY=old\\nOPEN_ROUTER_API_KEY=old-open\\n\",\n );\n apiKeys.deepseek = \"old\";\n apiKeys[\"open-router\"] = \"old-open\";\n const app = buildApp(apiKeys);\n\n const res = await app.request(\"/api/config/keys\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ deepseek: \"\", \"open-router\": null }),\n });\n\n expect(res.status).toBe(200);\n expect(apiKeys).toEqual({});\n expect(\n fs.readFileSync(path.join(tmpHome, \"keys.env\"), \"utf-8\"),\n ).not.toContain(\"_API_KEY=\");\n });\n\n it(\"round-trips settings.json and recovers from malformed settings\", async () => {\n process.env.COVEL_HOME = tmpHome;\n const app = buildApp(apiKeys);\n\n const putRes = await app.request(\"/api/config/settings\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ entries: { theme: \"dark\", modelSlot: \"story\" } }),\n });\n expect(putRes.status).toBe(200);\n\n const getBody = (await (\n await app.request(\"/api/config/settings\")\n ).json()) as Record<string, unknown>;\n expect(getBody).toMatchObject({\n schemaVersion: 1,\n entries: { theme: \"dark\", modelSlot: \"story\" },\n });\n expect(typeof getBody.savedAt).toBe(\"string\");\n\n fs.writeFileSync(path.join(tmpHome, \"settings.json\"), \"{bad json\");\n await expect(\n (await app.request(\"/api/config/settings\")).json(),\n ).resolves.toEqual({\n schemaVersion: 1,\n savedAt: \"\",\n entries: {},\n });\n });\n\n it(\"rewrites data_root only for absolute paths\", async () => {\n process.env.COVEL_HOME = tmpHome;\n const app = buildApp(apiKeys);\n\n const relativeRes = await app.request(\"/api/config/data-root\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ path: \"relative/path\" }),\n });\n expect(relativeRes.status).toBe(400);\n\n const absolutePath = path.join(tmpHome, \"new-data\");\n const okRes = await app.request(\"/api/config/data-root\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ path: absolutePath }),\n });\n expect(okRes.status).toBe(200);\n await expect(okRes.json()).resolves.toEqual({\n ok: true,\n restartRequired: true,\n });\n\n const configToml = fs.readFileSync(\n path.join(tmpHome, \"config.toml\"),\n \"utf-8\",\n );\n expect(configToml).toContain(\n `[paths]\\ndata_root = ${JSON.stringify(absolutePath)}`,\n );\n });\n\n // ── Desktop REST bearer token guard ────────────────────────────\n //\n // When the desktop shell injects COVEL_DESKTOP_REST_TOKEN into the sidecar\n // env, every privileged write must carry a matching `Authorization: Bearer\n // <token>` header. `/api/config/info` advertises `requiresAuth: true` so\n // the renderer attaches the header for privileged calls.\n describe(\"desktop REST bearer token guard\", () => {\n const TOKEN = \"token-deadbeefcafebabe\";\n\n it(\"advertises requiresAuth on /api/config/info when token is set\", async () => {\n process.env.COVEL_HOME = tmpHome;\n process.env.COVEL_DESKTOP_REST_TOKEN = TOKEN;\n const app = buildApp(apiKeys);\n\n const body = (await (await app.request(\"/api/config/info\")).json()) as {\n requiresAuth?: boolean;\n };\n expect(body.requiresAuth).toBe(true);\n });\n\n it(\"reports requiresAuth: false when token is absent\", async () => {\n process.env.COVEL_HOME = tmpHome;\n const app = buildApp(apiKeys);\n\n const body = (await (await app.request(\"/api/config/info\")).json()) as {\n requiresAuth?: boolean;\n };\n expect(body.requiresAuth).toBe(false);\n });\n\n it(\"rejects PUT /api/config/keys without Authorization header\", async () => {\n process.env.COVEL_HOME = tmpHome;\n process.env.COVEL_DESKTOP_REST_TOKEN = TOKEN;\n const app = buildApp(apiKeys);\n\n const res = await app.request(\"/api/config/keys\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ deepseek: \"sk-test\" }),\n });\n\n expect(res.status).toBe(401);\n expect(apiKeys).toEqual({});\n expect(fs.existsSync(path.join(tmpHome, \"keys.env\"))).toBe(false);\n });\n\n it(\"rejects PUT /api/config/keys with a wrong token\", async () => {\n process.env.COVEL_HOME = tmpHome;\n process.env.COVEL_DESKTOP_REST_TOKEN = TOKEN;\n const app = buildApp(apiKeys);\n\n const res = await app.request(\"/api/config/keys\", {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer wrong-token\",\n },\n body: JSON.stringify({ deepseek: \"sk-test\" }),\n });\n\n expect(res.status).toBe(401);\n expect(apiKeys).toEqual({});\n });\n\n it(\"accepts PUT /api/config/keys with the correct bearer token\", async () => {\n process.env.COVEL_HOME = tmpHome;\n process.env.COVEL_DESKTOP_REST_TOKEN = TOKEN;\n const app = buildApp(apiKeys);\n\n const res = await app.request(\"/api/config/keys\", {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${TOKEN}`,\n },\n body: JSON.stringify({ deepseek: \"sk-test\" }),\n });\n\n expect(res.status).toBe(200);\n expect(apiKeys).toEqual({ deepseek: \"sk-test\" });\n });\n\n it(\"gates settings, data-root, and open-folder writes with the same token\", async () => {\n process.env.COVEL_HOME = tmpHome;\n process.env.COVEL_DESKTOP_REST_TOKEN = TOKEN;\n const app = buildApp(apiKeys);\n\n const settingsRes = await app.request(\"/api/config/settings\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ entries: { theme: \"dark\" } }),\n });\n expect(settingsRes.status).toBe(401);\n\n const dataRootRes = await app.request(\"/api/config/data-root\", {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ path: path.join(tmpHome, \"new-data\") }),\n });\n expect(dataRootRes.status).toBe(401);\n\n const openRes = await app.request(\"/api/config/open-folder\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ target: \"config\" }),\n });\n expect(openRes.status).toBe(401);\n });\n\n it(\"gates GET /api/config/settings with the same token\", async () => {\n process.env.COVEL_HOME = tmpHome;\n process.env.COVEL_DESKTOP_REST_TOKEN = TOKEN;\n fs.writeFileSync(\n path.join(tmpHome, \"settings.json\"),\n JSON.stringify({\n schemaVersion: 1,\n savedAt: \"now\",\n entries: { importedSecret: \"sk-test\" },\n }),\n );\n const app = buildApp(apiKeys);\n\n expect((await app.request(\"/api/config/settings\")).status).toBe(401);\n const okRes = await app.request(\"/api/config/settings\", {\n headers: { Authorization: `Bearer ${TOKEN}` },\n });\n expect(okRes.status).toBe(200);\n await expect(okRes.json()).resolves.toMatchObject({\n entries: { importedSecret: \"sk-test\" },\n });\n });\n\n it(\"keeps public config reads available when the token is required\", async () => {\n process.env.COVEL_HOME = tmpHome;\n process.env.COVEL_DESKTOP_REST_TOKEN = TOKEN;\n const app = buildApp(apiKeys);\n\n expect((await app.request(\"/api/config/info\")).status).toBe(200);\n expect((await app.request(\"/api/config/keys\")).status).toBe(200);\n });\n });\n});\n"}}}}