Files
ccs/src/cliproxy/proxy-target-resolver.ts
T
Kai (Tam Nhu) TranandGitHub 24b03121fd fix(dashboard): cross-browser OAuth with manual callback fallback (#417) (#423)
- Remove destructive /start endpoint call from Dashboard OAuth dialog
  (was killing running CLIProxy Docker instances via killProcessOnPort)
- Use /start-url + polling only (management API, non-destructive)
- Auto-open browser tab via window.open() with manual fallback URL display
- Add paste-callback CLI mode (--paste-callback flag) for headless/SSH
- Use dynamic proxy target with management headers instead of hardcoded localhost
- Extract timeout constants, restore invariant comment
- Move hook-utils tests from src/__tests__/ to tests/unit/ (fixes tsc)
- Add try-catch for preset apply, remove auth URL console.log
2026-02-02 16:11:36 -05:00

141 lines
4.2 KiB
TypeScript

/**
* Proxy Target Resolver
*
* Determines whether CLIProxyAPI requests should go to local or remote
* based on unified config. Used by stats-fetcher, auth-routes, and UI.
*/
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import type { CliproxyServerConfig } from '../config/unified-config-types';
import {
CLIPROXY_DEFAULT_PORT,
getRemoteDefaultPort,
normalizeProtocol,
validateRemotePort,
} from './config-generator';
import { getEffectiveManagementSecret } from './auth-token-manager';
/** Resolved proxy target for making requests */
export interface ProxyTarget {
/** Target hostname or IP */
host: string;
/** Target port */
port: number;
/** Protocol (http/https) */
protocol: 'http' | 'https';
/** Optional auth token for API endpoints - only send header if defined and non-empty */
authToken?: string;
/** Optional management key for management API endpoints (/v0/management/*) */
managementKey?: string;
/** True if targeting remote server, false if local */
isRemote: boolean;
}
/**
* Load cliproxy_server configuration from unified config.
* Returns undefined if not configured.
*/
function loadCliproxyServerConfig(): CliproxyServerConfig | undefined {
const config = loadOrCreateUnifiedConfig();
return config.cliproxy_server;
}
/**
* Get the current CLIProxyAPI target based on unified config.
* Returns remote server config if enabled, otherwise localhost.
*/
export function getProxyTarget(): ProxyTarget {
const config = loadCliproxyServerConfig();
if (config?.remote?.enabled && config.remote?.host) {
// Normalize protocol (handles case sensitivity and invalid values)
const protocol = normalizeProtocol(config.remote.protocol);
// Validate port (returns undefined for invalid, triggering default)
const validatedPort = validateRemotePort(config.remote.port);
const port = validatedPort ?? getRemoteDefaultPort(protocol);
return {
host: config.remote.host,
port,
protocol,
authToken: config.remote.auth_token || undefined, // Empty string -> undefined
managementKey: config.remote.management_key || undefined, // Empty string -> undefined
isRemote: true,
};
}
const localPort = config?.local?.port ?? CLIPROXY_DEFAULT_PORT;
return {
host: '127.0.0.1',
port: localPort,
protocol: 'http',
isRemote: false,
};
}
/**
* Build URL for proxy endpoint
* @param target Resolved proxy target
* @param path Endpoint path (e.g., '/v0/management/usage')
*/
export function buildProxyUrl(target: ProxyTarget, path: string): string {
// Normalize path to ensure leading slash
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return `${target.protocol}://${target.host}:${target.port}${normalizedPath}`;
}
/**
* Build request headers for proxy requests
* Handles optional auth token - only adds Authorization header if token is set.
*
* @param target Resolved proxy target
* @param additionalHeaders Extra headers to merge
*/
export function buildProxyHeaders(
target: ProxyTarget,
additionalHeaders: Record<string, string> = {}
): Record<string, string> {
const headers: Record<string, string> = {
Accept: 'application/json',
...additionalHeaders,
};
// Only add auth header if token is configured
if (target.authToken) {
headers['Authorization'] = `Bearer ${target.authToken}`;
}
return headers;
}
/**
* Build request headers for management API endpoints (/v0/management/*).
* For remote targets: uses management_key, falls back to authToken.
* For local targets: uses the effective management secret from CCS config.
*
* @param target Resolved proxy target
* @param additionalHeaders Extra headers to merge
*/
export function buildManagementHeaders(
target: ProxyTarget,
additionalHeaders: Record<string, string> = {}
): Record<string, string> {
const headers: Record<string, string> = {
Accept: 'application/json',
...additionalHeaders,
};
// Remote: use configured management key or auth token
// Local: use CCS management secret (default: 'ccs')
const authKey = target.isRemote
? (target.managementKey ?? target.authToken)
: getEffectiveManagementSecret();
if (authKey) {
headers['Authorization'] = `Bearer ${authKey}`;
}
return headers;
}