agent-messenger 2.1.0 → 2.2.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.
Files changed (207) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.env.template +35 -17
  3. package/README.md +7 -7
  4. package/bun.lock +6 -6
  5. package/dist/package.json +2 -2
  6. package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
  7. package/dist/src/platforms/channeltalk/commands/auth.js +35 -28
  8. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  9. package/dist/src/platforms/channeltalk/ensure-auth.js +6 -6
  10. package/dist/src/platforms/channeltalk/ensure-auth.js.map +1 -1
  11. package/dist/src/platforms/channeltalk/token-extractor.d.ts +23 -1
  12. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  13. package/dist/src/platforms/channeltalk/token-extractor.js +299 -29
  14. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  15. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  16. package/dist/src/platforms/discord/commands/auth.js +57 -49
  17. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  18. package/dist/src/platforms/discord/ensure-auth.js +3 -3
  19. package/dist/src/platforms/discord/ensure-auth.js.map +1 -1
  20. package/dist/src/platforms/discord/token-extractor.d.ts +6 -1
  21. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  22. package/dist/src/platforms/discord/token-extractor.js +167 -14
  23. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  24. package/dist/src/platforms/instagram/client.d.ts +2 -0
  25. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  26. package/dist/src/platforms/instagram/client.js +2 -2
  27. package/dist/src/platforms/instagram/client.js.map +1 -1
  28. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  29. package/dist/src/platforms/instagram/commands/auth.js +107 -14
  30. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  31. package/dist/src/platforms/instagram/ensure-auth.d.ts.map +1 -1
  32. package/dist/src/platforms/instagram/ensure-auth.js +57 -11
  33. package/dist/src/platforms/instagram/ensure-auth.js.map +1 -1
  34. package/dist/src/platforms/instagram/index.d.ts +1 -0
  35. package/dist/src/platforms/instagram/index.d.ts.map +1 -1
  36. package/dist/src/platforms/instagram/index.js +1 -0
  37. package/dist/src/platforms/instagram/index.js.map +1 -1
  38. package/dist/src/platforms/instagram/token-extractor.d.ts +44 -0
  39. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -0
  40. package/dist/src/platforms/instagram/token-extractor.js +407 -0
  41. package/dist/src/platforms/instagram/token-extractor.js.map +1 -0
  42. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  43. package/dist/src/platforms/kakaotalk/client.js +2 -1
  44. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  45. package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
  46. package/dist/src/platforms/kakaotalk/commands/auth.js +14 -13
  47. package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
  48. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  49. package/dist/src/platforms/kakaotalk/protocol/connection.js +2 -1
  50. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  51. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  52. package/dist/src/platforms/line/commands/auth.js +6 -5
  53. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  54. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  55. package/dist/src/platforms/slack/commands/auth.js +11 -10
  56. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  57. package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
  58. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  59. package/dist/src/platforms/slack/token-extractor.js +300 -23
  60. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  61. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  62. package/dist/src/platforms/teams/commands/auth.js +9 -8
  63. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  64. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  65. package/dist/src/platforms/teams/ensure-auth.js +2 -1
  66. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  67. package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
  68. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  69. package/dist/src/platforms/teams/token-extractor.js +161 -29
  70. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  71. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  72. package/dist/src/platforms/telegram/client.js +25 -7
  73. package/dist/src/platforms/telegram/client.js.map +1 -1
  74. package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
  75. package/dist/src/platforms/telegram/commands/auth.js +6 -5
  76. package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
  77. package/dist/src/platforms/webex/client.d.ts +10 -0
  78. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  79. package/dist/src/platforms/webex/client.js +124 -0
  80. package/dist/src/platforms/webex/client.js.map +1 -1
  81. package/dist/src/platforms/webex/commands/auth.d.ts +4 -0
  82. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  83. package/dist/src/platforms/webex/commands/auth.js +46 -4
  84. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  85. package/dist/src/platforms/webex/credential-manager.js +1 -1
  86. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  87. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  88. package/dist/src/platforms/webex/ensure-auth.js +21 -5
  89. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  90. package/dist/src/platforms/webex/index.d.ts +2 -0
  91. package/dist/src/platforms/webex/index.d.ts.map +1 -1
  92. package/dist/src/platforms/webex/index.js +1 -0
  93. package/dist/src/platforms/webex/index.js.map +1 -1
  94. package/dist/src/platforms/webex/token-extractor.d.ts +28 -0
  95. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
  96. package/dist/src/platforms/webex/token-extractor.js +344 -0
  97. package/dist/src/platforms/webex/token-extractor.js.map +1 -0
  98. package/dist/src/platforms/webex/types.d.ts +4 -1
  99. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  100. package/dist/src/platforms/webex/types.js +2 -1
  101. package/dist/src/platforms/webex/types.js.map +1 -1
  102. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  103. package/dist/src/platforms/whatsapp/client.js +6 -2
  104. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  105. package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
  106. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  107. package/dist/src/shared/utils/error-handler.d.ts +1 -1
  108. package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
  109. package/dist/src/shared/utils/error-handler.js +3 -2
  110. package/dist/src/shared/utils/error-handler.js.map +1 -1
  111. package/dist/src/shared/utils/stderr.d.ts +5 -0
  112. package/dist/src/shared/utils/stderr.d.ts.map +1 -0
  113. package/dist/src/shared/utils/stderr.js +18 -0
  114. package/dist/src/shared/utils/stderr.js.map +1 -0
  115. package/docs/content/docs/cli/channeltalk.mdx +7 -7
  116. package/docs/content/docs/cli/discord.mdx +3 -3
  117. package/docs/content/docs/cli/instagram.mdx +28 -6
  118. package/docs/content/docs/cli/slack.mdx +2 -2
  119. package/docs/content/docs/cli/teams.mdx +6 -4
  120. package/docs/content/docs/cli/webex.mdx +30 -11
  121. package/e2e/README.md +132 -8
  122. package/e2e/channeltalk.e2e.test.ts +2 -7
  123. package/e2e/channeltalkbot.e2e.test.ts +2 -6
  124. package/e2e/config.ts +172 -10
  125. package/e2e/helpers.ts +7 -0
  126. package/e2e/instagram.e2e.test.ts +97 -0
  127. package/e2e/kakaotalk.e2e.test.ts +74 -0
  128. package/e2e/line.e2e.test.ts +92 -0
  129. package/e2e/teams.e2e.test.ts +46 -1
  130. package/e2e/telegram.e2e.test.ts +84 -0
  131. package/e2e/webex.e2e.test.ts +190 -0
  132. package/e2e/whatsapp.e2e.test.ts +90 -0
  133. package/e2e/whatsappbot.e2e.test.ts +78 -0
  134. package/package.json +2 -2
  135. package/skills/agent-channeltalk/SKILL.md +9 -9
  136. package/skills/agent-channeltalk/references/authentication.md +21 -18
  137. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  138. package/skills/agent-discord/SKILL.md +5 -5
  139. package/skills/agent-discord/references/authentication.md +8 -8
  140. package/skills/agent-discordbot/SKILL.md +1 -1
  141. package/skills/agent-instagram/SKILL.md +51 -9
  142. package/skills/agent-instagram/references/authentication.md +35 -3
  143. package/skills/agent-kakaotalk/SKILL.md +1 -1
  144. package/skills/agent-line/SKILL.md +1 -1
  145. package/skills/agent-slack/SKILL.md +5 -5
  146. package/skills/agent-slack/references/authentication.md +8 -8
  147. package/skills/agent-slackbot/SKILL.md +1 -1
  148. package/skills/agent-teams/SKILL.md +6 -6
  149. package/skills/agent-teams/references/authentication.md +8 -8
  150. package/skills/agent-telegram/SKILL.md +1 -1
  151. package/skills/agent-webex/SKILL.md +35 -15
  152. package/skills/agent-webex/references/authentication.md +62 -9
  153. package/skills/agent-webex/references/common-patterns.md +6 -3
  154. package/skills/agent-whatsapp/SKILL.md +1 -1
  155. package/skills/agent-whatsappbot/SKILL.md +1 -1
  156. package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
  157. package/src/platforms/channeltalk/commands/auth.ts +38 -32
  158. package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
  159. package/src/platforms/channeltalk/ensure-auth.ts +6 -6
  160. package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
  161. package/src/platforms/channeltalk/token-extractor.ts +344 -30
  162. package/src/platforms/discord/commands/auth.test.ts +3 -3
  163. package/src/platforms/discord/commands/auth.ts +58 -54
  164. package/src/platforms/discord/ensure-auth.test.ts +3 -3
  165. package/src/platforms/discord/ensure-auth.ts +3 -3
  166. package/src/platforms/discord/token-extractor.test.ts +199 -27
  167. package/src/platforms/discord/token-extractor.ts +190 -17
  168. package/src/platforms/instagram/client.ts +2 -2
  169. package/src/platforms/instagram/commands/auth.ts +133 -14
  170. package/src/platforms/instagram/ensure-auth.ts +63 -12
  171. package/src/platforms/instagram/index.ts +1 -0
  172. package/src/platforms/instagram/token-extractor.test.ts +424 -0
  173. package/src/platforms/instagram/token-extractor.ts +478 -0
  174. package/src/platforms/kakaotalk/client.ts +3 -1
  175. package/src/platforms/kakaotalk/commands/auth.ts +14 -13
  176. package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
  177. package/src/platforms/line/commands/auth.ts +7 -6
  178. package/src/platforms/slack/cli.test.ts +6 -5
  179. package/src/platforms/slack/commands/auth.test.ts +11 -7
  180. package/src/platforms/slack/commands/auth.ts +11 -10
  181. package/src/platforms/slack/token-extractor.test.ts +98 -1
  182. package/src/platforms/slack/token-extractor.ts +338 -26
  183. package/src/platforms/teams/commands/auth.ts +9 -8
  184. package/src/platforms/teams/ensure-auth.ts +3 -1
  185. package/src/platforms/teams/token-extractor.test.ts +136 -17
  186. package/src/platforms/teams/token-extractor.ts +182 -31
  187. package/src/platforms/telegram/client.test.ts +134 -0
  188. package/src/platforms/telegram/client.ts +27 -6
  189. package/src/platforms/telegram/commands/auth.ts +6 -5
  190. package/src/platforms/webex/client.test.ts +314 -0
  191. package/src/platforms/webex/client.ts +158 -0
  192. package/src/platforms/webex/commands/auth.ts +67 -4
  193. package/src/platforms/webex/commands/member.test.ts +10 -1
  194. package/src/platforms/webex/commands/message.test.ts +9 -5
  195. package/src/platforms/webex/commands/snapshot.test.ts +13 -4
  196. package/src/platforms/webex/commands/space.test.ts +12 -2
  197. package/src/platforms/webex/credential-manager.ts +1 -1
  198. package/src/platforms/webex/ensure-auth.test.ts +4 -0
  199. package/src/platforms/webex/ensure-auth.ts +23 -4
  200. package/src/platforms/webex/index.ts +2 -0
  201. package/src/platforms/webex/token-extractor.test.ts +327 -0
  202. package/src/platforms/webex/token-extractor.ts +393 -0
  203. package/src/platforms/webex/types.ts +4 -2
  204. package/src/platforms/whatsapp/client.ts +11 -7
  205. package/src/shared/utils/derived-key-cache.ts +1 -1
  206. package/src/shared/utils/error-handler.ts +4 -2
  207. package/src/shared/utils/stderr.ts +22 -0
