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,7 +1,8 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { readManagedCredential, writeManagedCredential } from '../credentials/providers.js';
|
||||
import {
|
||||
buildResult,
|
||||
formatMoney,
|
||||
@@ -54,25 +55,6 @@ const readStateValue = (key) => {
|
||||
}
|
||||
};
|
||||
|
||||
const writeStateValue = (key, value) => {
|
||||
if (!existsSync(STATE_DB)) return false;
|
||||
try {
|
||||
const escaped = String(value).replace(/'/g, "''");
|
||||
const escapedKey = String(key).replace(/'/g, "''");
|
||||
execFileSync('sqlite3', [
|
||||
STATE_DB,
|
||||
`INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('${escapedKey}', '${escaped}');`
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const readFileToken = (path) => {
|
||||
try {
|
||||
if (!path || !existsSync(path)) return null;
|
||||
@@ -83,16 +65,6 @@ const readFileToken = (path) => {
|
||||
}
|
||||
};
|
||||
|
||||
const writeFileToken = (path, value) => {
|
||||
try {
|
||||
if (!path) return false;
|
||||
writeFileSync(path, `${value}\n`, { encoding: 'utf8', mode: 0o600 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadAuthState = () => {
|
||||
const envAccessToken = process.env.CURSOR_TOKEN || process.env.CURSOR_ACCESS_TOKEN || null;
|
||||
const envRefreshToken = process.env.CURSOR_REFRESH_TOKEN || null;
|
||||
@@ -113,15 +85,15 @@ const loadAuthState = () => {
|
||||
return {
|
||||
accessToken: fileAccessToken,
|
||||
refreshToken: fileRefreshToken,
|
||||
source: 'file',
|
||||
accessTokenPath
|
||||
source: 'file'
|
||||
};
|
||||
}
|
||||
|
||||
const managed = readManagedCredential(providerId);
|
||||
return {
|
||||
accessToken: readStateValue('cursorAuth/accessToken'),
|
||||
refreshToken: readStateValue('cursorAuth/refreshToken'),
|
||||
source: 'sqlite'
|
||||
accessToken: managed?.accessToken || null,
|
||||
refreshToken: managed?.refreshToken || null,
|
||||
source: 'managed'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -133,8 +105,18 @@ const tokenNeedsRefresh = (token) => {
|
||||
};
|
||||
|
||||
const persistAccessToken = (auth, accessToken) => {
|
||||
if (auth.source === 'sqlite') writeStateValue('cursorAuth/accessToken', accessToken);
|
||||
if (auth.source === 'file') writeFileToken(auth.accessTokenPath, accessToken);
|
||||
if (auth.source === 'managed') writeManagedCredential(providerId, { accessToken, refreshToken: auth.refreshToken || '' });
|
||||
};
|
||||
|
||||
export const importCursorCredential = async () => {
|
||||
const credential = {
|
||||
accessToken: readStateValue('cursorAuth/accessToken') || '',
|
||||
refreshToken: readStateValue('cursorAuth/refreshToken') || '',
|
||||
};
|
||||
if (!credential.accessToken && !credential.refreshToken) throw new Error('Cursor credentials are unavailable');
|
||||
const accessToken = await resolveCredentialAccessToken({ ...credential, source: 'import' });
|
||||
if (!accessToken) throw new Error('Cursor credentials are invalid');
|
||||
return writeManagedCredential(providerId, { ...credential, accessToken });
|
||||
};
|
||||
|
||||
const refreshAccessToken = async (auth) => {
|
||||
@@ -165,13 +147,20 @@ const refreshAccessToken = async (auth) => {
|
||||
return body.access_token;
|
||||
};
|
||||
|
||||
const resolveAccessToken = async () => {
|
||||
const auth = loadAuthState();
|
||||
const resolveCredentialAccessToken = async (auth) => {
|
||||
if (!auth.accessToken && !auth.refreshToken) return null;
|
||||
if (!tokenNeedsRefresh(auth.accessToken)) return auth.accessToken;
|
||||
return refreshAccessToken(auth);
|
||||
};
|
||||
|
||||
const resolveAccessToken = async () => resolveCredentialAccessToken(loadAuthState());
|
||||
|
||||
export const validateCursorCredential = async (credential) => {
|
||||
const accessToken = await resolveCredentialAccessToken({ ...credential, source: 'validation' });
|
||||
if (!accessToken) throw new Error('Cursor credentials are invalid');
|
||||
await connectPost(USAGE_URL, accessToken);
|
||||
};
|
||||
|
||||
const connectPost = async (url, accessToken) => {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user