agent-messenger 1.9.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/commands/server.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/server.js +5 -2
- package/dist/src/platforms/discord/commands/server.js.map +1 -1
- package/dist/src/platforms/discord/token-extractor.d.ts +1 -0
- package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/discord/token-extractor.js +7 -0
- package/dist/src/platforms/discord/token-extractor.js.map +1 -1
- package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/client.js +1 -1
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/slack/commands/auth.d.ts +1 -0
- package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/auth.js +28 -6
- package/dist/src/platforms/slack/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/file.js +1 -1
- package/dist/src/platforms/slack/commands/file.js.map +1 -1
- package/dist/src/platforms/slack/commands/reaction.js +4 -4
- package/dist/src/platforms/slack/commands/reaction.js.map +1 -1
- package/dist/src/platforms/slack/commands/workspace.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/workspace.js +2 -2
- package/dist/src/platforms/slack/commands/workspace.js.map +1 -1
- package/dist/src/platforms/slack/ensure-auth.d.ts +7 -0
- package/dist/src/platforms/slack/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/ensure-auth.js +29 -3
- package/dist/src/platforms/slack/ensure-auth.js.map +1 -1
- package/dist/src/platforms/slack/token-extractor.d.ts +10 -2
- package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/slack/token-extractor.js +147 -23
- package/dist/src/platforms/slack/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/client.js +2 -2
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/team.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/team.js +4 -4
- package/dist/src/platforms/teams/commands/team.js.map +1 -1
- package/package.json +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/src/platforms/discord/commands/server.ts +15 -2
- package/src/platforms/discord/token-extractor.test.ts +25 -0
- package/src/platforms/discord/token-extractor.ts +7 -0
- package/src/platforms/discordbot/client.ts +4 -1
- package/src/platforms/slack/commands/auth.test.ts +23 -0
- package/src/platforms/slack/commands/auth.ts +34 -6
- package/src/platforms/slack/commands/file.ts +6 -1
- package/src/platforms/slack/commands/reaction.ts +4 -4
- package/src/platforms/slack/commands/workspace.ts +12 -2
- package/src/platforms/slack/ensure-auth.test.ts +54 -1
- package/src/platforms/slack/ensure-auth.ts +32 -2
- package/src/platforms/slack/token-extractor.test.ts +207 -0
- package/src/platforms/slack/token-extractor.ts +161 -28
- package/src/platforms/teams/client.ts +2 -2
- package/src/platforms/teams/commands/team.ts +14 -4
|
@@ -31,6 +31,213 @@ function createCookiesDb(
|
|
|
31
31
|
db.close()
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
describe('TokenExtractor token deduplication', () => {
|
|
35
|
+
test('keeps first token per team and upgrades unknown team name', async () => {
|
|
36
|
+
// given — two .log entries for the same team: first has unknown name, second has a name
|
|
37
|
+
const slackDir = mkdtempSync(join(tmpdir(), 'slack-dedup-'))
|
|
38
|
+
tempDirs.push(slackDir)
|
|
39
|
+
|
|
40
|
+
const hex64a = 'a'.repeat(64)
|
|
41
|
+
const hex64b = 'b'.repeat(64)
|
|
42
|
+
const tokenA = `xoxc-1111111111-2222222222-3333333333-${hex64a}`
|
|
43
|
+
const tokenB = `xoxc-4444444444-5555555555-6666666666-${hex64b}`
|
|
44
|
+
|
|
45
|
+
const leveldbDir = join(slackDir, 'Local Storage', 'leveldb')
|
|
46
|
+
mkdirSync(leveldbDir, { recursive: true })
|
|
47
|
+
// First entry: tokenA with team ID but no name
|
|
48
|
+
// Second entry: tokenB with same team ID and a name
|
|
49
|
+
writeFileSync(join(leveldbDir, '000001.log'), `"${tokenA}"T12345678xxx"${tokenB}"T12345678"name":"workspace-name"`)
|
|
50
|
+
|
|
51
|
+
// when
|
|
52
|
+
const extractor = new TokenExtractor('darwin', slackDir)
|
|
53
|
+
const result = await extractor.extract()
|
|
54
|
+
|
|
55
|
+
// then — first token wins, but team name is upgraded
|
|
56
|
+
expect(result.length).toBe(1)
|
|
57
|
+
expect(result[0].token).toBe(tokenA)
|
|
58
|
+
expect(result[0].workspace_name).toBe('workspace-name')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('prefers .log tokens over .ldb tokens for same team', async () => {
|
|
62
|
+
// given — same team ID in both .log (fresh) and .ldb (stale)
|
|
63
|
+
const slackDir = mkdtempSync(join(tmpdir(), 'slack-dedup-order-'))
|
|
64
|
+
tempDirs.push(slackDir)
|
|
65
|
+
|
|
66
|
+
const hex64fresh = 'f'.repeat(64)
|
|
67
|
+
const hex64stale = 's'.repeat(64)
|
|
68
|
+
const freshToken = `xoxc-1111111111-2222222222-3333333333-${hex64fresh}`
|
|
69
|
+
const staleToken = `xoxc-9999999999-8888888888-7777777777-${hex64stale}`
|
|
70
|
+
|
|
71
|
+
const leveldbDir = join(slackDir, 'Local Storage', 'leveldb')
|
|
72
|
+
mkdirSync(leveldbDir, { recursive: true })
|
|
73
|
+
writeFileSync(join(leveldbDir, '000001.log'), `"${freshToken}"T12345678"name":"fresh-workspace"`)
|
|
74
|
+
writeFileSync(join(leveldbDir, '000002.ldb'), `"${staleToken}"T12345678"team_name":"stale-workspace"`)
|
|
75
|
+
|
|
76
|
+
// when
|
|
77
|
+
const extractor = new TokenExtractor('darwin', slackDir)
|
|
78
|
+
const result = await extractor.extract()
|
|
79
|
+
|
|
80
|
+
// then — .log token wins
|
|
81
|
+
expect(result.length).toBe(1)
|
|
82
|
+
expect(result[0].token).toBe(freshToken)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('TokenExtractor LevelDB fragmentation markers', () => {
|
|
87
|
+
function buildFragmentedLdbContent(tokenParts: string[], marker: number[]): Buffer {
|
|
88
|
+
// given — build binary content simulating LevelDB fragmentation:
|
|
89
|
+
// token segments joined by 4-byte marker instead of hyphens
|
|
90
|
+
const segments: Buffer[] = []
|
|
91
|
+
for (let i = 0; i < tokenParts.length; i++) {
|
|
92
|
+
segments.push(Buffer.from(tokenParts[i]))
|
|
93
|
+
if (i < tokenParts.length - 1) {
|
|
94
|
+
segments.push(Buffer.from(marker))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Surround with team ID context and a terminator
|
|
98
|
+
const prefix = Buffer.from('"team_name":"test-workspace"T12345678')
|
|
99
|
+
const suffix = Buffer.from('"')
|
|
100
|
+
return Buffer.concat([prefix, segments[0] ? Buffer.concat(segments) : Buffer.alloc(0), suffix])
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test('extracts token with old fragmentation marker [19 0d f0 NN]', async () => {
|
|
104
|
+
// given
|
|
105
|
+
const slackDir = mkdtempSync(join(tmpdir(), 'slack-marker-old-'))
|
|
106
|
+
tempDirs.push(slackDir)
|
|
107
|
+
|
|
108
|
+
const hex64 = 'a'.repeat(64)
|
|
109
|
+
const tokenParts = ['xoxc-1111111111', '2222222222', '3333333333', hex64]
|
|
110
|
+
const content = buildFragmentedLdbContent(tokenParts, [0x19, 0x0d, 0xf0, 0x5e])
|
|
111
|
+
|
|
112
|
+
const leveldbDir = join(slackDir, 'Local Storage', 'leveldb')
|
|
113
|
+
mkdirSync(leveldbDir, { recursive: true })
|
|
114
|
+
writeFileSync(join(leveldbDir, '000001.ldb'), content)
|
|
115
|
+
|
|
116
|
+
// when
|
|
117
|
+
const extractor = new TokenExtractor('darwin', slackDir)
|
|
118
|
+
const result = await extractor.extract()
|
|
119
|
+
|
|
120
|
+
// then
|
|
121
|
+
expect(result.length).toBe(1)
|
|
122
|
+
expect(result[0].token).toBe(`xoxc-1111111111-2222222222-3333333333-${hex64}`)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('extracts token with new fragmentation marker [15 0b f0 43]', async () => {
|
|
126
|
+
// given — marker whose 4th byte (0x43 = "C") is a valid hex char
|
|
127
|
+
const slackDir = mkdtempSync(join(tmpdir(), 'slack-marker-new-'))
|
|
128
|
+
tempDirs.push(slackDir)
|
|
129
|
+
|
|
130
|
+
const hex64 = 'b'.repeat(64)
|
|
131
|
+
const tokenParts = ['xoxc-4063338523', '4063338531', '8150876260673', hex64]
|
|
132
|
+
const content = buildFragmentedLdbContent(tokenParts, [0x15, 0x0b, 0xf0, 0x43])
|
|
133
|
+
|
|
134
|
+
const leveldbDir = join(slackDir, 'Local Storage', 'leveldb')
|
|
135
|
+
mkdirSync(leveldbDir, { recursive: true })
|
|
136
|
+
writeFileSync(join(leveldbDir, '000001.ldb'), content)
|
|
137
|
+
|
|
138
|
+
// when
|
|
139
|
+
const extractor = new TokenExtractor('darwin', slackDir)
|
|
140
|
+
const result = await extractor.extract()
|
|
141
|
+
|
|
142
|
+
// then — 0x43 ("C") must NOT leak into the token
|
|
143
|
+
expect(result.length).toBe(1)
|
|
144
|
+
expect(result[0].token).toBe(`xoxc-4063338523-4063338531-8150876260673-${hex64}`)
|
|
145
|
+
expect(result[0].token).not.toContain('C')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('extracts token with new fragmentation marker [15 0b f0 58]', async () => {
|
|
149
|
+
// given — marker whose 4th byte (0x58 = "X") is not a valid hex char
|
|
150
|
+
const slackDir = mkdtempSync(join(tmpdir(), 'slack-marker-58-'))
|
|
151
|
+
tempDirs.push(slackDir)
|
|
152
|
+
|
|
153
|
+
const hex64 = 'c'.repeat(64)
|
|
154
|
+
const tokenParts = ['xoxc-5555555555', '6666666666', '7777777777', hex64]
|
|
155
|
+
const content = buildFragmentedLdbContent(tokenParts, [0x15, 0x0b, 0xf0, 0x58])
|
|
156
|
+
|
|
157
|
+
const leveldbDir = join(slackDir, 'Local Storage', 'leveldb')
|
|
158
|
+
mkdirSync(leveldbDir, { recursive: true })
|
|
159
|
+
writeFileSync(join(leveldbDir, '000001.ldb'), content)
|
|
160
|
+
|
|
161
|
+
// when
|
|
162
|
+
const extractor = new TokenExtractor('darwin', slackDir)
|
|
163
|
+
const result = await extractor.extract()
|
|
164
|
+
|
|
165
|
+
// then
|
|
166
|
+
expect(result.length).toBe(1)
|
|
167
|
+
expect(result[0].token).toBe(`xoxc-5555555555-6666666666-7777777777-${hex64}`)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('TokenExtractor Linux cookie decryption', () => {
|
|
172
|
+
test('decrypts v10 cookie using peanuts password on Linux', async () => {
|
|
173
|
+
// given — LevelDB with valid token + v10-encrypted cookie using Linux key
|
|
174
|
+
const slackDir = mkdtempSync(join(tmpdir(), 'slack-linux-'))
|
|
175
|
+
tempDirs.push(slackDir)
|
|
176
|
+
|
|
177
|
+
const cookiePlaintext = 'xoxd-linuxTestCookie%2Bvalue'
|
|
178
|
+
const key = require('node:crypto').pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1')
|
|
179
|
+
const iv = Buffer.alloc(16, ' ')
|
|
180
|
+
const cipher = createCipheriv('aes-128-cbc', key, iv)
|
|
181
|
+
const ciphertext = Buffer.concat([cipher.update(cookiePlaintext, 'utf8'), cipher.final()])
|
|
182
|
+
const encryptedCookie = Buffer.concat([Buffer.from('v10'), ciphertext])
|
|
183
|
+
|
|
184
|
+
createCookiesDb(join(slackDir, 'Cookies'), [
|
|
185
|
+
{
|
|
186
|
+
name: 'd',
|
|
187
|
+
value: '',
|
|
188
|
+
encrypted_value: new Uint8Array(encryptedCookie),
|
|
189
|
+
host_key: '.slack.com',
|
|
190
|
+
last_access_utc: 1,
|
|
191
|
+
},
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
const token = `xoxc-1111111111-2222222222-3333333333-${'a'.repeat(64)}`
|
|
195
|
+
const leveldbDir = join(slackDir, 'Local Storage', 'leveldb')
|
|
196
|
+
mkdirSync(leveldbDir, { recursive: true })
|
|
197
|
+
writeFileSync(join(leveldbDir, '000001.log'), `"${token}"T12345678"name":"test-workspace"`)
|
|
198
|
+
|
|
199
|
+
// when
|
|
200
|
+
const extractor = new TokenExtractor('linux', slackDir)
|
|
201
|
+
const result = await extractor.extract()
|
|
202
|
+
|
|
203
|
+
// then
|
|
204
|
+
expect(result.length).toBe(1)
|
|
205
|
+
expect(result[0].cookie).toBe(cookiePlaintext)
|
|
206
|
+
expect(result[0].token).toBe(token)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('TokenExtractor debug logging', () => {
|
|
211
|
+
test('calls debugLog callback during extraction', async () => {
|
|
212
|
+
// given
|
|
213
|
+
const slackDir = mkdtempSync(join(tmpdir(), 'slack-debug-'))
|
|
214
|
+
tempDirs.push(slackDir)
|
|
215
|
+
mkdirSync(join(slackDir, 'storage'), { recursive: true })
|
|
216
|
+
|
|
217
|
+
const messages: string[] = []
|
|
218
|
+
const debugLog = (msg: string) => messages.push(msg)
|
|
219
|
+
|
|
220
|
+
// when
|
|
221
|
+
const extractor = new TokenExtractor('darwin', slackDir, undefined, debugLog)
|
|
222
|
+
await extractor.extract()
|
|
223
|
+
|
|
224
|
+
// then — should have emitted debug messages
|
|
225
|
+
expect(messages.length).toBeGreaterThan(0)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('does not throw when debugLog is not provided', async () => {
|
|
229
|
+
// given
|
|
230
|
+
const slackDir = mkdtempSync(join(tmpdir(), 'slack-no-debug-'))
|
|
231
|
+
tempDirs.push(slackDir)
|
|
232
|
+
mkdirSync(join(slackDir, 'storage'), { recursive: true })
|
|
233
|
+
|
|
234
|
+
// when — then — should not throw
|
|
235
|
+
const extractor = new TokenExtractor('darwin', slackDir)
|
|
236
|
+
const result = await extractor.extract()
|
|
237
|
+
expect(result).toEqual([])
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
34
241
|
describe('TokenExtractor Windows DPAPI', () => {
|
|
35
242
|
test('decryptDPAPI returns null on non-win32 platform', () => {
|
|
36
243
|
const extractor = new TokenExtractor('darwin', '/tmp/slack-test')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process'
|
|
2
2
|
import { createDecipheriv, pbkdf2Sync } from 'node:crypto'
|
|
3
|
-
import { copyFileSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
5
|
import { homedir, tmpdir } from 'node:os'
|
|
6
6
|
import { join } from 'node:path'
|
|
@@ -26,8 +26,14 @@ export class TokenExtractor {
|
|
|
26
26
|
private platform: NodeJS.Platform
|
|
27
27
|
private slackDir: string
|
|
28
28
|
private keyCache: DerivedKeyCache
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
private debugLog: ((message: string) => void) | null
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
platform?: NodeJS.Platform,
|
|
33
|
+
slackDir?: string,
|
|
34
|
+
keyCache?: DerivedKeyCache,
|
|
35
|
+
debugLog?: (message: string) => void,
|
|
36
|
+
) {
|
|
31
37
|
this.platform = platform ?? process.platform
|
|
32
38
|
|
|
33
39
|
if (!['darwin', 'linux', 'win32'].includes(this.platform)) {
|
|
@@ -36,6 +42,11 @@ export class TokenExtractor {
|
|
|
36
42
|
|
|
37
43
|
this.slackDir = slackDir ?? this.getSlackDir()
|
|
38
44
|
this.keyCache = keyCache ?? new DerivedKeyCache()
|
|
45
|
+
this.debugLog = debugLog ?? null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private debug(message: string): void {
|
|
49
|
+
this.debugLog?.(message)
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
getSlackDir(): string {
|
|
@@ -72,6 +83,26 @@ export class TokenExtractor {
|
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
async extractCookie(): Promise<string> {
|
|
87
|
+
if (!existsSync(this.slackDir)) {
|
|
88
|
+
return ''
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await this.getDerivedKeyAsync()
|
|
92
|
+
|
|
93
|
+
const cookie = await this.extractCookieFromSQLite()
|
|
94
|
+
if (!cookie && this.usedCachedKey) {
|
|
95
|
+
await this.clearKeyCache()
|
|
96
|
+
this.cachedKey = this.getDerivedKeyFromKeychain()
|
|
97
|
+
if (this.cachedKey) {
|
|
98
|
+
await this.keyCache.set('slack', this.cachedKey)
|
|
99
|
+
return await this.extractCookieFromSQLite()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return cookie
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
async extract(): Promise<ExtractedWorkspace[]> {
|
|
76
107
|
if (!existsSync(this.slackDir)) {
|
|
77
108
|
throw new Error(`Slack directory not found: ${this.slackDir}`)
|
|
@@ -142,8 +173,11 @@ export class TokenExtractor {
|
|
|
142
173
|
private deduplicateTokens(tokens: TokenInfo[]): TokenInfo[] {
|
|
143
174
|
const seen = new Map<string, TokenInfo>()
|
|
144
175
|
for (const token of tokens) {
|
|
145
|
-
|
|
176
|
+
const existing = seen.get(token.teamId)
|
|
177
|
+
if (!existing) {
|
|
146
178
|
seen.set(token.teamId, token)
|
|
179
|
+
} else if (existing.teamName === 'unknown' && token.teamName !== 'unknown') {
|
|
180
|
+
seen.set(token.teamId, { ...token, token: existing.token })
|
|
147
181
|
}
|
|
148
182
|
}
|
|
149
183
|
return Array.from(seen.values())
|
|
@@ -180,25 +214,57 @@ export class TokenExtractor {
|
|
|
180
214
|
}
|
|
181
215
|
|
|
182
216
|
private async extractFromLevelDB(dbPath: string): Promise<TokenInfo[]> {
|
|
183
|
-
|
|
217
|
+
// ClassicLevel on a copy avoids LevelDB prefix-compression artifacts
|
|
218
|
+
// that corrupt tokens when reading raw .ldb files
|
|
219
|
+
const classicLevelTokens = await this.extractViaClassicLevelCopy(dbPath)
|
|
220
|
+
if (classicLevelTokens.length > 0) {
|
|
221
|
+
return classicLevelTokens
|
|
222
|
+
}
|
|
184
223
|
|
|
185
|
-
// First try reading LDB files directly (more reliable for sandboxed apps)
|
|
186
224
|
const directTokens = this.extractTokensFromLDBFiles(dbPath)
|
|
187
225
|
if (directTokens.length > 0) {
|
|
188
226
|
return directTokens
|
|
189
227
|
}
|
|
190
228
|
|
|
191
|
-
|
|
229
|
+
return this.extractViaClassicLevel(dbPath)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async extractViaClassicLevelCopy(dbPath: string): Promise<TokenInfo[]> {
|
|
233
|
+
const tempDir = join(tmpdir(), `slack-leveldb-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
mkdirSync(tempDir, { recursive: true })
|
|
237
|
+
|
|
238
|
+
const files = readdirSync(dbPath)
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
if (file === 'LOCK') continue // ClassicLevel creates its own
|
|
241
|
+
const src = join(dbPath, file)
|
|
242
|
+
try {
|
|
243
|
+
if (statSync(src).isFile()) {
|
|
244
|
+
copyFileSync(src, join(tempDir, file))
|
|
245
|
+
}
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return await this.extractViaClassicLevel(tempDir)
|
|
250
|
+
} catch {
|
|
251
|
+
return []
|
|
252
|
+
} finally {
|
|
253
|
+
try {
|
|
254
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async extractViaClassicLevel(dbPath: string): Promise<TokenInfo[]> {
|
|
260
|
+
const tokens: TokenInfo[] = []
|
|
192
261
|
let db: ClassicLevel<string, string> | null = null
|
|
193
262
|
try {
|
|
194
263
|
db = new ClassicLevel(dbPath, { valueEncoding: 'utf8' })
|
|
195
264
|
|
|
196
265
|
for await (const [key, value] of db.iterator()) {
|
|
197
266
|
if (typeof value === 'string' && value.includes('xoxc-')) {
|
|
198
|
-
|
|
199
|
-
if (extracted) {
|
|
200
|
-
tokens.push(extracted)
|
|
201
|
-
}
|
|
267
|
+
tokens.push(...this.parseTokenValue(key, value))
|
|
202
268
|
}
|
|
203
269
|
}
|
|
204
270
|
} catch {
|
|
@@ -209,7 +275,6 @@ export class TokenExtractor {
|
|
|
209
275
|
} catch {}
|
|
210
276
|
}
|
|
211
277
|
}
|
|
212
|
-
|
|
213
278
|
return tokens
|
|
214
279
|
}
|
|
215
280
|
|
|
@@ -219,7 +284,13 @@ export class TokenExtractor {
|
|
|
219
284
|
// Prioritize .log files (not compacted, have clean data)
|
|
220
285
|
// Then fall back to .ldb files
|
|
221
286
|
const logFiles = readdirSync(dbPath).filter((f) => f.endsWith('.log'))
|
|
222
|
-
const ldbFiles = readdirSync(dbPath)
|
|
287
|
+
const ldbFiles = readdirSync(dbPath)
|
|
288
|
+
.filter((f) => f.endsWith('.ldb'))
|
|
289
|
+
.sort((a, b) => {
|
|
290
|
+
const statA = statSync(join(dbPath, a))
|
|
291
|
+
const statB = statSync(join(dbPath, b))
|
|
292
|
+
return statB.mtimeMs - statA.mtimeMs
|
|
293
|
+
})
|
|
223
294
|
const files = [...logFiles, ...ldbFiles]
|
|
224
295
|
|
|
225
296
|
for (const file of files) {
|
|
@@ -305,8 +376,10 @@ export class TokenExtractor {
|
|
|
305
376
|
i++
|
|
306
377
|
} else {
|
|
307
378
|
// Check for 4-byte fragmentation marker pattern
|
|
308
|
-
//
|
|
309
|
-
|
|
379
|
+
// LevelDB compaction inserts 4-byte markers where hyphens should be.
|
|
380
|
+
// Known patterns: [19 0d f0 NN], [15 0b f0 NN] — the 3rd byte (0xf0) is consistent.
|
|
381
|
+
// Match any 4-byte sequence where byte at offset +2 is 0xf0.
|
|
382
|
+
if (i + 3 < chunk.length && chunk[i + 2] === 0xf0) {
|
|
310
383
|
// Skip the 4 garbage bytes and insert a hyphen
|
|
311
384
|
result.push(0x2d) // hyphen
|
|
312
385
|
i += 4
|
|
@@ -370,33 +443,57 @@ export class TokenExtractor {
|
|
|
370
443
|
return { token, teamId, teamName }
|
|
371
444
|
}
|
|
372
445
|
|
|
373
|
-
private
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
446
|
+
private parseTokenValue(_key: string, value: string): TokenInfo[] {
|
|
447
|
+
// LevelDB values may have leading control characters (e.g. 0x01).
|
|
448
|
+
// Built dynamically to satisfy biome's noControlCharactersInRegex.
|
|
449
|
+
const controlChars = new RegExp(
|
|
450
|
+
`[${String.fromCharCode(0)}-${String.fromCharCode(8)}${String.fromCharCode(11)}${String.fromCharCode(12)}${String.fromCharCode(14)}-${String.fromCharCode(31)}]`,
|
|
451
|
+
'g',
|
|
452
|
+
)
|
|
453
|
+
const cleaned = value.replace(controlChars, '')
|
|
454
|
+
try {
|
|
455
|
+
const parsed = JSON.parse(cleaned)
|
|
456
|
+
if (parsed?.teams && typeof parsed.teams === 'object') {
|
|
457
|
+
return this.parseTeamsObject(parsed.teams)
|
|
458
|
+
}
|
|
459
|
+
} catch {}
|
|
460
|
+
|
|
461
|
+
const single = this.parseSingleToken(cleaned)
|
|
462
|
+
return single ? [single] : []
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private parseTeamsObject(teams: Record<string, any>): TokenInfo[] {
|
|
466
|
+
const tokens: TokenInfo[] = []
|
|
467
|
+
for (const [teamId, team] of Object.entries(teams)) {
|
|
468
|
+
if (!team?.token || typeof team.token !== 'string' || !team.token.startsWith('xoxc-')) continue
|
|
469
|
+
tokens.push({
|
|
470
|
+
token: team.token,
|
|
471
|
+
teamId: team.id || teamId,
|
|
472
|
+
teamName: team.name || 'unknown',
|
|
473
|
+
})
|
|
377
474
|
}
|
|
475
|
+
return tokens
|
|
476
|
+
}
|
|
378
477
|
|
|
379
|
-
|
|
478
|
+
private parseSingleToken(value: string): TokenInfo | null {
|
|
479
|
+
const tokenMatch = value.match(/xoxc-[a-zA-Z0-9-]+/)
|
|
480
|
+
if (!tokenMatch) return null
|
|
380
481
|
|
|
381
482
|
let teamId = 'unknown'
|
|
382
483
|
let teamName = 'unknown'
|
|
383
484
|
|
|
384
485
|
const teamIdMatch = value.match(/"team_id"\s*:\s*"(T[A-Z0-9]+)"/)
|
|
385
|
-
if (teamIdMatch)
|
|
386
|
-
teamId = teamIdMatch[1]
|
|
387
|
-
}
|
|
486
|
+
if (teamIdMatch) teamId = teamIdMatch[1]
|
|
388
487
|
|
|
389
488
|
const teamNameMatch = value.match(/"team_name"\s*:\s*"([^"]+)"/)
|
|
390
489
|
if (teamNameMatch) {
|
|
391
490
|
teamName = teamNameMatch[1]
|
|
392
491
|
} else {
|
|
393
492
|
const domainMatch = value.match(/"domain"\s*:\s*"([^"]+)"/)
|
|
394
|
-
if (domainMatch)
|
|
395
|
-
teamName = domainMatch[1]
|
|
396
|
-
}
|
|
493
|
+
if (domainMatch) teamName = domainMatch[1]
|
|
397
494
|
}
|
|
398
495
|
|
|
399
|
-
return { token, teamId, teamName }
|
|
496
|
+
return { token: tokenMatch[0], teamId, teamName }
|
|
400
497
|
}
|
|
401
498
|
|
|
402
499
|
private async extractCookieFromSQLite(): Promise<string> {
|
|
@@ -404,10 +501,13 @@ export class TokenExtractor {
|
|
|
404
501
|
if (!existsSync(cookiesPath)) {
|
|
405
502
|
const networkCookiesPath = join(this.slackDir, 'Network', 'Cookies')
|
|
406
503
|
if (!existsSync(networkCookiesPath)) {
|
|
504
|
+
this.debug(`Cookie file not found at ${cookiesPath} or ${networkCookiesPath}`)
|
|
407
505
|
return ''
|
|
408
506
|
}
|
|
507
|
+
this.debug(`Using Network cookies path: ${networkCookiesPath}`)
|
|
409
508
|
return this.readCookieFromDB(networkCookiesPath)
|
|
410
509
|
}
|
|
510
|
+
this.debug(`Using cookies path: ${cookiesPath}`)
|
|
411
511
|
return this.readCookieFromDB(cookiesPath)
|
|
412
512
|
}
|
|
413
513
|
|
|
@@ -425,6 +525,7 @@ export class TokenExtractor {
|
|
|
425
525
|
'Quit the Slack app completely and try again.',
|
|
426
526
|
)
|
|
427
527
|
}
|
|
528
|
+
this.debug(`Failed to copy cookie DB: ${(error as Error).message}`)
|
|
428
529
|
return ''
|
|
429
530
|
}
|
|
430
531
|
|
|
@@ -451,22 +552,29 @@ export class TokenExtractor {
|
|
|
451
552
|
}
|
|
452
553
|
|
|
453
554
|
if (!row) {
|
|
555
|
+
this.debug('No cookie row found in database')
|
|
454
556
|
return ''
|
|
455
557
|
}
|
|
456
558
|
|
|
457
559
|
if (row.value?.startsWith('xoxd-')) {
|
|
560
|
+
this.debug('Found plaintext cookie')
|
|
458
561
|
return row.value
|
|
459
562
|
}
|
|
460
563
|
|
|
461
564
|
if (row.encrypted_value && row.encrypted_value.length > 0) {
|
|
565
|
+
this.debug(`Found encrypted cookie (${row.encrypted_value.length} bytes)`)
|
|
462
566
|
const decrypted = this.tryDecryptCookie(Buffer.from(row.encrypted_value))
|
|
463
567
|
if (decrypted) {
|
|
568
|
+
this.debug('Cookie decrypted successfully')
|
|
464
569
|
return decrypted
|
|
465
570
|
}
|
|
571
|
+
this.debug('Cookie decryption failed')
|
|
466
572
|
}
|
|
467
573
|
|
|
574
|
+
this.debug('No usable cookie value in row')
|
|
468
575
|
return ''
|
|
469
|
-
} catch {
|
|
576
|
+
} catch (error) {
|
|
577
|
+
this.debug(`Cookie DB query failed: ${(error as Error).message}`)
|
|
470
578
|
return ''
|
|
471
579
|
} finally {
|
|
472
580
|
try {
|
|
@@ -485,6 +593,9 @@ export class TokenExtractor {
|
|
|
485
593
|
if (this.platform === 'win32') {
|
|
486
594
|
return this.decryptV10CookieWindows(encrypted)
|
|
487
595
|
}
|
|
596
|
+
if (this.platform === 'linux') {
|
|
597
|
+
return this.decryptV10CookieLinux(encrypted)
|
|
598
|
+
}
|
|
488
599
|
return this.decryptV10Cookie(encrypted)
|
|
489
600
|
}
|
|
490
601
|
|
|
@@ -524,6 +635,23 @@ export class TokenExtractor {
|
|
|
524
635
|
}
|
|
525
636
|
}
|
|
526
637
|
|
|
638
|
+
private decryptV10CookieLinux(encrypted: Buffer): string | null {
|
|
639
|
+
try {
|
|
640
|
+
const key = pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1')
|
|
641
|
+
const iv = Buffer.alloc(16, ' ')
|
|
642
|
+
const ciphertext = encrypted.subarray(3)
|
|
643
|
+
|
|
644
|
+
const decipher = createDecipheriv('aes-128-cbc', key, iv)
|
|
645
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
|
646
|
+
const result = decrypted.toString('utf8')
|
|
647
|
+
|
|
648
|
+
const match = result.match(/xoxd-[A-Za-z0-9%]+/)
|
|
649
|
+
return match ? match[0] : null
|
|
650
|
+
} catch {
|
|
651
|
+
return null
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
527
655
|
decryptV10CookieWindows(encrypted: Buffer): string | null {
|
|
528
656
|
try {
|
|
529
657
|
const masterKey = this.getWindowsMasterKey()
|
|
@@ -607,6 +735,7 @@ export class TokenExtractor {
|
|
|
607
735
|
|
|
608
736
|
private async getDerivedKeyAsync(): Promise<Buffer | null> {
|
|
609
737
|
if (this.platform !== 'darwin') {
|
|
738
|
+
this.debug(`Skipping Keychain key derivation (platform: ${this.platform})`)
|
|
610
739
|
return null
|
|
611
740
|
}
|
|
612
741
|
|
|
@@ -614,6 +743,7 @@ export class TokenExtractor {
|
|
|
614
743
|
if (cached) {
|
|
615
744
|
this.cachedKey = cached
|
|
616
745
|
this.usedCachedKey = true
|
|
746
|
+
this.debug('Using cached derived key')
|
|
617
747
|
return cached
|
|
618
748
|
}
|
|
619
749
|
|
|
@@ -622,6 +752,9 @@ export class TokenExtractor {
|
|
|
622
752
|
this.cachedKey = key
|
|
623
753
|
await this.keyCache.set('slack', key)
|
|
624
754
|
this.usedCachedKey = false
|
|
755
|
+
this.debug('Derived key from Keychain')
|
|
756
|
+
} else {
|
|
757
|
+
this.debug('Failed to derive key from Keychain')
|
|
625
758
|
}
|
|
626
759
|
return key
|
|
627
760
|
}
|
|
@@ -86,7 +86,7 @@ export class TeamsClient {
|
|
|
86
86
|
|
|
87
87
|
private async request<T>(method: string, path: string, body?: unknown, baseUrl: string = MSG_API_BASE): Promise<T> {
|
|
88
88
|
if (this.isTokenExpired()) {
|
|
89
|
-
throw new TeamsError('Token has expired', 'token_expired')
|
|
89
|
+
throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired')
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const url = `${baseUrl}${path}`
|
|
@@ -151,7 +151,7 @@ export class TeamsClient {
|
|
|
151
151
|
|
|
152
152
|
private async requestFormData<T>(path: string, formData: FormData, baseUrl: string = MSG_API_BASE): Promise<T> {
|
|
153
153
|
if (this.isTokenExpired()) {
|
|
154
|
-
throw new TeamsError('Token has expired', 'token_expired')
|
|
154
|
+
throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired')
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
const url = `${baseUrl}${path}`
|
|
@@ -53,7 +53,12 @@ export async function switchAction(teamId: string, options: { pretty?: boolean }
|
|
|
53
53
|
const account = await credManager.getCurrentAccount()
|
|
54
54
|
|
|
55
55
|
if (!account?.teams?.[teamId]) {
|
|
56
|
-
console.log(
|
|
56
|
+
console.log(
|
|
57
|
+
formatOutput(
|
|
58
|
+
{ error: `Team not found: ${teamId}`, hint: 'Run "team list" to see available teams.' },
|
|
59
|
+
options.pretty,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
57
62
|
process.exit(1)
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -87,18 +92,23 @@ export async function removeAction(teamId: string, options: { pretty?: boolean }
|
|
|
87
92
|
const config = await credManager.loadConfig()
|
|
88
93
|
|
|
89
94
|
if (!config) {
|
|
90
|
-
console.log(formatOutput({ error: 'No configuration found.' }, options.pretty))
|
|
95
|
+
console.log(formatOutput({ error: 'No configuration found. Run "auth extract" first.' }, options.pretty))
|
|
91
96
|
process.exit(1)
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
const account = await credManager.getCurrentAccount()
|
|
95
100
|
if (!account) {
|
|
96
|
-
console.log(formatOutput({ error: 'No active account.' }, options.pretty))
|
|
101
|
+
console.log(formatOutput({ error: 'No active account. Run "auth extract" first.' }, options.pretty))
|
|
97
102
|
process.exit(1)
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
if (!account.teams[teamId]) {
|
|
101
|
-
console.log(
|
|
106
|
+
console.log(
|
|
107
|
+
formatOutput(
|
|
108
|
+
{ error: `Team not found: ${teamId}`, hint: 'Run "team list" to see available teams.' },
|
|
109
|
+
options.pretty,
|
|
110
|
+
),
|
|
111
|
+
)
|
|
102
112
|
process.exit(1)
|
|
103
113
|
}
|
|
104
114
|
|