github-actions[bot]
Deploy from GitHub: 7495fde758f0be655f95e6331fec2898267f790c
f6266b9
/**
* Dashboard API route handlers.
* All routes are under /dashboard/api/*.
*/
import { config, log } from '../config.js';
import {
getAccountList, getAccountCount, addAccountByKey, addAccountByToken,
removeAccount, setAccountStatus, resetAccountErrors, updateAccountLabel,
isAuthenticated, probeAccount, ensureLsForAccount,
refreshCredits, refreshAllCredits,
setAccountBlockedModels, setAccountTokens, setAccountTier,
} from '../auth.js';
import { restartLsForProxy } from '../langserver.js';
import { getLsStatus, stopLanguageServer, startLanguageServer, isLanguageServerRunning } from '../langserver.js';
import { getStats, resetStats, recordRequest } from './stats.js';
import { cacheStats, cacheClear } from '../cache.js';
import { getExperimental, setExperimental, getIdentityPrompts, setIdentityPrompts, resetIdentityPrompt, DEFAULT_IDENTITY_PROMPTS } from '../runtime-config.js';
import { poolStats as convPoolStats, poolClear as convPoolClear } from '../conversation-pool.js';
import { getLogs, subscribeToLogs, unsubscribeFromLogs } from './logger.js';
import { getProxyConfig, setGlobalProxy, setAccountProxy, removeProxy, getEffectiveProxy } from './proxy-config.js';
import { MODELS, MODEL_TIER_ACCESS as _TIER_TABLE, getTierModels as _getTierModels } from '../models.js';
import { windsurfLogin, refreshFirebaseToken, reRegisterWithCodeium } from './windsurf-login.js';
import { getModelAccessConfig, setModelAccessMode, setModelAccessList, addModelToList, removeModelFromList } from './model-access.js';
import { checkMessageRateLimit } from '../windsurf-api.js';
function json(res, status, body) {
const data = JSON.stringify(body);
res.writeHead(status, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-Dashboard-Password',
});
res.end(data);
}
function checkAuth(req) {
// Header is preferred (set by fetch). EventSource can't set custom headers,
// so /logs/stream etc. also accept ?pwd=... as fallback.
let pw = req.headers['x-dashboard-password'] || '';
if (!pw) {
try {
const qs = new URL(req.url, 'http://x').searchParams;
pw = qs.get('pwd') || '';
} catch {}
}
if (config.dashboardPassword) return pw === config.dashboardPassword;
if (config.apiKey) return pw === config.apiKey;
return true; // No password and no API key = open access
}
/**
* Handle all /dashboard/api/* requests.
*/
export async function handleDashboardApi(method, subpath, body, req, res) {
if (method === 'OPTIONS') return json(res, 204, '');
// Auth check (except for auth verification endpoint)
if (subpath !== '/auth' && !checkAuth(req)) {
return json(res, 401, { error: 'Unauthorized. Set X-Dashboard-Password header.' });
}
// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/auth') {
const needsAuth = !!(config.dashboardPassword || config.apiKey);
if (!needsAuth) return json(res, 200, { required: false });
return json(res, 200, { required: true, valid: checkAuth(req) });
}
// โ”€โ”€โ”€ Overview โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/overview' && method === 'GET') {
const stats = getStats();
return json(res, 200, {
uptime: process.uptime(),
startedAt: stats.startedAt,
accounts: getAccountCount(),
authenticated: isAuthenticated(),
langServer: getLsStatus(),
totalRequests: stats.totalRequests,
successCount: stats.successCount,
errorCount: stats.errorCount,
successRate: stats.totalRequests > 0
? ((stats.successCount / stats.totalRequests) * 100).toFixed(1)
: '0.0',
cache: cacheStats(),
});
}
// โ”€โ”€โ”€ Experimental features โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/experimental' && method === 'GET') {
return json(res, 200, { flags: getExperimental(), conversationPool: convPoolStats() });
}
if (subpath === '/experimental' && method === 'PUT') {
const flags = setExperimental(body || {});
// Dropping the toggle should also drop any live entries so nothing
// resumes against a disabled feature on the next request.
if (!flags.cascadeConversationReuse) convPoolClear();
return json(res, 200, { success: true, flags });
}
if (subpath === '/experimental/conversation-pool' && method === 'DELETE') {
const n = convPoolClear();
return json(res, 200, { success: true, cleared: n });
}
// โ”€โ”€โ”€ Identity prompts (per-provider editable templates) โ”€
if (subpath === '/identity-prompts' && method === 'GET') {
return json(res, 200, {
prompts: getIdentityPrompts(),
defaults: DEFAULT_IDENTITY_PROMPTS,
});
}
if (subpath === '/identity-prompts' && method === 'PUT') {
const prompts = setIdentityPrompts(body || {});
return json(res, 200, { success: true, prompts });
}
if (subpath.match(/^\/identity-prompts\/[^/]+$/) && method === 'DELETE') {
const provider = subpath.split('/').pop();
const prompts = resetIdentityPrompt(provider);
return json(res, 200, { success: true, prompts });
}
// โ”€โ”€โ”€ Proxy test โ€” try an HTTP CONNECT through the given proxy โ”€โ”€
if (subpath === '/test-proxy' && method === 'POST') {
const { host, port, username, password, type = 'http' } = body || {};
if (!host || !port) return json(res, 400, { ok: false, error: '็ผบๅฐ‘ host ๆˆ– port' });
const startTime = Date.now();
try {
const result = await testProxy({ host, port: Number(port), username, password, type });
return json(res, 200, { ok: true, ...result, latencyMs: Date.now() - startTime });
} catch (err) {
return json(res, 200, { ok: false, error: err.message, latencyMs: Date.now() - startTime });
}
}
// โ”€โ”€โ”€ Self-update: pull latest code + restart PM2 โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/self-update/check' && method === 'GET') {
try {
const info = await gitStatus();
return json(res, 200, { ok: true, ...info });
} catch (err) {
return json(res, 200, { ok: false, error: err.message });
}
}
if (subpath === '/self-update' && method === 'POST') {
try {
const before = await gitStatus();
// Guard: working tree must be clean (ignoring untracked files like
// accounts.json, stats.json, runtime-config.json which live in the
// repo root but aren't checked in). If the tracked files were edited
// manually (or pushed via SFTP without a corresponding commit),
// `git pull --ff-only` would refuse โ€” surface a friendly error
// instead of a raw git message.
const dirty = (await runShell('git status --porcelain -uno')).trim();
if (dirty) {
const allowForce = !!(body && body.forceReset);
if (!allowForce) {
return json(res, 200, {
ok: false,
dirty: true,
error: 'ๅทฅไฝœๅŒบๆœ‰ๆœชๆไบค็š„ไฟฎๆ”น๏ผˆSFTP ้ƒจ็ฝฒๆˆ–ๆ‰‹ๅŠจๆ”น่ฟ‡ไปฃ็ ๏ผ‰ใ€‚็กฎๅฎš่ฆ่ฆ†็›–ๆœฌๅœฐไฟฎๆ”น็”จ่ฟœ็จ‹ๆœ€ๆ–ฐ็‰ˆๆœฌๅ—๏ผŸ',
dirtyFiles: dirty.split('\n').slice(0, 20),
});
}
await runShell(`git fetch origin ${before.branch || 'master'}`);
await runShell(`git reset --hard origin/${before.branch || 'master'}`);
}
const pullCmd = `git pull origin ${before.branch || 'master'} --ff-only 2>&1`;
const pull = dirty ? 'hard-reset applied' : await runShell(pullCmd);
const after = await gitStatus();
const changed = before.commit !== after.commit;
// Schedule process exit so PM2 auto-restarts us. This is far simpler
// and port/env-agnostic compared to spawning update.sh (which hardcodes
// PORT=3003 default). Requires PM2 autorestart: true (the default).
if (changed) {
setTimeout(() => {
log.info('self-update: exiting for PM2 auto-restart');
process.exit(0);
}, 800);
}
return json(res, 200, {
ok: true,
changed,
before: before.commit,
after: after.commit,
pullOutput: pull.trim(),
restarting: changed,
});
} catch (err) {
return json(res, 200, { ok: false, error: err.message });
}
}
// โ”€โ”€โ”€ Cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/cache' && method === 'GET') {
return json(res, 200, cacheStats());
}
if (subpath === '/cache' && method === 'DELETE') {
cacheClear();
return json(res, 200, { success: true });
}
// โ”€โ”€โ”€ Accounts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/accounts' && method === 'GET') {
return json(res, 200, { accounts: getAccountList() });
}
if (subpath === '/accounts' && method === 'POST') {
try {
let account;
if (body.api_key) {
account = addAccountByKey(body.api_key, body.label);
} else if (body.token) {
account = await addAccountByToken(body.token, body.label);
} else {
return json(res, 400, { error: 'Provide api_key or token' });
}
// Fire-and-forget probe so the UI gets tier info shortly after add
probeAccount(account.id).catch(e => log.warn(`Auto-probe failed: ${e.message}`));
return json(res, 200, {
success: true,
account: { id: account.id, email: account.email, method: account.method, status: account.status },
...getAccountCount(),
});
} catch (err) {
return json(res, 400, { error: err.message });
}
}
// POST /accounts/probe-all โ€” probe every active account
if (subpath === '/accounts/probe-all' && method === 'POST') {
const list = getAccountList().filter(a => a.status === 'active');
const results = [];
for (const a of list) {
try {
const r = await probeAccount(a.id);
results.push({ id: a.id, email: a.email, tier: r?.tier || 'unknown' });
} catch (err) {
results.push({ id: a.id, email: a.email, error: err.message });
}
}
return json(res, 200, { success: true, results });
}
// POST /accounts/:id/probe โ€” manually trigger capability probe
const accountProbe = subpath.match(/^\/accounts\/([^/]+)\/probe$/);
if (accountProbe && method === 'POST') {
try {
const result = await probeAccount(accountProbe[1]);
if (!result) return json(res, 404, { error: 'Account not found' });
return json(res, 200, { success: true, ...result });
} catch (err) {
return json(res, 500, { error: err.message });
}
}
// POST /accounts/refresh-credits โ€” refresh every active account's balance
if (subpath === '/accounts/refresh-credits' && method === 'POST') {
const results = await refreshAllCredits();
return json(res, 200, { success: true, results });
}
// POST /accounts/:id/refresh-credits โ€” single-account refresh
const creditRefresh = subpath.match(/^\/accounts\/([^/]+)\/refresh-credits$/);
if (creditRefresh && method === 'POST') {
const r = await refreshCredits(creditRefresh[1]);
return json(res, r.ok ? 200 : 400, r);
}
// PATCH /accounts/:id
const accountPatch = subpath.match(/^\/accounts\/([^/]+)$/);
if (accountPatch && method === 'PATCH') {
const id = accountPatch[1];
if (body.status) setAccountStatus(id, body.status);
if (body.label) updateAccountLabel(id, body.label);
if (body.resetErrors) resetAccountErrors(id);
if (Array.isArray(body.blockedModels)) setAccountBlockedModels(id, body.blockedModels);
if (body.tier) setAccountTier(id, body.tier);
return json(res, 200, { success: true });
}
// GET /tier-access โ€” hardcoded FREE/PRO model entitlement tables.
// The dashboard uses this to render the full per-account model grid
// (every row in the tier's list is shown, blocked models are dimmed).
if (subpath === '/tier-access' && method === 'GET') {
return json(res, 200, {
free: _TIER_TABLE.free,
pro: _TIER_TABLE.pro,
unknown: _TIER_TABLE.unknown,
expired: _TIER_TABLE.expired,
allModels: Object.keys(MODELS),
});
}
// DELETE /accounts/:id
const accountDel = subpath.match(/^\/accounts\/([^/]+)$/);
if (accountDel && method === 'DELETE') {
const ok = removeAccount(accountDel[1]);
return json(res, ok ? 200 : 404, { success: ok });
}
// โ”€โ”€โ”€ Stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/stats' && method === 'GET') {
return json(res, 200, getStats());
}
if (subpath === '/stats' && method === 'DELETE') {
resetStats();
return json(res, 200, { success: true });
}
// โ”€โ”€โ”€ Logs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/logs' && method === 'GET') {
const url = new URL(req.url, 'http://localhost');
const since = parseInt(url.searchParams.get('since') || '0', 10);
const level = url.searchParams.get('level') || null;
return json(res, 200, { logs: getLogs(since, level) });
}
if (subpath === '/logs/stream' && method === 'GET') {
req.socket.setKeepAlive(true);
req.setTimeout(0);
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'X-Accel-Buffering': 'no',
});
res.write('retry: 3000\n\n');
// Send existing logs first
const existing = getLogs();
for (const entry of existing.slice(-50)) {
res.write(`data: ${JSON.stringify(entry)}\n\n`);
}
const heartbeat = setInterval(() => {
if (!res.writableEnded) res.write(': heartbeat\n\n');
}, 15000);
const cb = (entry) => {
if (!res.writableEnded) res.write(`data: ${JSON.stringify(entry)}\n\n`);
};
subscribeToLogs(cb);
req.on('close', () => {
clearInterval(heartbeat);
unsubscribeFromLogs(cb);
});
return;
}
// โ”€โ”€โ”€ Proxy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/proxy' && method === 'GET') {
return json(res, 200, getProxyConfig());
}
if (subpath === '/proxy/global' && method === 'PUT') {
setGlobalProxy(body);
return json(res, 200, { success: true, config: getProxyConfig() });
}
if (subpath === '/proxy/global' && method === 'DELETE') {
removeProxy('global');
return json(res, 200, { success: true });
}
const proxyAccount = subpath.match(/^\/proxy\/accounts\/([^/]+)$/);
if (proxyAccount && method === 'PUT') {
setAccountProxy(proxyAccount[1], body);
// Spawn (or adopt) the LS instance for this proxy so chat routes immediately
ensureLsForAccount(proxyAccount[1]).catch(e => log.warn(`LS ensure failed: ${e.message}`));
return json(res, 200, { success: true });
}
if (proxyAccount && method === 'DELETE') {
removeProxy('account', proxyAccount[1]);
return json(res, 200, { success: true });
}
// โ”€โ”€โ”€ Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/config' && method === 'GET') {
return json(res, 200, {
port: config.port,
defaultModel: config.defaultModel,
maxTokens: config.maxTokens,
logLevel: config.logLevel,
lsBinaryPath: config.lsBinaryPath,
lsPort: config.lsPort,
codeiumApiUrl: config.codeiumApiUrl,
hasApiKey: !!config.apiKey,
hasDashboardPassword: !!config.dashboardPassword,
});
}
// โ”€โ”€โ”€ Language Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/langserver/restart' && method === 'POST') {
if (!body.confirm) {
return json(res, 400, { error: 'Send { confirm: true } to restart language server' });
}
stopLanguageServer();
setTimeout(async () => {
await startLanguageServer({
binaryPath: config.lsBinaryPath,
port: config.lsPort,
apiServerUrl: config.codeiumApiUrl,
});
}, 2000);
return json(res, 200, { success: true, message: 'Restarting language server...' });
}
// โ”€โ”€โ”€ Models list โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/models' && method === 'GET') {
const models = Object.entries(MODELS).map(([id, info]) => ({
id, name: info.name, provider: info.provider,
}));
return json(res, 200, { models });
}
// โ”€โ”€โ”€ Model Access Control โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/model-access' && method === 'GET') {
return json(res, 200, getModelAccessConfig());
}
if (subpath === '/model-access' && method === 'PUT') {
if (body.mode) setModelAccessMode(body.mode);
if (body.list) setModelAccessList(body.list);
return json(res, 200, { success: true, config: getModelAccessConfig() });
}
if (subpath === '/model-access/add' && method === 'POST') {
if (!body.model) return json(res, 400, { error: 'model is required' });
addModelToList(body.model);
return json(res, 200, { success: true, config: getModelAccessConfig() });
}
if (subpath === '/model-access/remove' && method === 'POST') {
if (!body.model) return json(res, 400, { error: 'model is required' });
removeModelFromList(body.model);
return json(res, 200, { success: true, config: getModelAccessConfig() });
}
// โ”€โ”€โ”€ Windsurf Login โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (subpath === '/windsurf-login' && method === 'POST') {
try {
const { email, password, proxy: loginProxy, autoAdd } = body;
if (!email || !password) return json(res, 400, { error: 'email ๅ’Œ password ็‚บๅฟ…ๅกซ' });
// Use provided proxy, or global proxy
const proxy = loginProxy?.host ? loginProxy : getProxyConfig().global;
const result = await windsurfLogin(email, password, proxy);
// Auto-add to account pool if requested
let account = null;
if (autoAdd !== false) {
account = addAccountByKey(result.apiKey, result.name || email);
// Persist refresh token via the setter so it survives restart and
// the background Firebase-renewal loop can find it.
if (result.refreshToken) {
setAccountTokens(account.id, { refreshToken: result.refreshToken, idToken: result.idToken });
}
// Persist the per-account proxy we used for login so chat requests
// also egress through the same IP, then warm up a matching LS.
if (loginProxy?.host) setAccountProxy(account.id, loginProxy);
ensureLsForAccount(account.id)
.then(() => probeAccount(account.id))
.catch(e => log.warn(`Auto-probe failed: ${e.message}`));
}
return json(res, 200, {
success: true,
apiKey: result.apiKey,
name: result.name,
email: result.email,
apiServerUrl: result.apiServerUrl,
account: account ? { id: account.id, email: account.email, status: account.status } : null,
});
} catch (err) {
return json(res, 400, { error: err.message, isAuthFail: !!err.isAuthFail, firebaseCode: err.firebaseCode });
}
}
// โ”€โ”€โ”€ OAuth login (Google / GitHub via Firebase) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// POST /oauth-login โ€” accepts Firebase idToken from client-side OAuth
if (subpath === '/oauth-login' && method === 'POST') {
try {
const { idToken, refreshToken, email, provider, autoAdd } = body;
if (!idToken) return json(res, 400, { error: '็ผบๅฐ‘ idToken' });
const proxy = getProxyConfig().global;
const { apiKey, name } = await reRegisterWithCodeium(idToken, proxy);
let account = null;
if (autoAdd !== false) {
account = addAccountByKey(apiKey, name || email || provider || 'OAuth');
if (refreshToken) {
setAccountTokens(account.id, { refreshToken, idToken });
}
ensureLsForAccount(account.id)
.then(() => probeAccount(account.id))
.catch(e => log.warn(`OAuth auto-probe failed: ${e.message}`));
}
return json(res, 200, {
success: true,
apiKey,
name,
email: email || '',
account: account ? { id: account.id, email: account.email, status: account.status } : null,
});
} catch (err) {
return json(res, 400, { error: err.message });
}
}
// โ”€โ”€โ”€ Rate Limit Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// POST /accounts/:id/rate-limit โ€” check capacity for a single account
const rateLimitCheck = subpath.match(/^\/accounts\/([^/]+)\/rate-limit$/);
if (rateLimitCheck && method === 'POST') {
const list = getAccountList();
const acct = list.find(a => a.id === rateLimitCheck[1]);
if (!acct) return json(res, 404, { error: 'Account not found' });
try {
const proxy = getEffectiveProxy(acct.id) || null;
const result = await checkMessageRateLimit(acct.apiKey, proxy);
return json(res, 200, { success: true, account: acct.email, ...result });
} catch (err) {
return json(res, 500, { error: err.message });
}
}
// โ”€โ”€โ”€ Firebase Token Refresh โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// POST /accounts/:id/refresh-token โ€” manually refresh Firebase token
const tokenRefresh = subpath.match(/^\/accounts\/([^/]+)\/refresh-token$/);
if (tokenRefresh && method === 'POST') {
const list = getAccountList();
const acct = list.find(a => a.id === tokenRefresh[1]);
if (!acct) return json(res, 404, { error: 'Account not found' });
if (!acct.refreshToken) return json(res, 400, { error: 'Account has no refresh token' });
try {
const proxy = getEffectiveProxy(acct.id) || null;
const { idToken, refreshToken: newRefresh } = await refreshFirebaseToken(acct.refreshToken, proxy);
const { apiKey } = await reRegisterWithCodeium(idToken, proxy);
const keyChanged = apiKey && apiKey !== acct.apiKey;
// Persist the fresh credentials back onto the account. Without this, the
// in-memory apiKey stays on the now-stale value until the next server
// restart โ€” every subsequent request from this account will fail auth.
setAccountTokens(acct.id, { apiKey: apiKey || acct.apiKey, refreshToken: newRefresh || acct.refreshToken, idToken });
return json(res, 200, { success: true, keyChanged, email: acct.email });
} catch (err) {
return json(res, 400, { error: err.message });
}
}
json(res, 404, { error: `Dashboard API: ${method} ${subpath} not found` });
}
// โ”€โ”€โ”€ Proxy connectivity test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// HTTP CONNECT tunnel to api.ipify.org:443 โ†’ GET / โ†’ the returned IP is the
// proxy's egress IP. Confirms the proxy works AND that auth is accepted.
// โ”€โ”€โ”€ Self-update helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function runShell(cmd, opts = {}) {
return new Promise((resolve, reject) => {
import('node:child_process').then(({ exec }) => {
exec(cmd, { timeout: 30_000, maxBuffer: 1024 * 1024, ...opts }, (err, stdout, stderr) => {
if (err) return reject(new Error((stderr || err.message).toString().slice(0, 500)));
resolve(stdout.toString());
});
}).catch(reject);
});
}
async function gitStatus() {
const commit = (await runShell('git rev-parse HEAD')).trim();
const branch = (await runShell('git rev-parse --abbrev-ref HEAD')).trim();
let remote = '';
try {
await runShell('git fetch --quiet origin');
remote = (await runShell(`git rev-parse origin/${branch}`)).trim();
} catch {}
const localMsg = (await runShell('git log -1 --pretty=format:%s')).trim();
const behind = remote && remote !== commit;
const remoteMsg = behind ? (await runShell(`git log -1 --pretty=format:%s ${remote}`).catch(() => '')).trim() : '';
return {
commit: commit.slice(0, 7),
commitFull: commit,
branch,
localMessage: localMsg,
remoteCommit: remote ? remote.slice(0, 7) : '',
remoteMessage: remoteMsg,
behind,
};
}
async function testProxy({ host, port, username, password, type }) {
const http = await import('node:http');
const tls = await import('node:tls');
return new Promise((resolve, reject) => {
const targetHost = 'api.ipify.org';
const targetPort = 443;
const authHeader = username
? { 'Proxy-Authorization': 'Basic ' + Buffer.from(`${username}:${password || ''}`).toString('base64') }
: {};
const req = http.request({
host,
port,
method: 'CONNECT',
path: `${targetHost}:${targetPort}`,
headers: { Host: `${targetHost}:${targetPort}`, ...authHeader },
timeout: 10000,
});
req.on('connect', (res, socket) => {
if (res.statusCode !== 200) {
socket.destroy();
return reject(new Error(`ไปฃ็†่ฟ”ๅ›ž HTTP ${res.statusCode}`));
}
// Do a quick TLS handshake + GET to verify the tunnel actually works
const tlsSock = tls.connect({ socket, servername: targetHost, rejectUnauthorized: false }, () => {
tlsSock.write(`GET / HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\nUser-Agent: WindsurfAPI/ProxyTest\r\n\r\n`);
});
const chunks = [];
tlsSock.on('data', c => chunks.push(c));
tlsSock.on('end', () => {
const body = Buffer.concat(chunks).toString('utf-8');
const match = body.match(/\r\n\r\n([^\r\n]+)/);
const ip = match ? match[1].trim() : '';
tlsSock.destroy();
if (!ip || !/^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
return reject(new Error('TLS ้šง้“ๅปบ็ซ‹ไฝ†่ฟ”ๅ›žๅ†…ๅฎนๅผ‚ๅธธ'));
}
resolve({ egressIp: ip, type });
});
tlsSock.on('error', (err) => reject(new Error(`TLS ๅคฑ่ดฅ: ${err.message}`)));
});
req.on('error', (err) => reject(new Error(`่ฟžๆŽฅๅคฑ่ดฅ: ${err.message}`)));
req.on('timeout', () => { req.destroy(); reject(new Error('่ถ…ๆ—ถ๏ผˆ10s๏ผ‰')); });
req.end();
});
}