fix(cliproxy): mask control panel login key (#1617)

This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-30 12:40:07 -04:00
1 parent 4283c4665c
commit bb7c3a4037
3 files changed
+22 -16

No files matched your search

@@ -1,12 +1,12 @@
/** /**
* Tests for printControlPanelAccess: the Control Panel (API Management Center) * Tests for printControlPanelAccess: the Control Panel (API Management Center)
* URL + login-key surface shown by `ccs cliproxy status` / `start`. * URL + masked login-key hint shown by `ccs cliproxy status` / `start`.
* *
* Why this matters: * Why this matters:
* - The Control Panel login screen only asks for a "Management Key" with no * - Routine lifecycle output is commonly pasted into logs and support tickets,
* hint. Users who don't know the default (`ccs`) cannot get in. This surface * so it must not disclose the raw management key. The helper should print
* is the fix, so it must print the panel URL on the active port and the * the panel URL, a masked key for orientation, and the explicit command users
* EFFECTIVE key (default `ccs`, or the user's custom management_secret). * can run when they intentionally need the full key.
*/ */
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
@@ -56,12 +56,14 @@ describe('printControlPanelAccess', () => {
} }
}); });
it('prints the panel URL on the given port and the default key (ccs)', () => { it('prints the panel URL on the given port and masks the default key', () => {
printControlPanelAccess(8317); printControlPanelAccess(8317);
const out = lines.join('\n'); const out = lines.join('\n');
expect(out).toContain('http://127.0.0.1:8317/management.html'); expect(out).toContain('http://127.0.0.1:8317/management.html');
expect(out).toContain('Panel login key:'); expect(out).toContain('Panel login key:');
expect(out).toContain('ccs'); expect(out).toContain('Panel login key: ****');
expect(out).toContain('ccs tokens --show');
expect(out).not.toContain('Panel login key: ccs');
}); });
it('uses the active port in the URL', () => { it('uses the active port in the URL', () => {
@@ -69,7 +71,7 @@ describe('printControlPanelAccess', () => {
expect(lines.join('\n')).toContain('http://127.0.0.1:9000/management.html'); expect(lines.join('\n')).toContain('http://127.0.0.1:9000/management.html');
}); });
it('prints a custom management_secret when configured', () => { it('masks a custom management_secret when configured', () => {
mutateConfig((config) => { mutateConfig((config) => {
if (!config.cliproxy) { if (!config.cliproxy) {
config.cliproxy = {}; config.cliproxy = {};
@@ -84,6 +86,8 @@ describe('printControlPanelAccess', () => {
printControlPanelAccess(8317); printControlPanelAccess(8317);
const out = lines.join('\n'); const out = lines.join('\n');
expect(out).toContain('Panel login key:'); expect(out).toContain('Panel login key:');
expect(out).toContain('my-custom-key'); expect(out).toContain('my-c...-key');
expect(out).toContain('ccs tokens --show');
expect(out).not.toContain('my-custom-key');
}); });
}); });
+1 -1
View File
@@ -91,7 +91,7 @@ export async function showHelp(): Promise<void> {
['restart', 'Restart CLIProxy instance'], ['restart', 'Restart CLIProxy instance'],
[ [
'status [--verbose]', 'status [--verbose]',
'Show CLIProxy status + Control Panel URL and login key (--verbose adds uptime)', 'Show CLIProxy status + Control Panel URL and masked login key (--verbose adds uptime)',
], ],
['stop', 'Stop running CLIProxy instance'], ['stop', 'Stop running CLIProxy instance'],
['doctor | diag', 'Quota diagnostics and shared project detection'], ['doctor | diag', 'Quota diagnostics and shared project detection'],
@@ -12,22 +12,24 @@ import { initUI, header, color, dim, ok, warn, info } from '../../utils/ui';
import { getProxyStatus, startProxy, stopProxy } from '../../cliproxy/services'; import { getProxyStatus, startProxy, stopProxy } from '../../cliproxy/services';
import { detectRunningProxy } from '../../cliproxy/proxy/proxy-detector'; import { detectRunningProxy } from '../../cliproxy/proxy/proxy-detector';
import { resolveLifecyclePort } from '../../cliproxy/config/port-manager'; import { resolveLifecyclePort } from '../../cliproxy/config/port-manager';
import { getEffectiveManagementSecret } from '../../cliproxy/auth/auth-token-manager'; import { getEffectiveManagementSecret, maskToken } from '../../cliproxy/auth/auth-token-manager';
/** /**
* Print how to reach the local CLIProxy Control Panel (a.k.a. API Management * Print how to reach the local CLIProxy Control Panel (a.k.a. API Management
* Center) and the key needed to log in. * Center) without exposing the management secret in routine output.
* *
* The panel is served by CLIProxy at `/management.html` on the proxy port and * The panel is served by CLIProxy at `/management.html` on the proxy port and
* its login is gated by the management secret (default `ccs`). The login screen * its login is gated by the management secret (default `ccs`). Keep the resolved
* only asks for a "Management Key" with no hint, so users frequently cannot get * key masked here because `start` and `status` output is commonly shared in
* in. Surfacing the URL + resolved key here removes that guesswork. * support tickets and logs. Users can explicitly reveal tokens with
* `ccs tokens --show` when they need the raw value.
*/ */
export function printControlPanelAccess(port: number): void { export function printControlPanelAccess(port: number): void {
const secret = getEffectiveManagementSecret(); const secret = getEffectiveManagementSecret();
console.log(''); console.log('');
console.log(` Control Panel: http://127.0.0.1:${port}/management.html`); console.log(` Control Panel: http://127.0.0.1:${port}/management.html`);
console.log(` Panel login key: ${secret}`); console.log(` Panel login key: ${maskToken(secret)}`);
console.log(dim(' To show the full key: ccs tokens --show'));
} }
export async function handleStart(verbose = false): Promise<void> { export async function handleStart(verbose = false): Promise<void> {