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,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' });
|
||||
|
||||
Reference in New Issue
Block a user