From 88fbac3a324e409a26bcbe7cf44810b94ef134e2 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 22:37:55 -0400 Subject: [PATCH] fix(channels): avoid secrets in --set-token argv (#1389) --- src/channels/official-channels-runtime.ts | 2 +- src/commands/config-channels-command.ts | 55 ++++++++----------- .../commands/config-channels-command.test.ts | 15 ++--- 3 files changed, 31 insertions(+), 41 deletions(-) diff --git a/src/channels/official-channels-runtime.ts b/src/channels/official-channels-runtime.ts index 9846ef71..fe8de5b6 100644 --- a/src/channels/official-channels-runtime.ts +++ b/src/channels/official-channels-runtime.ts @@ -809,7 +809,7 @@ export function getOfficialChannelsLegacyEnableHelp(): string { } export function getOfficialChannelTokenHelp(): string { - return 'Use --set-token =. If no channel is provided, Discord is assumed for backward compatibility.'; + return 'Use --set-token and pass the token via that channel env var (for example TELEGRAM_BOT_TOKEN=... ccs config channels --set-token telegram).'; } export function getOfficialChannelClearTokenHelp(): string { diff --git a/src/commands/config-channels-command.ts b/src/commands/config-channels-command.ts index c31cc9ab..6fffd019 100644 --- a/src/commands/config-channels-command.ts +++ b/src/commands/config-channels-command.ts @@ -54,31 +54,13 @@ interface ChannelsCommandOptions { setSelectionMissing: boolean; clearTokenAll: boolean; clearTokenChannel?: OfficialChannelId; - setToken?: { channelId: OfficialChannelId; token: string }; + setTokenChannel?: OfficialChannelId; setTokenMissing: boolean; clearTokenInvalid?: string; setTokenInvalid?: string; 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 { const setSelection = extractOption(args, ['--set']); 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; if (setToken.found && !setToken.missingValue && setToken.value) { - parsedSetToken = parseTokenAssignment(setToken.value) ?? undefined; - if (!parsedSetToken) { + const channelId = setToken.value.trim().toLowerCase(); + if (isOfficialChannelId(channelId)) { + parsedSetTokenChannel = channelId; + } else { setTokenInvalid = setToken.value; } } @@ -120,7 +104,7 @@ export function parseChannelsCommandArgs(args: string[]): ChannelsCommandOptions clearTokenAll, clearTokenChannel, clearTokenInvalid, - setToken: parsedSetToken, + setTokenChannel: parsedSetTokenChannel, setTokenMissing: setToken.found && setToken.missingValue, setTokenInvalid, help: hasAnyFlag(args, ['--help', '-h']), @@ -153,7 +137,7 @@ function showHelp(): void { ` ${color('--unattended', 'command')} Also add --dangerously-skip-permissions` ); console.log(` ${color('--no-unattended', 'command')} Disable unattended runtime flag`); - console.log(` ${color('--set-token ', 'command')} ${getOfficialChannelTokenHelp()}`); + console.log(` ${color('--set-token ', 'command')} ${getOfficialChannelTokenHelp()}`); console.log( ` ${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')}` ); 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( ` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}` @@ -395,7 +379,9 @@ export async function handleConfigChannelsCommand(args: string[]): Promise } if (options.setTokenInvalid) { console.error( - fail(`Invalid --set-token value: ${options.setTokenInvalid} (use =)`) + fail( + `Invalid --set-token value: ${options.setTokenInvalid} (use ${getOfficialChannelChoices()})` + ) ); process.exitCode = 1; return; @@ -444,12 +430,19 @@ export async function handleConfigChannelsCommand(args: string[]): Promise updateConfig({ channels: nextConfig }); } - if (options.setToken) { - if (!getOfficialChannelTokenIds().includes(options.setToken.channelId)) { - throw new Error(`${options.setToken.channelId} does not use a bot token.`); + if (options.setTokenChannel) { + if (!getOfficialChannelTokenIds().includes(options.setTokenChannel)) { + throw new Error(`${options.setTokenChannel} does not use a bot token.`); } - setConfiguredOfficialChannelToken(options.setToken.channelId, options.setToken.token); - console.log(ok(`${getOfficialChannelDisplayName(options.setToken.channelId)} token saved`)); + const envKey = getOfficialChannelEnvKey(options.setTokenChannel); + 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(''); } diff --git a/tests/unit/commands/config-channels-command.test.ts b/tests/unit/commands/config-channels-command.test.ts index a8ea3af8..b68d7557 100644 --- a/tests/unit/commands/config-channels-command.test.ts +++ b/tests/unit/commands/config-channels-command.test.ts @@ -2,35 +2,32 @@ import { describe, expect, it } from 'bun:test'; import { parseChannelsCommandArgs } from '../../../src/commands/config-channels-command'; 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([ '--set', 'telegram,discord', '--unattended', '--set-token', - 'telegram=telegram-secret', + 'telegram', ]); expect(result.setSelection).toBe('telegram,discord'); expect(result.unattended).toBe(true); - expect(result.setToken).toEqual({ - channelId: 'telegram', - token: 'telegram-secret', - }); + expect(result.setTokenChannel).toBe('telegram'); }); - it('supports inline token assignment, legacy flags, and clear-token variants', () => { + it('supports legacy flags and clear-token variants', () => { const result = parseChannelsCommandArgs([ '--disable', '--no-unattended', - '--set-token=abc', + '--set-token=discord', ]); const clearAll = parseChannelsCommandArgs(['--clear-token']); const clearOne = parseChannelsCommandArgs(['--clear-token', 'discord']); expect(result.disable).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(clearOne.clearTokenChannel).toBe('discord'); });