refactor(quota): secure managed provider credentials (#2160)
- add shared owner-only credential storage for OpenCode Go, Ollama Cloud, and Cursor - validate credentials before atomic writes using 0700 directories and 0600 files - replace provider-specific credential routes with an allowlisted lifecycle API - stop automatically reading Ollama's legacy cookie file - stop reading or modifying Cursor's database during regular quota requests - add explicit one-time Cursor credential import without mutating Cursor storage - persist refreshed Cursor credentials only in OpenChamber-managed storage - add Ollama Cloud and Cursor credential controls to provider settings - preserve OpenCode Go tracking through the shared credential flow - add VS Code credential management and Cursor quota parity - reject authentication redirects, enforce request timeouts, and fail on unparseable usage pages - mask stored secrets in API responses and extend quota security coverage - update quota provider documentation
This commit is contained in:
committed by
GitHub
parent
3b92d97795
commit
b09614fd68
@@ -1,53 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
type OpenCodeGoCredential = { workspaceId: string; authCookie: string };
|
||||
|
||||
const targetPath = () => path.join(process.env.OPENCHAMBER_DATA_DIR ? path.resolve(process.env.OPENCHAMBER_DATA_DIR) : path.join(os.homedir(), '.config', 'openchamber'), 'quota', 'opencode-go.json');
|
||||
|
||||
export const normalizeOpenCodeGoCredential = (value: unknown): OpenCodeGoCredential | null => {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const data = value as Record<string, unknown>;
|
||||
const workspaceId = typeof data.workspaceId === 'string' ? data.workspaceId.trim() : '';
|
||||
let authCookie = typeof data.authCookie === 'string' ? data.authCookie.trim() : '';
|
||||
if (authCookie.startsWith('auth=')) authCookie = authCookie.slice(5).trim();
|
||||
return workspaceId && authCookie && !/[\r\n]/.test(workspaceId + authCookie) ? { workspaceId, authCookie } : null;
|
||||
};
|
||||
|
||||
export const readOpenCodeGoCredential = (): OpenCodeGoCredential | null => {
|
||||
try {
|
||||
return normalizeOpenCodeGoCredential(JSON.parse(fs.readFileSync(targetPath(), 'utf8')));
|
||||
} catch (error) {
|
||||
if ((error as { code?: string }).code !== 'ENOENT') {
|
||||
console.warn('Failed to read OpenCode Go credentials');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getOpenCodeGoCredentialStatus = () => {
|
||||
const value = readOpenCodeGoCredential();
|
||||
return value ? { configured: true, workspaceId: value.workspaceId, authCookieMasked: '••••••••' } : { configured: false };
|
||||
};
|
||||
|
||||
export const writeOpenCodeGoCredential = (value: OpenCodeGoCredential) => {
|
||||
const target = targetPath();
|
||||
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
||||
try {
|
||||
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
||||
fs.chmodSync(temporary, 0o600);
|
||||
fs.renameSync(temporary, target);
|
||||
fs.chmodSync(target, 0o600);
|
||||
} finally {
|
||||
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
||||
}
|
||||
return getOpenCodeGoCredentialStatus();
|
||||
};
|
||||
|
||||
export const deleteOpenCodeGoCredential = () => { try { fs.unlinkSync(targetPath()); } catch (error) { if ((error as { code?: string }).code !== 'ENOENT') throw error; } };
|
||||
|
||||
const toWindow = (usedPercent: number, resetInSec: number) => ({
|
||||
usedPercent: Math.min(100, Math.max(0, usedPercent)),
|
||||
remainingPercent: 100 - Math.min(100, Math.max(0, usedPercent)),
|
||||
@@ -59,8 +11,8 @@ const toWindow = (usedPercent: number, resetInSec: number) => ({
|
||||
});
|
||||
|
||||
export const fetchOpenCodeGoUsage = async (credential: OpenCodeGoCredential) => {
|
||||
const response = await fetch(`https://opencode.ai/workspace/${encodeURIComponent(credential.workspaceId)}/go`, { headers: { Accept: 'text/html', Cookie: `auth=${credential.authCookie}` }, signal: AbortSignal.timeout(15_000) });
|
||||
if (response.status === 401 || response.status === 403 || (response.redirected && /\/auth(?:\/|$|\?)/.test(new URL(response.url).pathname))) throw new Error('OpenCode Go authentication failed');
|
||||
const response = await fetch(`https://opencode.ai/workspace/${encodeURIComponent(credential.workspaceId)}/go`, { headers: { Accept: 'text/html', Cookie: `auth=${credential.authCookie}` }, redirect: 'manual', signal: AbortSignal.timeout(15_000) });
|
||||
if (response.status === 401 || response.status === 403 || (response.status >= 300 && response.status < 400)) throw new Error('OpenCode Go authentication failed');
|
||||
if (!response.ok) throw new Error(`OpenCode Go dashboard returned HTTP ${response.status}`);
|
||||
const html = (await response.text()).replaceAll('"', '"').replaceAll('"', '"').replaceAll('\\u0022', '"').replaceAll('\\"', '"');
|
||||
const windows: Record<string, ReturnType<typeof toWindow>> = {};
|
||||
|
||||
Reference in New Issue
Block a user