@@ -13,9 +13,9 @@ let credManagerClearTokenSpy: ReturnType<typeof spyOn>
13
13
 
14
14
  beforeEach(() => {
15
15
  // Spy on DiscordTokenExtractor.prototype.extract
16
- extractorExtractSpy = spyOn(DiscordTokenExtractor.prototype, 'extract').mockResolvedValue({
16
+ extractorExtractSpy = spyOn(DiscordTokenExtractor.prototype, 'extract').mockResolvedValue([{
17
17
  token: 'test-token-123',
18
- })
18
+ }])
19
19
 
20
20
  // Spy on DiscordClient.prototype methods
21
21
  clientTestAuthSpy = spyOn(DiscordClient.prototype, 'testAuth').mockResolvedValue({
@@ -53,7 +53,7 @@ test('extract: calls DiscordTokenExtractor', async () => {
53
53
  const extractor = new DiscordTokenExtractor()
54
54
  const result = await extractor.extract()
55
55
  expect(result).toBeDefined()
56
- expect(result?.token).toBe('test-token-123')
56
+ expect(result[0]?.token).toBe('test-token-123')
57
57
  })
58
58
 
59
59
  test('extract: validates token with DiscordClient', async () => {
@@ -2,6 +2,7 @@ import { Command } from 'commander'
2
2
 
3
3
  import { handleError } from '@/shared/utils/error-handler'
4
4
  import { formatOutput } from '@/shared/utils/output'
5
+ import { debug } from '@/shared/utils/stderr'
5
6
 
6
7
  import { DiscordClient } from '../client'
7
8
  import { DiscordCredentialManager } from '../credential-manager'
@@ -29,12 +30,12 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
29
30
  }
30
31
 
31
32
  if (options.debug) {
32
- console.error(`[debug] Extracting Discord token...`)
33
+ debug(`[debug] Extracting Discord token...`)
33
34
  }
34
35
 
35
36
  const extracted = await extractor.extract()
36
37
 
37
- if (!extracted) {
38
+ if (extracted.length === 0) {
38
39
  console.log(
39
40
  formatOutput(
40
41
  {
@@ -47,62 +48,58 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
47
48
  process.exit(1)
48
49
  }
49
50
 
50
- if (options.debug) {
51
- console.error(`[debug] Token extracted: ${extracted.token.substring(0, 20)}...`)
52
- }
53
-
54
- try {
55
- const client = await new DiscordClient().login({ token: extracted.token })
56
-
51
+ for (const { token } of extracted) {
57
52
  if (options.debug) {
58
- console.error(`[debug] Testing token validity...`)
53
+ debug(`[debug] Token extracted: ${token.substring(0, 20)}...`)
59
54
  }
60
55
 
61
- const authInfo = await client.testAuth()
56
+ try {
57
+ const client = await new DiscordClient().login({ token })
62
58
 
63
- if (options.debug) {
64
- console.error(`[debug] Token valid for user: ${authInfo.username}`)
65
- console.error(`[debug] Discovering servers...`)
66
- }
59
+ if (options.debug) {
60
+ debug(`[debug] Testing token validity...`)
61
+ }
67
62
 
68
- const servers = await client.listServers()
63
+ const authInfo = await client.testAuth()
69
64
 
70
- if (options.debug) {
71
- console.error(`[debug] ✓ Found ${servers.length} server(s)`)
72
- }
65
+ if (options.debug) {
66
+ debug(`[debug] ✓ Token valid for user: ${authInfo.username}`)
67
+ debug(`[debug] Discovering servers...`)
68
+ }
73
69
 
74
- if (servers.length === 0) {
75
- console.log(
76
- formatOutput(
77
- {
78
- error: 'No servers found. Make sure you are a member of at least one Discord server.',
79
- },
80
- options.pretty,
81
- ),
82
- )
83
- process.exit(1)
84
- }
70
+ const servers = await client.listServers()
85
71
 
86
- const credManager = new DiscordCredentialManager()
87
- const serverMap: Record<string, { server_id: string; server_name: string }> = {}
72
+ if (options.debug) {
73
+ debug(`[debug] Found ${servers.length} server(s)`)
74
+ }
88
75
 
89
- for (const server of servers) {
90
- serverMap[server.id] = {
91
- server_id: server.id,
92
- server_name: server.name,
76
+ if (servers.length === 0) {
77
+ if (options.debug) {
78
+ debug(`[debug] No servers found for this token, trying next...`)
79
+ }
80
+ continue
93
81
  }
94
- }
95
82
 
96
- const config = {
97
- token: extracted.token,
98
- current_server: servers[0].id,
99
- servers: serverMap,
100
- }
83
+ const credManager = new DiscordCredentialManager()
84
+ const serverMap: Record<string, { server_id: string; server_name: string }> = {}
101
85
 
102
- await credManager.save(config)
86
+ for (const server of servers) {
87
+ serverMap[server.id] = {
88
+ server_id: server.id,
89
+ server_name: server.name,
90
+ }
91
+ }
92
+
93
+ const config = {
94
+ token,
95
+ current_server: servers[0].id,
96
+ servers: serverMap,
97
+ }
98
+
99
+ await credManager.save(config)
103
100
 
104
101
  if (options.debug) {
105
- console.error(`[debug] ✓ Credentials saved`)
102
+ debug(`[debug] ✓ Credentials saved`)
106
103
  }
107
104
 
108
105
  const output = {
@@ -111,18 +108,25 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
111
108
  }
112
109
 
113
110
  console.log(formatOutput(output, options.pretty))
111
+ return
114
112
  } catch (error) {
115
- console.log(
116
- formatOutput(
117
- {
118
- error: `Token validation failed: ${(error as Error).message}`,
119
- hint: 'Make sure your Discord token is valid and has not expired.',
120
- },
121
- options.pretty,
122
- ),
123
- )
124
- process.exit(1)
113
+ if (options.debug) {
114
+ debug(`[debug] Token validation failed: ${(error as Error).message}, trying next...`)
115
+ }
116
+ continue
117
+ }
125
118
  }
119
+
120
+ console.log(
121
+ formatOutput(
122
+ {
123
+ error: 'No usable Discord token found. Tokens may be expired or have no servers.',
124
+ hint: 'Make sure Discord is logged in and you are a member of at least one server.',
125
+ },
126
+ options.pretty,
127
+ ),
128
+ )
129
+ process.exit(1)
126
130
  } catch (error) {
127
131
  handleError(error as Error)
128
132
  }
@@ -14,9 +14,9 @@ let saveSpy: ReturnType<typeof spyOn>
14
14
  beforeEach(() => {
15
15
  getTokenSpy = spyOn(DiscordCredentialManager.prototype, 'getToken').mockResolvedValue(null)
16
16
 
17
- extractSpy = spyOn(DiscordTokenExtractor.prototype, 'extract').mockResolvedValue({
17
+ extractSpy = spyOn(DiscordTokenExtractor.prototype, 'extract').mockResolvedValue([{
18
18
  token: 'test-token-123',
19
- })
19
+ }])
20
20
 
21
21
  testAuthSpy = spyOn(DiscordClient.prototype, 'testAuth').mockResolvedValue({
22
22
  id: 'user-123',
@@ -90,7 +90,7 @@ describe('ensureDiscordAuth', () => {
90
90
 
91
91
  test('does not save when extraction returns null', async () => {
92
92
  // given
93
- extractSpy.mockResolvedValue(null)
93
+ extractSpy.mockResolvedValue([])
94
94
 
95
95
  // when
96
96
  await ensureDiscordAuth()
@@ -10,9 +10,9 @@ export async function ensureDiscordAuth(): Promise<void> {
10
10
 
11
11
  const extractor = new DiscordTokenExtractor()
12
12
  const extracted = await extractor.extract()
13
- if (!extracted) return
13
+ if (extracted.length === 0) return
14
14
 
15
- const client = await new DiscordClient().login({ token: extracted.token })
15
+ const client = await new DiscordClient().login({ token: extracted[0].token })
16
16
  const authInfo = await client.testAuth()
17
17
  if (!authInfo) return
18
18
 
@@ -23,7 +23,7 @@ export async function ensureDiscordAuth(): Promise<void> {
23
23
  }
24
24
 
25
25
  await credManager.save({
26
- token: extracted.token,
26
+ token: extracted[0].token,
27
27
  current_server: servers[0]?.id ?? null,
28
28
  servers: serverMap,
29
29
  })
@@ -55,6 +55,38 @@ describe('DiscordTokenExtractor', () => {
55
55
  })
56
56
  })
57
57
 
58
+ describe('getBrowserLevelDBDirs', () => {
59
+ test('returns browser LevelDB paths on macOS', () => {
60
+ const darwinExtractor = new DiscordTokenExtractor('darwin')
61
+ const dirs = darwinExtractor.getBrowserLevelDBDirs()
62
+
63
+ const chromeBase = join(homedir(), 'Library', 'Application Support', 'Google', 'Chrome')
64
+ expect(dirs).toContain(join(chromeBase, 'Default', 'Local Storage', 'leveldb'))
65
+ })
66
+
67
+ test('returns browser LevelDB paths on Linux', () => {
68
+ const linuxExtractor = new DiscordTokenExtractor('linux')
69
+ const dirs = linuxExtractor.getBrowserLevelDBDirs()
70
+
71
+ const chromeBase = join(homedir(), '.config', 'google-chrome')
72
+ expect(dirs).toContain(join(chromeBase, 'Default', 'Local Storage', 'leveldb'))
73
+ })
74
+
75
+ test('returns browser LevelDB paths on Windows', () => {
76
+ const winExtractor = new DiscordTokenExtractor('win32')
77
+ const dirs = winExtractor.getBrowserLevelDBDirs()
78
+
79
+ const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local')
80
+ const chromeBase = join(localAppData, 'Google', 'Chrome', 'User Data')
81
+ expect(dirs).toContain(join(chromeBase, 'Default', 'Local Storage', 'leveldb'))
82
+ })
83
+
84
+ test('returns empty array for unsupported platform', () => {
85
+ const unsupportedExtractor = new DiscordTokenExtractor('freebsd' as NodeJS.Platform)
86
+ expect(unsupportedExtractor.getBrowserLevelDBDirs()).toEqual([])
87
+ })
88
+ })
89
+
58
90
  describe('token patterns', () => {
59
91
  test('validates standard token format (base64.base64.base64)', () => {
60
92
  const validToken = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.ZZZZZZZZZZZZZZZZZZZZZZZZZ'
@@ -167,77 +199,217 @@ describe('DiscordTokenExtractor', () => {
167
199
  })
168
200
 
169
201
  describe('extract', () => {
170
- test('returns null when no Discord directories exist on linux', async () => {
202
+ test('returns empty array when no Discord directories exist on linux', async () => {
171
203
  const linuxExtractor = new DiscordTokenExtractor('linux')
172
- const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue(null)
204
+ const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue([])
205
+ const extractFromBrowserLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromBrowserLevelDB').mockResolvedValue([])
173
206
 
174
207
  const result = await linuxExtractor.extract()
175
- expect(result).toBeNull()
208
+ expect(result).toEqual([])
176
209
 
177
210
  extractFromLevelDBSpy.mockRestore()
211
+ extractFromBrowserLevelDBSpy.mockRestore()
178
212
  })
179
213
 
180
214
  test('extracts token from LevelDB when available', async () => {
181
215
  const mockToken = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.ZZZZZZZZZZZZZZZZZZZZZZZZZ'
182
216
 
183
217
  const linuxExtractor = new DiscordTokenExtractor('linux')
184
- const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue({
185
- token: mockToken,
186
- })
218
+ const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue([
219
+ { token: mockToken },
220
+ ])
221
+ const extractFromBrowserLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromBrowserLevelDB').mockResolvedValue([])
187
222
 
188
223
  const result = await linuxExtractor.extract()
189
224
 
190
- expect(result).not.toBeNull()
191
- expect(result?.token).toBe(mockToken)
225
+ expect(result).not.toEqual([])
226
+ expect(result[0]?.token).toBe(mockToken)
192
227
 
193
228
  extractFromLevelDBSpy.mockRestore()
229
+ extractFromBrowserLevelDBSpy.mockRestore()
194
230
  })
195
231
 
196
- test('tries CDP on macOS when LevelDB extraction fails', async () => {
232
+ test('tries browser LevelDB when desktop LevelDB extraction fails', async () => {
233
+ const mockToken = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.browser_token_1234567890123'
234
+
235
+ const linuxExtractor = new DiscordTokenExtractor('linux')
236
+ const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue([])
237
+ const extractFromBrowserLevelDBSpy = spyOn(
238
+ linuxExtractor as any,
239
+ 'extractFromBrowserLevelDB',
240
+ ).mockResolvedValue([{ token: mockToken }])
241
+
242
+ const result = await linuxExtractor.extract()
243
+
244
+ expect(extractFromLevelDBSpy).toHaveBeenCalled()
245
+ expect(extractFromBrowserLevelDBSpy).toHaveBeenCalled()
246
+ expect(result[0]?.token).toBe(mockToken)
247
+
248
+ extractFromLevelDBSpy.mockRestore()
249
+ extractFromBrowserLevelDBSpy.mockRestore()
250
+ })
251
+
252
+ test('tries CDP on macOS when both LevelDB extractions fail', async () => {
197
253
  const mockToken = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.cdp_token_12345678901234567'
198
254
 
199
255
  const darwinExtractor = new DiscordTokenExtractor('darwin', 0)
200
- const extractFromLevelDBSpy = spyOn(darwinExtractor as any, 'extractFromLevelDB').mockResolvedValue(null)
256
+ const extractFromLevelDBSpy = spyOn(darwinExtractor as any, 'extractFromLevelDB').mockResolvedValue([])
257
+ const extractFromBrowserLevelDBSpy = spyOn(
258
+ darwinExtractor as any,
259
+ 'extractFromBrowserLevelDB',
260
+ ).mockResolvedValue([])
201
261
  const tryExtractViaCDPSpy = spyOn(darwinExtractor as any, 'tryExtractViaCDP').mockResolvedValue(mockToken)
202
262
 
203
263
  const result = await darwinExtractor.extract()
204
264
 
205
- expect(result).not.toBeNull()
206
- expect(result?.token).toBe(mockToken)
265
+ expect(extractFromLevelDBSpy).toHaveBeenCalled()
266
+ expect(extractFromBrowserLevelDBSpy).toHaveBeenCalled()
267
+ expect(tryExtractViaCDPSpy).toHaveBeenCalled()
268
+ expect(result[0]?.token).toBe(mockToken)
207
269
 
208
270
  extractFromLevelDBSpy.mockRestore()
271
+ extractFromBrowserLevelDBSpy.mockRestore()
209
272
  tryExtractViaCDPSpy.mockRestore()
210
273
  })
211
274
 
212
- test('returns first valid token found across variants', async () => {
275
+ test('browser LevelDB tried before CDP on macOS', async () => {
276
+ const callOrder: string[] = []
277
+
278
+ const darwinExtractor = new DiscordTokenExtractor('darwin', 0)
279
+ const extractFromLevelDBSpy = spyOn(darwinExtractor as any, 'extractFromLevelDB').mockImplementation(async () => {
280
+ callOrder.push('desktop')
281
+ return []
282
+ })
283
+ const extractFromBrowserLevelDBSpy = spyOn(
284
+ darwinExtractor as any,
285
+ 'extractFromBrowserLevelDB',
286
+ ).mockImplementation(async () => {
287
+ callOrder.push('browser')
288
+ return []
289
+ })
290
+ const tryExtractViaCDPSpy = spyOn(darwinExtractor as any, 'tryExtractViaCDP').mockImplementation(async () => {
291
+ callOrder.push('cdp')
292
+ return null
293
+ })
294
+
295
+ await darwinExtractor.extract()
296
+
297
+ expect(callOrder).toEqual(['desktop', 'browser', 'cdp'])
298
+
299
+ extractFromLevelDBSpy.mockRestore()
300
+ extractFromBrowserLevelDBSpy.mockRestore()
301
+ tryExtractViaCDPSpy.mockRestore()
302
+ })
303
+
304
+ test('returns all valid tokens found across variants', async () => {
213
305
  const mockToken = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.first_token_found_1234567'
214
306
 
215
307
  const linuxExtractor = new DiscordTokenExtractor('linux')
216
- const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue({
217
- token: mockToken,
218
- })
308
+ const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue([
309
+ { token: mockToken },
310
+ ])
311
+ const extractFromBrowserLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromBrowserLevelDB').mockResolvedValue([])
312
+
313
+ const result = await linuxExtractor.extract()
314
+
315
+ expect(result).not.toEqual([])
316
+ expect(typeof result[0]?.token).toBe('string')
317
+
318
+ extractFromLevelDBSpy.mockRestore()
319
+ extractFromBrowserLevelDBSpy.mockRestore()
320
+ })
321
+
322
+ test('deduplicates the same token found in desktop and browser sources', async () => {
323
+ const mockToken = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.ZZZZZZZZZZZZZZZZZZZZZZZZZ'
324
+
325
+ const linuxExtractor = new DiscordTokenExtractor('linux')
326
+ const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue([
327
+ { token: mockToken },
328
+ ])
329
+ const extractFromBrowserLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromBrowserLevelDB').mockResolvedValue([
330
+ { token: mockToken },
331
+ ])
332
+
333
+ const result = await linuxExtractor.extract()
334
+ expect(result).toHaveLength(1)
335
+ expect(result[0]?.token).toBe(mockToken)
336
+
337
+ extractFromLevelDBSpy.mockRestore()
338
+ extractFromBrowserLevelDBSpy.mockRestore()
339
+ })
340
+
341
+ test('collects multiple distinct tokens from browser profiles', async () => {
342
+ const token1 = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.browser_token_1234567890123'
343
+ const token2 = 'YYYYYYYYYYYYYYYYYYYYYYYY.ZZZZZZ.browser_token_2345678901234'
344
+
345
+ const linuxExtractor = new DiscordTokenExtractor('linux')
346
+ const extractFromLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromLevelDB').mockResolvedValue([])
347
+ const extractFromBrowserLevelDBSpy = spyOn(linuxExtractor as any, 'extractFromBrowserLevelDB').mockResolvedValue([
348
+ { token: token1 },
349
+ { token: token2 },
350
+ ])
219
351
 
220
352
  const result = await linuxExtractor.extract()
353
+ expect(result).toHaveLength(2)
354
+ expect(result.map((r) => r.token)).toContain(token1)
355
+ expect(result.map((r) => r.token)).toContain(token2)
356
+
357
+ extractFromLevelDBSpy.mockRestore()
358
+ extractFromBrowserLevelDBSpy.mockRestore()
359
+ })
360
+
361
+ test('does not call CDP when desktop LevelDB extraction returns results', async () => {
362
+ const mockToken = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.ZZZZZZZZZZZZZZZZZZZZZZZZZ'
221
363
 
222
- expect(result).not.toBeNull()
223
- expect(typeof result?.token).toBe('string')
364
+ const darwinExtractor = new DiscordTokenExtractor('darwin', 0)
365
+ const extractFromLevelDBSpy = spyOn(darwinExtractor as any, 'extractFromLevelDB').mockResolvedValue([
366
+ { token: mockToken },
367
+ ])
368
+ const extractFromBrowserLevelDBSpy = spyOn(darwinExtractor as any, 'extractFromBrowserLevelDB').mockResolvedValue([])
369
+ const tryExtractViaCDPSpy = spyOn(darwinExtractor as any, 'tryExtractViaCDP').mockResolvedValue(null)
370
+
371
+ await darwinExtractor.extract()
372
+ expect(tryExtractViaCDPSpy).not.toHaveBeenCalled()
224
373
 
225
374
  extractFromLevelDBSpy.mockRestore()
375
+ extractFromBrowserLevelDBSpy.mockRestore()
376
+ tryExtractViaCDPSpy.mockRestore()
226
377
  })
227
378
  })
228
379
 
229
380
  describe('getKeychainVariants', () => {
230
- test('returns keychain variants for macOS', () => {
381
+ test('includes Discord-specific keychain variants', () => {
231
382
  const macExtractor = new DiscordTokenExtractor('darwin')
383
+ const variants = macExtractor.getKeychainVariants()
384
+
385
+ expect(variants).toContainEqual({ service: 'discord Safe Storage', account: 'discord Key' })
386
+ expect(variants).toContainEqual({ service: 'discordcanary Safe Storage', account: 'discordcanary Key' })
387
+ expect(variants).toContainEqual({ service: 'discordptb Safe Storage', account: 'discordptb Key' })
388
+ expect(variants).toContainEqual({ service: 'Discord Safe Storage', account: 'Discord' })
389
+ expect(variants).toContainEqual({ service: 'Discord Canary Safe Storage', account: 'Discord Canary' })
390
+ expect(variants).toContainEqual({ service: 'Discord PTB Safe Storage', account: 'Discord PTB' })
391
+ })
232
392
 
233
- expect(macExtractor.getKeychainVariants()).toEqual([
234
- { service: 'discord Safe Storage', account: 'discord Key' },
235
- { service: 'discordcanary Safe Storage', account: 'discordcanary Key' },
236
- { service: 'discordptb Safe Storage', account: 'discordptb Key' },
237
- { service: 'Discord Safe Storage', account: 'Discord' },
238
- { service: 'Discord Canary Safe Storage', account: 'Discord Canary' },
239
- { service: 'Discord PTB Safe Storage', account: 'Discord PTB' },
240
- ])
393
+ test('includes browser keychain variants appended after Discord entries', () => {
394
+ const macExtractor = new DiscordTokenExtractor('darwin')
395
+ const variants = macExtractor.getKeychainVariants()
396
+
397
+ expect(variants).toContainEqual({ service: 'Chrome Safe Storage', account: 'Chrome' })
398
+ expect(variants).toContainEqual({ service: 'Chrome Canary Safe Storage', account: 'Chrome Canary' })
399
+ expect(variants).toContainEqual({ service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' })
400
+ expect(variants).toContainEqual({ service: 'Arc Safe Storage', account: 'Arc' })
401
+ expect(variants).toContainEqual({ service: 'Brave Safe Storage', account: 'Brave' })
402
+ expect(variants).toContainEqual({ service: 'Vivaldi Safe Storage', account: 'Vivaldi' })
403
+ expect(variants).toContainEqual({ service: 'Chromium Safe Storage', account: 'Chromium' })
404
+ })
405
+
406
+ test('Discord entries come before browser entries', () => {
407
+ const macExtractor = new DiscordTokenExtractor('darwin')
408
+ const variants = macExtractor.getKeychainVariants()
409
+
410
+ const discordIdx = variants.findIndex((v) => v.service === 'discord Safe Storage')
411
+ const chromeIdx = variants.findIndex((v) => v.service === 'Chrome Safe Storage')
412
+ expect(discordIdx).toBeLessThan(chromeIdx)
241
413
  })
242
414
  })
243
415