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:
Bohdan Triapitsyn
2026-07-12 16:21:38 +03:00
committed by GitHub
parent 3b92d97795
commit b09614fd68
20 changed files with 431 additions and 368 deletions
@@ -1,60 +1,11 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { deleteManagedCredential, getManagedCredentialStatus, normalizers, readManagedCredential, writeManagedCredential } from './credentials/providers.js';
const credentialsPath = () => 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 = normalizers['opencode-go'];
export const normalizeOpenCodeGoCredential = (value) => {
const workspaceId = typeof value?.workspaceId === 'string' ? value.workspaceId.trim() : '';
let authCookie = typeof value?.authCookie === 'string' ? value.authCookie.trim() : '';
if (authCookie.startsWith('auth=')) authCookie = authCookie.slice(5).trim();
if (!workspaceId || !authCookie || /[\r\n]/.test(workspaceId) || /[\r\n]/.test(authCookie)) {
return null;
}
return { workspaceId, authCookie };
};
export const readOpenCodeGoCredential = () => readManagedCredential('opencode-go');
export const readOpenCodeGoCredential = () => {
try {
const parsed = JSON.parse(fs.readFileSync(credentialsPath(), 'utf8'));
return normalizeOpenCodeGoCredential(parsed);
} catch (error) {
if (error?.code !== 'ENOENT') console.warn('Failed to read OpenCode Go credentials');
return null;
}
};
export const getOpenCodeGoCredentialStatus = () => getManagedCredentialStatus('opencode-go');
export const getOpenCodeGoCredentialStatus = () => {
const credential = readOpenCodeGoCredential();
return credential ? { configured: true, workspaceId: credential.workspaceId, authCookieMasked: '••••••••' } : { configured: false };
};
export const writeOpenCodeGoCredential = (value) => writeManagedCredential('opencode-go', value);
export const writeOpenCodeGoCredential = (value) => {
const credential = normalizeOpenCodeGoCredential(value);
if (!credential) throw new Error('Workspace ID and auth cookie are required');
const target = credentialsPath();
const directory = path.dirname(target);
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
try {
fs.writeFileSync(temporary, `${JSON.stringify(credential, null, 2)}\n`, { mode: 0o600 });
fs.chmodSync(temporary, 0o600);
fs.renameSync(temporary, target);
fs.chmodSync(target, 0o600);
} finally {
try { fs.unlinkSync(temporary); } catch {}
}
return getOpenCodeGoCredentialStatus();
};
export const deleteOpenCodeGoCredential = () => {
try { fs.unlinkSync(credentialsPath()); } catch (error) {
if (error?.code !== 'ENOENT') throw error;
}
};
export const deleteOpenCodeGoCredential = () => deleteManagedCredential('opencode-go');