mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
187 lines
6.0 KiB
TypeScript
187 lines
6.0 KiB
TypeScript
import { randomBytes } from 'crypto';
|
|
import { spawn } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import { ensureCLIProxyBinary, getInstalledCliproxyVersion } from '../cliproxy/binary-manager';
|
|
import {
|
|
configExists,
|
|
configNeedsRegeneration,
|
|
CCS_CONTROL_PANEL_SECRET,
|
|
CCS_INTERNAL_API_KEY,
|
|
generateConfig,
|
|
getCliproxyWritablePath,
|
|
regenerateConfig,
|
|
} from '../cliproxy/config/config-generator';
|
|
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
|
|
import { getCliproxyConfigPath } from '../cliproxy/config/path-resolver';
|
|
import { registerSession, unregisterSession } from '../cliproxy/session-tracker';
|
|
import {
|
|
getConfigYamlPath,
|
|
loadOrCreateUnifiedConfig,
|
|
mutateConfig,
|
|
} from '../config/config-loader-facade';
|
|
import {
|
|
addLegacyKeyGrace,
|
|
createDockerBootstrapState,
|
|
DOCKER_LEGACY_API_KEY,
|
|
isDockerLegacyKeyGraceActive,
|
|
isLikelyDockerGeneratedApiKey,
|
|
readDockerBootstrapState,
|
|
shouldRestoreDockerLegacyApiKey,
|
|
writeDockerBootstrapState,
|
|
} from './docker-key-rotation';
|
|
|
|
function generateDockerSecret(): string {
|
|
return randomBytes(32).toString('base64url');
|
|
}
|
|
|
|
export function ensureDockerCliproxyAuth(): boolean {
|
|
const now = new Date();
|
|
const hadUnifiedConfig = fs.existsSync(getConfigYamlPath());
|
|
const hadCliproxyConfig = configExists(CLIPROXY_DEFAULT_PORT);
|
|
const cliproxyConfigPath = getCliproxyConfigPath();
|
|
const existingCliproxyConfig = hadCliproxyConfig
|
|
? fs.readFileSync(cliproxyConfigPath, 'utf8')
|
|
: '';
|
|
const config = loadOrCreateUnifiedConfig();
|
|
const auth = config.cliproxy.auth;
|
|
const needsApiKey = !auth?.api_key || auth.api_key === CCS_INTERNAL_API_KEY;
|
|
const needsManagementSecret =
|
|
!auth?.management_secret || auth.management_secret === CCS_CONTROL_PANEL_SECRET;
|
|
const stateRead = readDockerBootstrapState();
|
|
const existingState = stateRead.state;
|
|
const existingApiKey = auth?.api_key;
|
|
|
|
let replacementApiKey = existingApiKey;
|
|
let configChanged = false;
|
|
|
|
if (needsApiKey || needsManagementSecret) {
|
|
mutateConfig((nextConfig) => {
|
|
nextConfig.cliproxy.auth ??= {};
|
|
if (needsApiKey) {
|
|
replacementApiKey = generateDockerSecret();
|
|
nextConfig.cliproxy.auth.api_key = replacementApiKey;
|
|
}
|
|
if (needsManagementSecret) {
|
|
nextConfig.cliproxy.auth.management_secret = generateDockerSecret();
|
|
}
|
|
});
|
|
configChanged = true;
|
|
}
|
|
|
|
if (!replacementApiKey) {
|
|
replacementApiKey = loadOrCreateUnifiedConfig().cliproxy.auth?.api_key;
|
|
}
|
|
|
|
const existingLegacyKeyInCliproxyConfig = existingCliproxyConfig.includes(
|
|
`"${DOCKER_LEGACY_API_KEY}"`
|
|
);
|
|
const freshInstall = !hadUnifiedConfig && !hadCliproxyConfig;
|
|
const oldDefaultUpgrade = needsApiKey && !freshInstall;
|
|
const explicitLegacyRestore = shouldRestoreDockerLegacyApiKey();
|
|
const legacyRestoreEligible = !existingState?.legacyKeyGrace;
|
|
const alreadyBrokenUpgrade =
|
|
explicitLegacyRestore &&
|
|
legacyRestoreEligible &&
|
|
hadCliproxyConfig &&
|
|
isLikelyDockerGeneratedApiKey(replacementApiKey) &&
|
|
!existingLegacyKeyInCliproxyConfig;
|
|
const corruptedRecoverableUpgrade =
|
|
explicitLegacyRestore &&
|
|
stateRead.corrupted &&
|
|
hadCliproxyConfig &&
|
|
isLikelyDockerGeneratedApiKey(replacementApiKey);
|
|
|
|
let nextState = existingState ?? createDockerBootstrapState(replacementApiKey, now);
|
|
|
|
if (replacementApiKey && !nextState.apiKey) {
|
|
nextState = { ...nextState, apiKey: replacementApiKey };
|
|
}
|
|
|
|
if (
|
|
replacementApiKey &&
|
|
!isDockerLegacyKeyGraceActive(existingState, now) &&
|
|
(oldDefaultUpgrade || alreadyBrokenUpgrade || corruptedRecoverableUpgrade)
|
|
) {
|
|
nextState = addLegacyKeyGrace(nextState, replacementApiKey, now);
|
|
}
|
|
|
|
if (!existingState || stateRead.corrupted || nextState !== existingState) {
|
|
writeDockerBootstrapState(nextState);
|
|
}
|
|
|
|
const legacyShouldBePresent = isDockerLegacyKeyGraceActive(nextState, now);
|
|
const legacyPresenceChanged = existingLegacyKeyInCliproxyConfig !== legacyShouldBePresent;
|
|
|
|
return configChanged || legacyPresenceChanged;
|
|
}
|
|
|
|
async function prepareIntegratedRuntime(): Promise<{ binaryPath: string; configPath: string }> {
|
|
const binaryPath = await ensureCLIProxyBinary(false);
|
|
const authWasGenerated = ensureDockerCliproxyAuth();
|
|
const configPath = !configExists(CLIPROXY_DEFAULT_PORT)
|
|
? generateConfig('gemini', CLIPROXY_DEFAULT_PORT)
|
|
: authWasGenerated || configNeedsRegeneration()
|
|
? regenerateConfig(CLIPROXY_DEFAULT_PORT)
|
|
: getCliproxyConfigPath();
|
|
|
|
return { binaryPath, configPath };
|
|
}
|
|
|
|
async function runCliproxy(): Promise<number> {
|
|
const { binaryPath, configPath } = await prepareIntegratedRuntime();
|
|
return new Promise<number>((resolve, reject) => {
|
|
const child = spawn(binaryPath, ['--config', configPath], {
|
|
stdio: 'inherit',
|
|
env: {
|
|
...process.env,
|
|
WRITABLE_PATH: getCliproxyWritablePath(),
|
|
},
|
|
});
|
|
|
|
// Register session lock so dashboard can detect the running proxy
|
|
let sessionId: string | undefined;
|
|
child.on('spawn', () => {
|
|
if (!child.pid) return;
|
|
try {
|
|
const version = getInstalledCliproxyVersion();
|
|
sessionId = registerSession(CLIPROXY_DEFAULT_PORT, child.pid, version, 'plus');
|
|
} catch (err) {
|
|
console.error(
|
|
`[cliproxy] Failed to register session lock: ${err instanceof Error ? err.message : String(err)}`
|
|
);
|
|
}
|
|
});
|
|
|
|
child.on('error', reject);
|
|
child.on('close', (code) => {
|
|
if (sessionId) {
|
|
unregisterSession(sessionId, CLIPROXY_DEFAULT_PORT);
|
|
}
|
|
resolve(code ?? 1);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function main(): Promise<number> {
|
|
const command = process.argv[2];
|
|
if (command !== 'run-cliproxy') {
|
|
console.error('[X] Usage: node dist/docker/docker-bootstrap.js run-cliproxy');
|
|
return 1;
|
|
}
|
|
|
|
return runCliproxy();
|
|
}
|
|
|
|
if (require.main === module) {
|
|
void main()
|
|
.then((exitCode) => {
|
|
process.exitCode = exitCode;
|
|
})
|
|
.catch((error) => {
|
|
console.error(
|
|
`[X] Failed to prepare Docker runtime: ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|