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
@@ -18,7 +18,7 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide
| --- | --- | --- | --- |
| `claude` | Claude | `providers/claude.js` | `anthropic`, `claude` |
| `codex` | Codex | `providers/codex.js` | `openai`, `codex`, `chatgpt` |
| `cursor` | Cursor | `providers/cursor.js` | `CURSOR_TOKEN` / `CURSOR_ACCESS_TOKEN`, `CURSOR_REFRESH_TOKEN`, optional token files, or Cursor desktop SQLite DB |
| `cursor` | Cursor | `providers/cursor.js` | Environment/token files, OpenChamber-managed credentials, or explicit one-time Cursor import |
| `google` | Google | `providers/google/index.js` | `google`, `google.oauth`, Antigravity accounts file |
| `github-copilot` | GitHub Copilot | `providers/copilot.js` | `github-copilot`, `copilot` |
| `github-copilot-addon` | GitHub Copilot Add-on | `providers/copilot.js` | `github-copilot`, `copilot` |
@@ -29,7 +29,7 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide
| `zhipuai-coding-plan` | Zhipu AI Coding Plan | `providers/zhipuai-coding-plan.js` | `zhipuai-coding-plan`, `zhipuai`, `zhipu` |
| `minimax-coding-plan` | MiniMax Coding Plan (minimax.io) | `providers/minimax-coding-plan.js` / `providers/minimax-shared.js` | `minimax-coding-plan` |
| `minimax-cn-coding-plan` | MiniMax Coding Plan (minimaxi.com) | `providers/minimax-cn-coding-plan.js` / `providers/minimax-shared.js` | `minimax-cn-coding-plan` |
| `ollama-cloud` | Ollama Cloud | `providers/ollama-cloud.js` | Cookie file at `~/.config/ollama-quota/cookie` (raw session cookie string) |
| `ollama-cloud` | Ollama Cloud | `providers/ollama-cloud.js` | Manual cookie stored under `~/.config/openchamber/quota/` |
| `wafer` | Wafer.ai | `providers/wafer.js` | `wafer`, `wafer-ai`, `wafer_ai`, `wafer.ai` |
| `opencode-go` | OpenCode Go | `providers/opencode-go.js` | Manual workspace ID and auth cookie stored under `~/.config/openchamber/quota/` |
@@ -45,7 +45,7 @@ All providers should return results via shared helpers to preserve API shape:
Provider modules must export `providerId`, `providerName`, `aliases`, `isConfigured(auth?)`, and `fetchQuota()`.
`fetchQuota()` should return a quota result with `usage.windows` keyed by window name (for example `5h`, `7d`, `daily`) and optional provider-specific `usage.models` data.
OpenCode Go credentials are explicitly supplied by the user through Settings. OpenChamber never scans browser cookie stores. The server validates the credential before an atomic `0600` write and never returns the cookie through its API.
OpenCode Go, Ollama Cloud, and Cursor credentials are explicitly managed through Settings. The server validates credentials before atomic `0600` writes and never returns secrets through its API. OpenChamber never scans browser cookie stores or automatically reads Cursor storage; Cursor import is an explicit one-time user action and never modifies Cursor's database.
## Add a new provider (quick steps)
1. Choose module shape based on complexity:
@@ -0,0 +1,43 @@
import { deleteQuotaCredential, readQuotaCredential, writeQuotaCredential } from './store.js';
const clean = (value) => typeof value === 'string' && !/[\r\n]/.test(value) ? value.trim() : '';
export const normalizers = {
'opencode-go': (value) => {
const workspaceId = clean(value?.workspaceId);
let authCookie = clean(value?.authCookie);
if (authCookie.startsWith('auth=')) authCookie = authCookie.slice(5).trim();
return workspaceId && authCookie ? { workspaceId, authCookie } : null;
},
'ollama-cloud': (value) => {
const cookie = clean(value?.cookie);
return cookie ? { cookie } : null;
},
cursor: (value) => {
const accessToken = clean(value?.accessToken);
const refreshToken = clean(value?.refreshToken);
return accessToken || refreshToken ? { accessToken, refreshToken } : null;
},
};
export const readManagedCredential = (providerId) => {
const normalize = normalizers[providerId];
return normalize ? readQuotaCredential(providerId, normalize) : null;
};
export const writeManagedCredential = (providerId, value) => {
const credential = normalizers[providerId]?.(value);
if (!credential) throw new Error('Invalid credential');
writeQuotaCredential(providerId, credential);
return getManagedCredentialStatus(providerId);
};
export const getManagedCredentialStatus = (providerId) => {
const credential = readManagedCredential(providerId);
if (!credential) return { configured: false };
if (providerId === 'opencode-go') return { configured: true, workspaceId: credential.workspaceId, secretMasked: '••••••••' };
if (providerId === 'cursor') return { configured: true, hasRefreshToken: Boolean(credential.refreshToken), secretMasked: '••••••••' };
return { configured: true, secretMasked: '••••••••' };
};
export const deleteManagedCredential = (providerId) => deleteQuotaCredential(providerId);
@@ -0,0 +1,48 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const MANAGED_QUOTA_PROVIDERS = new Set(['opencode-go', 'ollama-cloud', 'cursor']);
const credentialsDirectory = () => path.join(
process.env.OPENCHAMBER_DATA_DIR
? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
: path.join(os.homedir(), '.config', 'openchamber'),
'quota',
);
const credentialPath = (providerId) => {
if (!MANAGED_QUOTA_PROVIDERS.has(providerId)) throw new Error('Unsupported credential provider');
return path.join(credentialsDirectory(), `${providerId}.json`);
};
export const readQuotaCredential = (providerId, normalize) => {
try {
return normalize(JSON.parse(fs.readFileSync(credentialPath(providerId), 'utf8')));
} catch (error) {
if (error?.code !== 'ENOENT') console.warn(`Failed to read ${providerId} quota credentials`);
return null;
}
};
export const writeQuotaCredential = (providerId, credential) => {
const target = credentialPath(providerId);
const directory = path.dirname(target);
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
fs.chmodSync(directory, 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 {}
}
};
export const deleteQuotaCredential = (providerId) => {
try { fs.unlinkSync(credentialPath(providerId)); } catch (error) {
if (error?.code !== 'ENOENT') throw error;
}
};
@@ -0,0 +1,26 @@
import { afterAll, describe, expect, it } from 'bun:test';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { deleteQuotaCredential, readQuotaCredential, writeQuotaCredential } from './store.js';
const previousDataDir = process.env.OPENCHAMBER_DATA_DIR;
const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-quota-store-'));
process.env.OPENCHAMBER_DATA_DIR = temporaryDirectory;
describe('quota credential store', () => {
it('uses owner-only permissions and rejects arbitrary provider paths', () => {
writeQuotaCredential('ollama-cloud', { cookie: 'secret' });
expect(fs.statSync(path.join(temporaryDirectory, 'quota')).mode & 0o777).toBe(0o700);
expect(fs.statSync(path.join(temporaryDirectory, 'quota', 'ollama-cloud.json')).mode & 0o777).toBe(0o600);
expect(readQuotaCredential('ollama-cloud', (value) => value)).toEqual({ cookie: 'secret' });
expect(() => writeQuotaCredential('../escape', {})).toThrow('Unsupported credential provider');
deleteQuotaCredential('ollama-cloud');
});
});
afterAll(() => {
if (previousDataDir === undefined) delete process.env.OPENCHAMBER_DATA_DIR;
else process.env.OPENCHAMBER_DATA_DIR = previousDataDir;
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
});
@@ -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');
@@ -13,7 +13,7 @@ afterEach(() => deleteOpenCodeGoCredential());
describe('OpenCode Go credential store', () => {
it('normalizes, masks, and stores credentials with owner-only permissions', () => {
const status = writeOpenCodeGoCredential({ workspaceId: ' wrk_test ', authCookie: ' auth=secret ' });
expect(status).toEqual({ configured: true, workspaceId: 'wrk_test', authCookieMasked: '••••••••' });
expect(status).toEqual({ configured: true, workspaceId: 'wrk_test', secretMasked: '••••••••' });
expect(readOpenCodeGoCredential()).toEqual({ workspaceId: 'wrk_test', authCookie: 'secret' });
expect(fs.statSync(path.join(temporaryDirectory, 'quota', 'opencode-go.json')).mode & 0o777).toBe(0o600);
});
@@ -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',
@@ -1,26 +1,11 @@
import { homedir } from 'os';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { buildResult, toUsageWindow, toNumber } from '../utils/index.js';
const COOKIE_PATH = join(homedir(), '.config', 'ollama-quota', 'cookie');
import { readManagedCredential } from '../credentials/providers.js';
export const providerId = 'ollama-cloud';
export const providerName = 'Ollama Cloud';
const aliases = ['ollama-cloud', 'ollamacloud'];
const readCookieFile = () => {
try {
if (!existsSync(COOKIE_PATH)) return null;
const content = readFileSync(COOKIE_PATH, 'utf-8');
const trimmed = content.trim();
return trimmed || null;
} catch {
return null;
}
};
const parseOllamaSettingsHtml = (html) => {
export const parseOllamaSettingsHtml = (html) => {
const windows = {};
const sessionMatch = html.match(/Session\s+usage[^0-9]*([0-9.]+)%/i);
if (sessionMatch) {
@@ -54,14 +39,29 @@ const parseOllamaSettingsHtml = (html) => {
};
export const isConfigured = () => {
const cookie = readCookieFile();
return Boolean(cookie);
return Boolean(readManagedCredential(providerId));
};
export const fetchOllamaCloudUsage = async (credential, fetchImpl = fetch) => {
const response = await fetchImpl('https://ollama.com/settings', {
method: 'GET',
headers: { Cookie: credential.cookie, 'User-Agent': 'OpenChamber quota provider' },
redirect: 'manual',
signal: AbortSignal.timeout(15_000),
});
if (response.status === 401 || response.status === 403 || (response.status >= 300 && response.status < 400)) {
throw new Error('Ollama Cloud authentication failed');
}
if (!response.ok) throw new Error(`Ollama Cloud returned HTTP ${response.status}`);
const windows = parseOllamaSettingsHtml(await response.text());
if (Object.keys(windows).length === 0) throw new Error('Ollama Cloud usage data could not be parsed');
return windows;
};
export const fetchQuota = async () => {
const cookie = readCookieFile();
const credential = readManagedCredential(providerId);
if (!cookie) {
if (!credential) {
return buildResult({
providerId,
providerName,
@@ -72,26 +72,7 @@ export const fetchQuota = async () => {
}
try {
const response = await fetch('https://ollama.com/settings', {
method: 'GET',
headers: {
Cookie: cookie,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
}
});
if (!response.ok) {
return buildResult({
providerId,
providerName,
ok: false,
configured: true,
error: `API error: ${response.status}`
});
}
const html = await response.text();
const windows = parseOllamaSettingsHtml(html);
const windows = await fetchOllamaCloudUsage(credential);
return buildResult({
providerId,
@@ -109,4 +90,4 @@ export const fetchQuota = async () => {
error: error instanceof Error ? error.message : 'Request failed'
});
}
};
};
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'bun:test';
import { fetchOllamaCloudUsage } from './ollama-cloud.js';
describe('Ollama Cloud quota provider', () => {
it('rejects redirects without forwarding credentials', async () => {
await expect(fetchOllamaCloudUsage({ cookie: 'session=secret' }, async () => new Response('', { status: 302 }))).rejects.toThrow('authentication failed');
});
it('rejects successful pages without usage data', async () => {
await expect(fetchOllamaCloudUsage({ cookie: 'session=secret' }, async () => new Response('<html></html>'))).rejects.toThrow('could not be parsed');
});
});
@@ -44,9 +44,10 @@ export const fetchOpenCodeGoUsage = async (credential, fetchImpl = fetch) => {
Cookie: `auth=${credential.authCookie}`,
'User-Agent': 'OpenChamber quota provider',
},
redirect: 'manual',
signal: AbortSignal.timeout(15_000),
});
if (response.status === 401 || response.status === 403 || (response.redirected && /\/auth(?:\/|$|\?)/.test(new URL(response.url).pathname))) {
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}`);
+59 -29
View File
@@ -1,65 +1,95 @@
import express from 'express';
import {
deleteOpenCodeGoCredential,
getOpenCodeGoCredentialStatus,
normalizeOpenCodeGoCredential,
readOpenCodeGoCredential,
writeOpenCodeGoCredential,
} from './opencode-go-credentials.js';
import { deleteManagedCredential, getManagedCredentialStatus, normalizers, readManagedCredential, writeManagedCredential } from './credentials/providers.js';
import { fetchOpenCodeGoUsage } from './providers/opencode-go.js';
import { fetchOllamaCloudUsage } from './providers/ollama-cloud.js';
import { importCursorCredential, validateCursorCredential } from './providers/cursor.js';
const validators = {
'opencode-go': fetchOpenCodeGoUsage,
'ollama-cloud': fetchOllamaCloudUsage,
cursor: validateCursorCredential,
};
const getProvider = (req, res) => {
const providerId = req.params.providerId;
if (!normalizers[providerId]) {
res.status(404).json({ code: 'UNSUPPORTED_PROVIDER', error: 'Unsupported credential provider' });
return null;
}
return providerId;
};
const credentialError = (res, error) => res.status(400).json({
code: 'INVALID_CREDENTIAL',
error: error instanceof Error ? error.message : 'Credential validation failed',
});
export function registerQuotaRoutes(app, { getQuotaProviders }) {
app.get('/api/quota/providers', async (_req, res) => {
try {
const { listConfiguredQuotaProviders } = await getQuotaProviders();
const providers = listConfiguredQuotaProviders();
res.json({ providers });
res.json({ providers: listConfiguredQuotaProviders() });
} catch (error) {
console.error('Failed to list quota providers:', error);
res.status(500).json({ error: error.message || 'Failed to list quota providers' });
}
});
app.get('/api/quota/credentials/opencode-go', (_req, res) => {
res.json(getOpenCodeGoCredentialStatus());
app.get('/api/quota/credentials/:providerId', (req, res) => {
const providerId = getProvider(req, res);
if (providerId) res.json(getManagedCredentialStatus(providerId));
});
app.put('/api/quota/credentials/opencode-go', express.json({ limit: '16kb' }), async (req, res) => {
app.put('/api/quota/credentials/:providerId', express.json({ limit: '16kb' }), async (req, res) => {
const providerId = getProvider(req, res);
if (!providerId) return;
const credential = normalizers[providerId](req.body);
if (!credential) return credentialError(res, new Error('Invalid credential'));
try {
const credential = normalizeOpenCodeGoCredential(req.body);
if (!credential) return res.status(400).json({ error: 'Workspace ID and auth cookie are required' });
await fetchOpenCodeGoUsage(credential);
res.json(writeOpenCodeGoCredential(credential));
await validators[providerId](credential);
res.json(writeManagedCredential(providerId, credential));
} catch (error) {
res.status(400).json({ error: error instanceof Error ? error.message : 'Credential validation failed' });
credentialError(res, error);
}
});
app.post('/api/quota/credentials/opencode-go/validate', async (_req, res) => {
app.post('/api/quota/credentials/:providerId/validate', async (req, res) => {
const providerId = getProvider(req, res);
if (!providerId) return;
const credential = readManagedCredential(providerId);
if (!credential) return res.status(404).json({ code: 'NOT_CONFIGURED', error: 'Not configured' });
try {
const credential = readOpenCodeGoCredential();
if (!credential) return res.status(404).json({ error: 'Not configured' });
await fetchOpenCodeGoUsage(credential);
await validators[providerId](credential);
res.json({ valid: true });
} catch (error) {
res.status(400).json({ valid: false, error: error instanceof Error ? error.message : 'Credential validation failed' });
credentialError(res, error);
}
});
app.delete('/api/quota/credentials/opencode-go', (_req, res) => {
deleteOpenCodeGoCredential();
app.post('/api/quota/credentials/:providerId/import', async (req, res) => {
const providerId = getProvider(req, res);
if (!providerId) return;
if (providerId !== 'cursor') return res.status(404).json({ code: 'IMPORT_UNAVAILABLE', error: 'Import unavailable' });
try {
res.json(await importCursorCredential());
} catch (error) {
credentialError(res, error);
}
});
app.delete('/api/quota/credentials/:providerId', (req, res) => {
const providerId = getProvider(req, res);
if (!providerId) return;
deleteManagedCredential(providerId);
res.json({ configured: false });
});
app.get('/api/quota/:providerId', async (req, res) => {
try {
const { providerId } = req.params;
if (!providerId) {
return res.status(400).json({ error: 'Provider ID is required' });
}
if (!providerId) return res.status(400).json({ error: 'Provider ID is required' });
const { fetchQuotaForProvider } = await getQuotaProviders();
const result = await fetchQuotaForProvider(providerId);
res.json(result);
res.json(await fetchQuotaForProvider(providerId));
} catch (error) {
console.error('Failed to fetch quota:', error);
res.status(500).json({ error: error.message || 'Failed to fetch quota' });
+1 -1
View File
@@ -31,7 +31,7 @@ describe('OpenCode Go credential routes', () => {
body: JSON.stringify({ workspaceId: 'wrk_test', authCookie: 'auth=secret' }),
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ configured: true, workspaceId: 'wrk_test', authCookieMasked: '••••••••' });
expect(await response.json()).toEqual({ configured: true, workspaceId: 'wrk_test', secretMasked: '••••••••' });
} finally {
globalThis.fetch = originalFetch;
server.close();