diff --git a/src/platforms/slack/commands/auth.ts b/src/platforms/slack/commands/auth.ts index 6ae099b..3379b4c 100644 --- a/src/platforms/slack/commands/auth.ts +++ b/src/platforms/slack/commands/auth.ts @@ -7,7 +7,8 @@ import { TokenExtractor } from '../token-extractor' async function extractAction(options: { pretty?: boolean; debug?: boolean }): Promise { try { - const extractor = new TokenExtractor() + const debugLog = options.debug ? (msg: string) => console.error(`[debug] ${msg}`) : undefined + const extractor = new TokenExtractor(undefined, undefined, undefined, debugLog) if (process.platform === 'darwin') { console.log('') diff --git a/src/platforms/slack/token-extractor.test.ts b/src/platforms/slack/token-extractor.test.ts index 8d113ae..ec5f819 100644 --- a/src/platforms/slack/token-extractor.test.ts +++ b/src/platforms/slack/token-extractor.test.ts @@ -155,6 +155,37 @@ describe('TokenExtractor Linux cookie decryption', () => { }) }) +describe('TokenExtractor debug logging', () => { + test('calls debugLog callback during extraction', async () => { + // given + const slackDir = mkdtempSync(join(tmpdir(), 'slack-debug-')) + tempDirs.push(slackDir) + mkdirSync(join(slackDir, 'storage'), { recursive: true }) + + const messages: string[] = [] + const debugLog = (msg: string) => messages.push(msg) + + // when + const extractor = new TokenExtractor('darwin', slackDir, undefined, debugLog) + await extractor.extract() + + // then — should have emitted debug messages + expect(messages.length).toBeGreaterThan(0) + }) + + test('does not throw when debugLog is not provided', async () => { + // given + const slackDir = mkdtempSync(join(tmpdir(), 'slack-no-debug-')) + tempDirs.push(slackDir) + mkdirSync(join(slackDir, 'storage'), { recursive: true }) + + // when — then — should not throw + const extractor = new TokenExtractor('darwin', slackDir) + const result = await extractor.extract() + expect(result).toEqual([]) + }) +}) + describe('TokenExtractor Windows DPAPI', () => { test('decryptDPAPI returns null on non-win32 platform', () => { const extractor = new TokenExtractor('darwin', '/tmp/slack-test') diff --git a/src/platforms/slack/token-extractor.ts b/src/platforms/slack/token-extractor.ts index 4c4774b..ee07bfa 100644 --- a/src/platforms/slack/token-extractor.ts +++ b/src/platforms/slack/token-extractor.ts @@ -26,8 +26,14 @@ export class TokenExtractor { private platform: NodeJS.Platform private slackDir: string private keyCache: DerivedKeyCache - - constructor(platform?: NodeJS.Platform, slackDir?: string, keyCache?: DerivedKeyCache) { + private debugLog: ((message: string) => void) | null + + constructor( + platform?: NodeJS.Platform, + slackDir?: string, + keyCache?: DerivedKeyCache, + debugLog?: (message: string) => void, + ) { this.platform = platform ?? process.platform if (!['darwin', 'linux', 'win32'].includes(this.platform)) { @@ -36,6 +42,11 @@ export class TokenExtractor { this.slackDir = slackDir ?? this.getSlackDir() this.keyCache = keyCache ?? new DerivedKeyCache() + this.debugLog = debugLog ?? null + } + + private debug(message: string): void { + this.debugLog?.(message) } getSlackDir(): string { @@ -406,10 +417,13 @@ export class TokenExtractor { if (!existsSync(cookiesPath)) { const networkCookiesPath = join(this.slackDir, 'Network', 'Cookies') if (!existsSync(networkCookiesPath)) { + this.debug(`Cookie file not found at ${cookiesPath} or ${networkCookiesPath}`) return '' } + this.debug(`Using Network cookies path: ${networkCookiesPath}`) return this.readCookieFromDB(networkCookiesPath) } + this.debug(`Using cookies path: ${cookiesPath}`) return this.readCookieFromDB(cookiesPath) } @@ -427,6 +441,7 @@ export class TokenExtractor { 'Quit the Slack app completely and try again.', ) } + this.debug(`Failed to copy cookie DB: ${(error as Error).message}`) return '' } @@ -453,22 +468,29 @@ export class TokenExtractor { } if (!row) { + this.debug('No cookie row found in database') return '' } if (row.value?.startsWith('xoxd-')) { + this.debug('Found plaintext cookie') return row.value } if (row.encrypted_value && row.encrypted_value.length > 0) { + this.debug(`Found encrypted cookie (${row.encrypted_value.length} bytes)`) const decrypted = this.tryDecryptCookie(Buffer.from(row.encrypted_value)) if (decrypted) { + this.debug('Cookie decrypted successfully') return decrypted } + this.debug('Cookie decryption failed') } + this.debug('No usable cookie value in row') return '' - } catch { + } catch (error) { + this.debug(`Cookie DB query failed: ${(error as Error).message}`) return '' } finally { try { @@ -629,6 +651,7 @@ export class TokenExtractor { private async getDerivedKeyAsync(): Promise { if (this.platform !== 'darwin') { + this.debug(`Skipping Keychain key derivation (platform: ${this.platform})`) return null } @@ -636,6 +659,7 @@ export class TokenExtractor { if (cached) { this.cachedKey = cached this.usedCachedKey = true + this.debug('Using cached derived key') return cached } @@ -644,6 +668,9 @@ export class TokenExtractor { this.cachedKey = key await this.keyCache.set('slack', key) this.usedCachedKey = false + this.debug('Derived key from Keychain') + } else { + this.debug('Failed to derive key from Keychain') } return key }