fix(channels): avoid secrets in --set-token argv (#1389)

This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-23 22:37:55 -04:00
committed by GitHub
parent fec84417a6
commit 88fbac3a32
3 changed files with 31 additions and 41 deletions
+1 -1
View File
@@ -809,7 +809,7 @@ export function getOfficialChannelsLegacyEnableHelp(): string {
} }
export function getOfficialChannelTokenHelp(): string { export function getOfficialChannelTokenHelp(): string {
return 'Use --set-token <channel>=<token>. If no channel is provided, Discord is assumed for backward compatibility.'; return 'Use --set-token <channel> and pass the token via that channel env var (for example TELEGRAM_BOT_TOKEN=... ccs config channels --set-token telegram).';
} }
export function getOfficialChannelClearTokenHelp(): string { export function getOfficialChannelClearTokenHelp(): string {
+24 -31
View File
@@ -54,31 +54,13 @@ interface ChannelsCommandOptions {
setSelectionMissing: boolean; setSelectionMissing: boolean;
clearTokenAll: boolean; clearTokenAll: boolean;
clearTokenChannel?: OfficialChannelId; clearTokenChannel?: OfficialChannelId;
setToken?: { channelId: OfficialChannelId; token: string }; setTokenChannel?: OfficialChannelId;
setTokenMissing: boolean; setTokenMissing: boolean;
clearTokenInvalid?: string; clearTokenInvalid?: string;
setTokenInvalid?: string; setTokenInvalid?: string;
help: boolean; help: boolean;
} }
function parseTokenAssignment(value: string): {
channelId: OfficialChannelId;
token: string;
} | null {
const separatorIndex = value.indexOf('=');
if (separatorIndex === -1) {
return value.trim() ? { channelId: 'discord', token: value.trim() } : null;
}
const channelId = value.slice(0, separatorIndex).trim().toLowerCase();
const token = value.slice(separatorIndex + 1).trim();
if (!isOfficialChannelId(channelId) || !token) {
return null;
}
return { channelId, token };
}
export function parseChannelsCommandArgs(args: string[]): ChannelsCommandOptions { export function parseChannelsCommandArgs(args: string[]): ChannelsCommandOptions {
const setSelection = extractOption(args, ['--set']); const setSelection = extractOption(args, ['--set']);
const setToken = extractOption(args, ['--set-token']); const setToken = extractOption(args, ['--set-token']);
@@ -100,11 +82,13 @@ export function parseChannelsCommandArgs(args: string[]): ChannelsCommandOptions
} }
} }
let parsedSetToken: { channelId: OfficialChannelId; token: string } | undefined; let parsedSetTokenChannel: OfficialChannelId | undefined;
let setTokenInvalid: string | undefined; let setTokenInvalid: string | undefined;
if (setToken.found && !setToken.missingValue && setToken.value) { if (setToken.found && !setToken.missingValue && setToken.value) {
parsedSetToken = parseTokenAssignment(setToken.value) ?? undefined; const channelId = setToken.value.trim().toLowerCase();
if (!parsedSetToken) { if (isOfficialChannelId(channelId)) {
parsedSetTokenChannel = channelId;
} else {
setTokenInvalid = setToken.value; setTokenInvalid = setToken.value;
} }
} }
@@ -120,7 +104,7 @@ export function parseChannelsCommandArgs(args: string[]): ChannelsCommandOptions
clearTokenAll, clearTokenAll,
clearTokenChannel, clearTokenChannel,
clearTokenInvalid, clearTokenInvalid,
setToken: parsedSetToken, setTokenChannel: parsedSetTokenChannel,
setTokenMissing: setToken.found && setToken.missingValue, setTokenMissing: setToken.found && setToken.missingValue,
setTokenInvalid, setTokenInvalid,
help: hasAnyFlag(args, ['--help', '-h']), help: hasAnyFlag(args, ['--help', '-h']),
@@ -153,7 +137,7 @@ function showHelp(): void {
` ${color('--unattended', 'command')} Also add --dangerously-skip-permissions` ` ${color('--unattended', 'command')} Also add --dangerously-skip-permissions`
); );
console.log(` ${color('--no-unattended', 'command')} Disable unattended runtime flag`); console.log(` ${color('--no-unattended', 'command')} Disable unattended runtime flag`);
console.log(` ${color('--set-token <spec>', 'command')} ${getOfficialChannelTokenHelp()}`); console.log(` ${color('--set-token <channel>', 'command')} ${getOfficialChannelTokenHelp()}`);
console.log( console.log(
` ${color('--clear-token [channel]', 'command')} ${getOfficialChannelClearTokenHelp()}` ` ${color('--clear-token [channel]', 'command')} ${getOfficialChannelClearTokenHelp()}`
); );
@@ -173,7 +157,7 @@ function showHelp(): void {
` $ ${color('ccs config channels --set all', 'command')} ${dim('# Enable all official channels')}` ` $ ${color('ccs config channels --set all', 'command')} ${dim('# Enable all official channels')}`
); );
console.log( console.log(
` $ ${color('ccs config channels --set-token telegram=123:abc', 'command')} ${dim('# Save TELEGRAM_BOT_TOKEN')}` ` $ ${color('TELEGRAM_BOT_TOKEN=123:abc ccs config channels --set-token telegram', 'command')} ${dim('# Save TELEGRAM_BOT_TOKEN')}`
); );
console.log( console.log(
` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}` ` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}`
@@ -395,7 +379,9 @@ export async function handleConfigChannelsCommand(args: string[]): Promise<void>
} }
if (options.setTokenInvalid) { if (options.setTokenInvalid) {
console.error( console.error(
fail(`Invalid --set-token value: ${options.setTokenInvalid} (use <channel>=<token>)`) fail(
`Invalid --set-token value: ${options.setTokenInvalid} (use ${getOfficialChannelChoices()})`
)
); );
process.exitCode = 1; process.exitCode = 1;
return; return;
@@ -444,12 +430,19 @@ export async function handleConfigChannelsCommand(args: string[]): Promise<void>
updateConfig({ channels: nextConfig }); updateConfig({ channels: nextConfig });
} }
if (options.setToken) { if (options.setTokenChannel) {
if (!getOfficialChannelTokenIds().includes(options.setToken.channelId)) { if (!getOfficialChannelTokenIds().includes(options.setTokenChannel)) {
throw new Error(`${options.setToken.channelId} does not use a bot token.`); throw new Error(`${options.setTokenChannel} does not use a bot token.`);
} }
setConfiguredOfficialChannelToken(options.setToken.channelId, options.setToken.token); const envKey = getOfficialChannelEnvKey(options.setTokenChannel);
console.log(ok(`${getOfficialChannelDisplayName(options.setToken.channelId)} token saved`)); const token = envKey ? process.env[envKey]?.trim() : '';
if (!token) {
throw new Error(
`${getOfficialChannelDisplayName(options.setTokenChannel)} token missing. Set ${envKey} in your environment and rerun.`
);
}
setConfiguredOfficialChannelToken(options.setTokenChannel, token);
console.log(ok(`${getOfficialChannelDisplayName(options.setTokenChannel)} token saved`));
console.log(''); console.log('');
} }
@@ -2,35 +2,32 @@ import { describe, expect, it } from 'bun:test';
import { parseChannelsCommandArgs } from '../../../src/commands/config-channels-command'; import { parseChannelsCommandArgs } from '../../../src/commands/config-channels-command';
describe('config channels command parser', () => { describe('config channels command parser', () => {
it('parses selection, unattended mode, and token input', () => { it('parses selection, unattended mode, and token channel input', () => {
const result = parseChannelsCommandArgs([ const result = parseChannelsCommandArgs([
'--set', '--set',
'telegram,discord', 'telegram,discord',
'--unattended', '--unattended',
'--set-token', '--set-token',
'telegram=telegram-secret', 'telegram',
]); ]);
expect(result.setSelection).toBe('telegram,discord'); expect(result.setSelection).toBe('telegram,discord');
expect(result.unattended).toBe(true); expect(result.unattended).toBe(true);
expect(result.setToken).toEqual({ expect(result.setTokenChannel).toBe('telegram');
channelId: 'telegram',
token: 'telegram-secret',
});
}); });
it('supports inline token assignment, legacy flags, and clear-token variants', () => { it('supports legacy flags and clear-token variants', () => {
const result = parseChannelsCommandArgs([ const result = parseChannelsCommandArgs([
'--disable', '--disable',
'--no-unattended', '--no-unattended',
'--set-token=abc', '--set-token=discord',
]); ]);
const clearAll = parseChannelsCommandArgs(['--clear-token']); const clearAll = parseChannelsCommandArgs(['--clear-token']);
const clearOne = parseChannelsCommandArgs(['--clear-token', 'discord']); const clearOne = parseChannelsCommandArgs(['--clear-token', 'discord']);
expect(result.disable).toBe(true); expect(result.disable).toBe(true);
expect(result.noUnattended).toBe(true); expect(result.noUnattended).toBe(true);
expect(result.setToken).toEqual({ channelId: 'discord', token: 'abc' }); expect(result.setTokenChannel).toBe('discord');
expect(clearAll.clearTokenAll).toBe(true); expect(clearAll.clearTokenAll).toBe(true);
expect(clearOne.clearTokenChannel).toBe('discord'); expect(clearOne.clearTokenChannel).toBe('discord');
}); });