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
@@ -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 { TeamsClient } from '../client'
7
8
  import { TeamsCredentialManager } from '../credential-manager'
@@ -35,7 +36,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
35
36
  }
36
37
 
37
38
  if (options.debug) {
38
- console.error('[debug] Extracting Teams tokens from all accounts...')
39
+ debug('[debug] Extracting Teams tokens from all accounts...')
39
40
  }
40
41
 
41
42
  const extracted = await extractor.extract()
@@ -54,7 +55,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
54
55
  }
55
56
 
56
57
  if (options.debug) {
57
- console.error(`[debug] Found ${extracted.length} account(s)`)
58
+ debug(`[debug] Found ${extracted.length} account(s)`)
58
59
  }
59
60
 
60
61
  const credManager = new TeamsCredentialManager()
@@ -67,7 +68,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
67
68
 
68
69
  for (const { token, accountType } of extracted) {
69
70
  if (options.debug) {
70
- console.error(`[debug] Validating ${accountType} account token...`)
71
+ debug(`[debug] Validating ${accountType} account token...`)
71
72
  }
72
73
 
73
74
  try {
@@ -76,7 +77,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
76
77
  const teams = await client.listTeams()
77
78
 
78
79
  if (options.debug) {
79
- console.error(`[debug] ✓ ${accountType}: ${authInfo.displayName} (${teams.length} team(s))`)
80
+ debug(`[debug] ✓ ${accountType}: ${authInfo.displayName} (${teams.length} team(s))`)
80
81
  }
81
82
 
82
83
  const teamMap: Record<string, { team_id: string; team_name: string }> = {}
@@ -107,7 +108,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
107
108
  const errorMessage = (error as Error).message
108
109
  const is401 = errorMessage.includes('401') || errorMessage.includes('Unauthorized')
109
110
  if (options.debug) {
110
- console.error(`[debug] ✗ ${accountType}: ${errorMessage}`)
111
+ debug(`[debug] ✗ ${accountType}: ${errorMessage}`)
111
112
  }
112
113
  if (extracted.length === 1) {
113
114
  console.log(
@@ -139,7 +140,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
139
140
  await credManager.saveConfig(config)
140
141
 
141
142
  if (options.debug) {
142
- console.error('[debug] ✓ Credentials saved')
143
+ debug('[debug] ✓ Credentials saved')
143
144
  }
144
145
 
145
146
  console.log(
@@ -158,7 +159,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
158
159
 
159
160
  async function extractManualToken(token: string, options: { pretty?: boolean; debug?: boolean }): Promise<void> {
160
161
  if (options.debug) {
161
- console.error(`[debug] Using provided token: ${token.substring(0, 20)}...`)
162
+ debug(`[debug] Using provided token: ${token.substring(0, 20)}...`)
162
163
  }
163
164
 
164
165
  try {
@@ -202,7 +203,7 @@ async function extractManualToken(token: string, options: { pretty?: boolean; de
202
203
  await credManager.saveConfig(config)
203
204
 
204
205
  if (options.debug) {
205
- console.error('[debug] ✓ Credentials saved')
206
+ debug('[debug] ✓ Credentials saved')
206
207
  }
207
208
 
208
209
  console.log(
@@ -1,3 +1,5 @@
1
+ import { warn } from '@/shared/utils/stderr'
2
+
1
3
  import { TeamsClient } from './client'
2
4
  import { TeamsCredentialManager } from './credential-manager'
3
5
  import { TeamsTokenExtractor } from './token-extractor'
@@ -47,7 +49,7 @@ export async function ensureTeamsAuth(): Promise<void> {
47
49
  newConfig.current_account = accountType
48
50
  }
49
51
  } catch (error) {
50
- console.error(`[agent-teams] Skipping ${accountType} account: ${(error as Error).message}`)
52
+ warn(`[agent-teams] Skipping ${accountType} account: ${(error as Error).message}`)
51
53
  }
52
54
  }
53
55
 
@@ -11,10 +11,10 @@ describe('TeamsTokenExtractor', () => {
11
11
  extractor = new TeamsTokenExtractor()
12
12
  })
13
13
 
14
- describe('getTeamsCookiesPaths', () => {
15
- test('returns darwin paths on macOS with New Teams first', () => {
14
+ describe('getDesktopCookiesPaths', () => {
15
+ test('returns darwin desktop paths on macOS', () => {
16
16
  const darwinExtractor = new TeamsTokenExtractor('darwin')
17
- const paths = darwinExtractor.getTeamsCookiesPaths()
17
+ const paths = darwinExtractor.getDesktopCookiesPaths()
18
18
 
19
19
  expect(paths).toEqual([
20
20
  {
@@ -75,9 +75,9 @@ describe('TeamsTokenExtractor', () => {
75
75
  ])
76
76
  })
77
77
 
78
- test('returns linux paths on Linux', () => {
78
+ test('returns linux desktop path on Linux', () => {
79
79
  const linuxExtractor = new TeamsTokenExtractor('linux')
80
- const paths = linuxExtractor.getTeamsCookiesPaths()
80
+ const paths = linuxExtractor.getDesktopCookiesPaths()
81
81
 
82
82
  expect(paths).toEqual([
83
83
  {
@@ -87,9 +87,9 @@ describe('TeamsTokenExtractor', () => {
87
87
  ])
88
88
  })
89
89
 
90
- test('returns win32 paths on Windows with New Teams first', () => {
90
+ test('returns win32 desktop paths on Windows', () => {
91
91
  const winExtractor = new TeamsTokenExtractor('win32')
92
- const paths = winExtractor.getTeamsCookiesPaths()
92
+ const paths = winExtractor.getDesktopCookiesPaths()
93
93
 
94
94
  const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local')
95
95
  const appdata = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming')
@@ -143,6 +143,105 @@ describe('TeamsTokenExtractor', () => {
143
143
  ])
144
144
  })
145
145
 
146
+ test('returns empty array for unsupported platform', () => {
147
+ const unsupportedExtractor = new TeamsTokenExtractor('freebsd' as NodeJS.Platform)
148
+ expect(unsupportedExtractor.getDesktopCookiesPaths()).toEqual([])
149
+ })
150
+ })
151
+
152
+ describe('getBrowserCookiesPaths', () => {
153
+ test('returns browser cookie paths on macOS (at least Default profile per browser)', () => {
154
+ const darwinExtractor = new TeamsTokenExtractor('darwin')
155
+ const paths = darwinExtractor.getBrowserCookiesPaths()
156
+
157
+ const chromeBase = join(homedir(), 'Library', 'Application Support', 'Google', 'Chrome')
158
+ expect(paths).toContainEqual({
159
+ path: join(chromeBase, 'Default', 'Cookies'),
160
+ accountType: 'work',
161
+ })
162
+ expect(paths).toContainEqual({
163
+ path: join(chromeBase, 'Default', 'Network', 'Cookies'),
164
+ accountType: 'work',
165
+ })
166
+ })
167
+
168
+ test('returns browser cookie paths on Linux', () => {
169
+ const linuxExtractor = new TeamsTokenExtractor('linux')
170
+ const paths = linuxExtractor.getBrowserCookiesPaths()
171
+
172
+ const chromeBase = join(homedir(), '.config', 'google-chrome')
173
+ expect(paths).toContainEqual({
174
+ path: join(chromeBase, 'Default', 'Cookies'),
175
+ accountType: 'work',
176
+ })
177
+ })
178
+
179
+ test('returns browser cookie paths on Windows', () => {
180
+ const winExtractor = new TeamsTokenExtractor('win32')
181
+ const paths = winExtractor.getBrowserCookiesPaths()
182
+
183
+ const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local')
184
+ const chromeBase = join(localAppData, 'Google', 'Chrome', 'User Data')
185
+ expect(paths).toContainEqual({
186
+ path: join(chromeBase, 'Default', 'Cookies'),
187
+ accountType: 'work',
188
+ })
189
+ })
190
+
191
+ test('returns empty array for unsupported platform', () => {
192
+ const unsupportedExtractor = new TeamsTokenExtractor('freebsd' as NodeJS.Platform)
193
+ expect(unsupportedExtractor.getBrowserCookiesPaths()).toEqual([])
194
+ })
195
+
196
+ test('all browser paths have accountType work', () => {
197
+ const darwinExtractor = new TeamsTokenExtractor('darwin')
198
+ const paths = darwinExtractor.getBrowserCookiesPaths()
199
+ expect(paths.every((p) => p.accountType === 'work')).toBe(true)
200
+ })
201
+ })
202
+
203
+ describe('getTeamsCookiesPaths', () => {
204
+ test('returns darwin paths on macOS with desktop paths first', () => {
205
+ const darwinExtractor = new TeamsTokenExtractor('darwin')
206
+ const paths = darwinExtractor.getTeamsCookiesPaths()
207
+ const desktopPaths = darwinExtractor.getDesktopCookiesPaths()
208
+
209
+ expect(paths.slice(0, desktopPaths.length)).toEqual(desktopPaths)
210
+ })
211
+
212
+ test('browser paths come after desktop paths on macOS', () => {
213
+ const darwinExtractor = new TeamsTokenExtractor('darwin')
214
+ const paths = darwinExtractor.getTeamsCookiesPaths()
215
+ const desktopPaths = darwinExtractor.getDesktopCookiesPaths()
216
+ const browserPaths = darwinExtractor.getBrowserCookiesPaths()
217
+
218
+ expect(paths.length).toBe(desktopPaths.length + browserPaths.length)
219
+ expect(paths.slice(desktopPaths.length)).toEqual(browserPaths)
220
+ })
221
+
222
+ test('returns linux paths with desktop first then browser paths', () => {
223
+ const linuxExtractor = new TeamsTokenExtractor('linux')
224
+ const paths = linuxExtractor.getTeamsCookiesPaths()
225
+ const desktopPaths = linuxExtractor.getDesktopCookiesPaths()
226
+
227
+ expect(paths.slice(0, 1)).toEqual([
228
+ {
229
+ path: join(homedir(), '.config', 'Microsoft', 'Microsoft Teams', 'Cookies'),
230
+ accountType: 'work',
231
+ },
232
+ ])
233
+ expect(paths.length).toBeGreaterThan(desktopPaths.length)
234
+ })
235
+
236
+ test('returns win32 paths with desktop first then browser paths', () => {
237
+ const winExtractor = new TeamsTokenExtractor('win32')
238
+ const paths = winExtractor.getTeamsCookiesPaths()
239
+ const desktopPaths = winExtractor.getDesktopCookiesPaths()
240
+
241
+ expect(paths.slice(0, desktopPaths.length)).toEqual(desktopPaths)
242
+ expect(paths.length).toBeGreaterThan(desktopPaths.length)
243
+ })
244
+
146
245
  test('returns empty array for unsupported platform', () => {
147
246
  const unsupportedExtractor = new TeamsTokenExtractor('freebsd' as NodeJS.Platform)
148
247
  const paths = unsupportedExtractor.getTeamsCookiesPaths()
@@ -176,18 +275,38 @@ describe('TeamsTokenExtractor', () => {
176
275
  })
177
276
 
178
277
  describe('getKeychainVariants', () => {
179
- test('returns keychain variants for macOS', () => {
278
+ test('includes Teams-specific keychain entries', () => {
180
279
  const macExtractor = new TeamsTokenExtractor('darwin')
280
+ const variants = macExtractor.getKeychainVariants()
181
281
 
182
- expect(macExtractor.getKeychainVariants()).toEqual([
183
- { service: 'Microsoft Teams Safe Storage', account: 'Microsoft Teams' },
184
- {
185
- service: 'Microsoft Teams (work or school) Safe Storage',
186
- account: 'Microsoft Teams (work or school)',
187
- },
188
- { service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
189
- { service: 'Teams Safe Storage', account: 'Teams' },
190
- ])
282
+ expect(variants).toContainEqual({ service: 'Microsoft Teams Safe Storage', account: 'Microsoft Teams' })
283
+ expect(variants).toContainEqual({
284
+ service: 'Microsoft Teams (work or school) Safe Storage',
285
+ account: 'Microsoft Teams (work or school)',
286
+ })
287
+ expect(variants).toContainEqual({ service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' })
288
+ expect(variants).toContainEqual({ service: 'Teams Safe Storage', account: 'Teams' })
289
+ })
290
+
291
+ test('includes browser keychain entries appended after Teams entries', () => {
292
+ const macExtractor = new TeamsTokenExtractor('darwin')
293
+ const variants = macExtractor.getKeychainVariants()
294
+
295
+ expect(variants).toContainEqual({ service: 'Chrome Safe Storage', account: 'Chrome' })
296
+ expect(variants).toContainEqual({ service: 'Chrome Canary Safe Storage', account: 'Chrome Canary' })
297
+ expect(variants).toContainEqual({ service: 'Arc Safe Storage', account: 'Arc' })
298
+ expect(variants).toContainEqual({ service: 'Brave Safe Storage', account: 'Brave' })
299
+ expect(variants).toContainEqual({ service: 'Vivaldi Safe Storage', account: 'Vivaldi' })
300
+ expect(variants).toContainEqual({ service: 'Chromium Safe Storage', account: 'Chromium' })
301
+ })
302
+
303
+ test('Teams entries come before browser entries', () => {
304
+ const macExtractor = new TeamsTokenExtractor('darwin')
305
+ const variants = macExtractor.getKeychainVariants()
306
+
307
+ const teamsIdx = variants.findIndex((v) => v.service === 'Microsoft Teams Safe Storage')
308
+ const chromeIdx = variants.findIndex((v) => v.service === 'Chrome Safe Storage')
309
+ expect(teamsIdx).toBeLessThan(chromeIdx)
191
310
  })
192
311
  })
193
312
 
@@ -1,6 +1,6 @@
1
1
  import { execSync } from 'node:child_process'
2
2
  import { createDecipheriv, pbkdf2Sync } from 'node:crypto'
3
- import { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs'
3
+ import { copyFileSync, existsSync, readFileSync, readdirSync, unlinkSync } 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,6 +26,13 @@ interface KeychainVariant {
26
26
  account: string
27
27
  }
28
28
 
29
+ interface BrowserConfig {
30
+ name: string
31
+ darwin: string
32
+ linux: string
33
+ win32: string
34
+ }
35
+
29
36
  const TEAMS_PROCESS_NAMES: Record<string, string> = {
30
37
  darwin: 'Microsoft Teams',
31
38
  win32: 'Teams.exe',
@@ -41,6 +48,51 @@ const TEAMS_HOST_PATTERNS = [
41
48
  '.microsoft.com',
42
49
  ]
43
50
 
51
+ const BROWSERS: BrowserConfig[] = [
52
+ {
53
+ name: 'Chrome',
54
+ darwin: join('Google', 'Chrome'),
55
+ linux: 'google-chrome',
56
+ win32: join('Google', 'Chrome', 'User Data'),
57
+ },
58
+ {
59
+ name: 'Chrome Canary',
60
+ darwin: join('Google', 'Chrome Canary'),
61
+ linux: 'google-chrome-unstable',
62
+ win32: join('Google', 'Chrome SxS', 'User Data'),
63
+ },
64
+ {
65
+ name: 'Edge',
66
+ darwin: 'Microsoft Edge',
67
+ linux: 'microsoft-edge',
68
+ win32: join('Microsoft', 'Edge', 'User Data'),
69
+ },
70
+ {
71
+ name: 'Arc',
72
+ darwin: join('Arc', 'User Data'),
73
+ linux: '',
74
+ win32: join('Arc', 'User Data'),
75
+ },
76
+ {
77
+ name: 'Brave',
78
+ darwin: join('BraveSoftware', 'Brave-Browser'),
79
+ linux: join('BraveSoftware', 'Brave-Browser'),
80
+ win32: join('BraveSoftware', 'Brave-Browser', 'User Data'),
81
+ },
82
+ {
83
+ name: 'Vivaldi',
84
+ darwin: 'Vivaldi',
85
+ linux: 'vivaldi',
86
+ win32: join('Vivaldi', 'User Data'),
87
+ },
88
+ {
89
+ name: 'Chromium',
90
+ darwin: 'Chromium',
91
+ linux: 'chromium',
92
+ win32: join('Chromium', 'User Data'),
93
+ },
94
+ ]
95
+
44
96
  export class TeamsTokenExtractor {
45
97
  private platform: NodeJS.Platform
46
98
  private keyCache: DerivedKeyCache
@@ -51,7 +103,7 @@ export class TeamsTokenExtractor {
51
103
  this.keyCache = keyCache ?? new DerivedKeyCache()
52
104
  }
53
105
 
54
- getTeamsCookiesPaths(): TeamsCookiePath[] {
106
+ getDesktopCookiesPaths(): TeamsCookiePath[] {
55
107
  switch (this.platform) {
56
108
  case 'darwin': {
57
109
  const ebWebViewBase = join(
@@ -107,6 +159,83 @@ export class TeamsTokenExtractor {
107
159
  }
108
160
  }
109
161
 
162
+ getBrowserCookiesPaths(): TeamsCookiePath[] {
163
+ const paths: TeamsCookiePath[] = []
164
+
165
+ for (const browser of BROWSERS) {
166
+ const browserBase = this.getBrowserBasePath(browser)
167
+ if (!browserBase) continue
168
+
169
+ const profileDirs = this.discoverProfileDirs(browserBase)
170
+ for (const profileDir of profileDirs) {
171
+ paths.push({ path: join(profileDir, 'Cookies'), accountType: 'work' })
172
+ paths.push({ path: join(profileDir, 'Network', 'Cookies'), accountType: 'work' })
173
+ }
174
+ }
175
+
176
+ return paths
177
+ }
178
+
179
+ getTeamsCookiesPaths(): TeamsCookiePath[] {
180
+ const desktopPaths = this.getDesktopCookiesPaths()
181
+ const browserPaths = this.getBrowserCookiesPaths()
182
+ return [...desktopPaths, ...browserPaths]
183
+ }
184
+
185
+ private getBrowserBasePath(browser: BrowserConfig): string | null {
186
+ let relative: string
187
+
188
+ switch (this.platform) {
189
+ case 'darwin':
190
+ relative = browser.darwin
191
+ if (!relative) return null
192
+ return join(homedir(), 'Library', 'Application Support', relative)
193
+ case 'linux':
194
+ relative = browser.linux
195
+ if (!relative) return null
196
+ return join(homedir(), '.config', relative)
197
+ case 'win32':
198
+ relative = browser.win32
199
+ if (!relative) return null
200
+ return join(
201
+ process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'),
202
+ relative,
203
+ )
204
+ default:
205
+ return null
206
+ }
207
+ }
208
+
209
+ private discoverProfileDirs(browserBase: string): string[] {
210
+ const dirs: string[] = []
211
+
212
+ dirs.push(join(browserBase, 'Default'))
213
+
214
+ if (!existsSync(browserBase)) return dirs
215
+
216
+ try {
217
+ const entries = readdirSync(browserBase, { withFileTypes: true })
218
+ for (const entry of entries) {
219
+ if (!entry.isDirectory()) continue
220
+ if (!/^Profile \d+$/i.test(entry.name)) continue
221
+ dirs.push(join(browserBase, entry.name))
222
+ }
223
+ } catch {}
224
+
225
+ return dirs
226
+ }
227
+
228
+ private findLocalStateForCookiePath(cookiePath: string): string | null {
229
+ const parts = cookiePath.split(/[/\\]/)
230
+ for (let levels = 2; levels <= 4; levels++) {
231
+ if (parts.length < levels) break
232
+ const base = parts.slice(0, parts.length - levels).join('/')
233
+ const candidate = join(base, 'Local State')
234
+ if (existsSync(candidate)) return candidate
235
+ }
236
+ return null
237
+ }
238
+
110
239
  getLocalStatePath(): string {
111
240
  switch (this.platform) {
112
241
  case 'darwin':
@@ -136,17 +265,21 @@ export class TeamsTokenExtractor {
136
265
 
137
266
  getKeychainVariants(): KeychainVariant[] {
138
267
  return [
139
- // New Teams (com.microsoft.teams2) keychain entry - try first
268
+ // Teams-specific keychain entries
140
269
  { service: 'Microsoft Teams Safe Storage', account: 'Microsoft Teams' },
141
- // Work/school variant
142
270
  {
143
271
  service: 'Microsoft Teams (work or school) Safe Storage',
144
272
  account: 'Microsoft Teams (work or school)',
145
273
  },
146
- // Edge WebView2 fallback
147
274
  { service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
148
- // Classic Teams fallback
149
275
  { service: 'Teams Safe Storage', account: 'Teams' },
276
+ // Browser keychain entries for fallback
277
+ { service: 'Chrome Safe Storage', account: 'Chrome' },
278
+ { service: 'Chrome Canary Safe Storage', account: 'Chrome Canary' },
279
+ { service: 'Arc Safe Storage', account: 'Arc' },
280
+ { service: 'Brave Safe Storage', account: 'Brave' },
281
+ { service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
282
+ { service: 'Chromium Safe Storage', account: 'Chromium' },
150
283
  ]
151
284
  }
152
285
 
@@ -206,7 +339,13 @@ export class TeamsTokenExtractor {
206
339
 
207
340
  try {
208
341
  this.copyDatabaseToTemp(dbPath, tempPath)
209
- const token = await this.extractFromSQLite(tempPath)
342
+ // For Windows: find the Local State relative to the cookie path so browser cookies
343
+ // use the browser's own Local State instead of the Teams app Local State.
344
+ const localStatePath =
345
+ this.platform === 'win32'
346
+ ? (this.findLocalStateForCookiePath(dbPath) ?? this.getLocalStatePath())
347
+ : undefined
348
+ const token = await this.extractFromSQLite(tempPath, localStatePath)
210
349
  this.cleanupTempFile(tempPath)
211
350
  return token
212
351
  } catch {
@@ -230,7 +369,7 @@ export class TeamsTokenExtractor {
230
369
  }
231
370
  }
232
371
 
233
- private async extractFromSQLite(dbPath: string): Promise<string | null> {
372
+ private async extractFromSQLite(dbPath: string, localStatePath?: string): Promise<string | null> {
234
373
  try {
235
374
  for (const hostPattern of TEAMS_HOST_PATTERNS) {
236
375
  const sql = `
@@ -257,7 +396,7 @@ export class TeamsTokenExtractor {
257
396
  }
258
397
 
259
398
  if (row?.encrypted_value) {
260
- const decrypted = this.decryptCookie(Buffer.from(row.encrypted_value))
399
+ const decrypted = this.decryptCookie(Buffer.from(row.encrypted_value), localStatePath)
261
400
  if (decrypted && this.isValidSkypeToken(decrypted)) {
262
401
  return decrypted
263
402
  }
@@ -270,14 +409,14 @@ export class TeamsTokenExtractor {
270
409
  }
271
410
  }
272
411
 
273
- private decryptCookie(encryptedValue: Buffer): string | null {
412
+ private decryptCookie(encryptedValue: Buffer, localStatePath?: string): string | null {
274
413
  if (!this.isEncryptedValue(encryptedValue)) {
275
414
  // Not encrypted, return as-is
276
415
  return encryptedValue.toString('utf8')
277
416
  }
278
417
 
279
418
  if (this.platform === 'win32') {
280
- return this.decryptWindowsCookie(encryptedValue)
419
+ return this.decryptWindowsCookie(encryptedValue, localStatePath)
281
420
  } else if (this.platform === 'darwin') {
282
421
  return this.decryptMacCookie(encryptedValue)
283
422
  } else if (this.platform === 'linux') {
@@ -287,12 +426,12 @@ export class TeamsTokenExtractor {
287
426
  return null
288
427
  }
289
428
 
290
- private decryptWindowsCookie(encryptedData: Buffer): string | null {
429
+ private decryptWindowsCookie(encryptedData: Buffer, localStatePath?: string): string | null {
291
430
  try {
292
- const localStatePath = this.getLocalStatePath()
293
- if (!existsSync(localStatePath)) return null
431
+ const statePath = localStatePath ?? this.getLocalStatePath()
432
+ if (!existsSync(statePath)) return null
294
433
 
295
- const localState = JSON.parse(readFileSync(localStatePath, 'utf8'))
434
+ const localState = JSON.parse(readFileSync(statePath, 'utf8'))
296
435
  const encryptedKey = Buffer.from(localState.os_crypt.encrypted_key, 'base64')
297
436
 
298
437
  // Remove DPAPI prefix (5 bytes)
@@ -329,16 +468,20 @@ export class TeamsTokenExtractor {
329
468
  if (decrypted) return decrypted
330
469
  }
331
470
 
332
- const password = this.getKeychainPassword()
333
- if (!password) return null
334
-
335
- const key = pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
336
- const decrypted = this.decryptAESCBC(encryptedData, key)
337
- if (decrypted) {
338
- this.cachedKey = key
339
- this.keyCache.set('teams', key).catch(() => {})
471
+ for (const variant of this.getKeychainVariants()) {
472
+ const password = this.execSecurityCommand(variant.service, variant.account)
473
+ if (!password) continue
474
+
475
+ const key = pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
476
+ const decrypted = this.decryptAESCBC(encryptedData, key)
477
+ if (decrypted) {
478
+ this.cachedKey = key
479
+ this.keyCache.set('teams', key).catch(() => {})
480
+ return decrypted
481
+ }
340
482
  }
341
- return decrypted
483
+
484
+ return null
342
485
  }
343
486
 
344
487
  private decryptLinuxCookie(encryptedData: Buffer): string | null {
@@ -348,13 +491,10 @@ export class TeamsTokenExtractor {
348
491
  }
349
492
 
350
493
  private getKeychainPassword(): string | null {
351
- const variants = this.getKeychainVariants()
352
-
353
- for (const variant of variants) {
494
+ for (const variant of this.getKeychainVariants()) {
354
495
  const password = this.execSecurityCommand(variant.service, variant.account)
355
496
  if (password) return password
356
497
  }
357
-
358
498
  return null
359
499
  }
360
500
 
@@ -363,9 +503,10 @@ export class TeamsTokenExtractor {
363
503
  // Escape double quotes in service/account to prevent command injection
364
504
  const safeService = service.replace(/"/g, '\\"')
365
505
  const safeAccount = account.replace(/"/g, '\\"')
366
- const result = execSync(`security find-generic-password -s "${safeService}" -a "${safeAccount}" -w 2>/dev/null`, {
367
- encoding: 'utf8',
368
- })
506
+ const result = execSync(
507
+ `security find-generic-password -s "${safeService}" -a "${safeAccount}" -w 2>/dev/null`,
508
+ { encoding: 'utf8' },
509
+ )
369
510
  return result.trim()
370
511
  } catch {
371
512
  return null
@@ -381,6 +522,16 @@ export class TeamsTokenExtractor {
381
522
  decipher.setAutoPadding(true)
382
523
 
383
524
  const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
525
+
526
+ // Chromium v130+ prepends a 32-byte integrity hash before the actual cookie value.
527
+ // Detect by checking if the first bytes contain non-printable characters.
528
+ if (decrypted.length > 32) {
529
+ const hasNonPrintablePrefix = decrypted.subarray(0, 32).some((b) => b < 0x20 || b > 0x7e)
530
+ if (hasNonPrintablePrefix) {
531
+ return decrypted.subarray(32).toString('utf8')
532
+ }
533
+ }
534
+
384
535
  const decryptedStr = decrypted.toString('utf8')
385
536
 
386
537
  // Chromium v24+ prepends a 32-byte integrity hash before the actual value