agent-messenger 2.1.0 → 2.3.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 (217) 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 +31 -7
  5. package/dist/package.json +5 -3
  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/client.d.ts.map +1 -1
  52. package/dist/src/platforms/line/client.js +36 -9
  53. package/dist/src/platforms/line/client.js.map +1 -1
  54. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  55. package/dist/src/platforms/line/commands/auth.js +6 -5
  56. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  57. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  58. package/dist/src/platforms/slack/commands/auth.js +11 -10
  59. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  60. package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
  61. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  62. package/dist/src/platforms/slack/token-extractor.js +300 -23
  63. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  64. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  65. package/dist/src/platforms/teams/commands/auth.js +9 -8
  66. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  67. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  68. package/dist/src/platforms/teams/ensure-auth.js +2 -1
  69. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  70. package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
  71. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  72. package/dist/src/platforms/teams/token-extractor.js +161 -29
  73. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  74. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  75. package/dist/src/platforms/telegram/client.js +25 -7
  76. package/dist/src/platforms/telegram/client.js.map +1 -1
  77. package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
  78. package/dist/src/platforms/telegram/commands/auth.js +6 -5
  79. package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
  80. package/dist/src/platforms/webex/client.d.ts +12 -0
  81. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  82. package/dist/src/platforms/webex/client.js +168 -1
  83. package/dist/src/platforms/webex/client.js.map +1 -1
  84. package/dist/src/platforms/webex/commands/auth.d.ts +4 -0
  85. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  86. package/dist/src/platforms/webex/commands/auth.js +50 -4
  87. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  88. package/dist/src/platforms/webex/credential-manager.js +1 -1
  89. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  90. package/dist/src/platforms/webex/encryption.d.ts +10 -0
  91. package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
  92. package/dist/src/platforms/webex/encryption.js +49 -0
  93. package/dist/src/platforms/webex/encryption.js.map +1 -0
  94. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  95. package/dist/src/platforms/webex/ensure-auth.js +25 -5
  96. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  97. package/dist/src/platforms/webex/index.d.ts +2 -0
  98. package/dist/src/platforms/webex/index.d.ts.map +1 -1
  99. package/dist/src/platforms/webex/index.js +1 -0
  100. package/dist/src/platforms/webex/index.js.map +1 -1
  101. package/dist/src/platforms/webex/token-extractor.d.ts +29 -0
  102. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
  103. package/dist/src/platforms/webex/token-extractor.js +393 -0
  104. package/dist/src/platforms/webex/token-extractor.js.map +1 -0
  105. package/dist/src/platforms/webex/types.d.ts +8 -1
  106. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  107. package/dist/src/platforms/webex/types.js +4 -1
  108. package/dist/src/platforms/webex/types.js.map +1 -1
  109. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  110. package/dist/src/platforms/whatsapp/client.js +6 -2
  111. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  112. package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
  113. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  114. package/dist/src/shared/utils/error-handler.d.ts +1 -1
  115. package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
  116. package/dist/src/shared/utils/error-handler.js +3 -2
  117. package/dist/src/shared/utils/error-handler.js.map +1 -1
  118. package/dist/src/shared/utils/stderr.d.ts +5 -0
  119. package/dist/src/shared/utils/stderr.d.ts.map +1 -0
  120. package/dist/src/shared/utils/stderr.js +18 -0
  121. package/dist/src/shared/utils/stderr.js.map +1 -0
  122. package/docs/content/docs/cli/channeltalk.mdx +7 -7
  123. package/docs/content/docs/cli/discord.mdx +3 -3
  124. package/docs/content/docs/cli/instagram.mdx +28 -6
  125. package/docs/content/docs/cli/slack.mdx +2 -2
  126. package/docs/content/docs/cli/teams.mdx +6 -4
  127. package/docs/content/docs/cli/webex.mdx +32 -11
  128. package/e2e/README.md +132 -8
  129. package/e2e/channeltalk.e2e.test.ts +2 -7
  130. package/e2e/channeltalkbot.e2e.test.ts +2 -6
  131. package/e2e/config.ts +172 -10
  132. package/e2e/helpers.ts +7 -0
  133. package/e2e/instagram.e2e.test.ts +97 -0
  134. package/e2e/kakaotalk.e2e.test.ts +74 -0
  135. package/e2e/line.e2e.test.ts +92 -0
  136. package/e2e/teams.e2e.test.ts +46 -1
  137. package/e2e/telegram.e2e.test.ts +84 -0
  138. package/e2e/webex.e2e.test.ts +190 -0
  139. package/e2e/whatsapp.e2e.test.ts +90 -0
  140. package/e2e/whatsappbot.e2e.test.ts +78 -0
  141. package/package.json +5 -3
  142. package/skills/agent-channeltalk/SKILL.md +9 -9
  143. package/skills/agent-channeltalk/references/authentication.md +21 -18
  144. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  145. package/skills/agent-discord/SKILL.md +5 -5
  146. package/skills/agent-discord/references/authentication.md +8 -8
  147. package/skills/agent-discordbot/SKILL.md +1 -1
  148. package/skills/agent-instagram/SKILL.md +51 -9
  149. package/skills/agent-instagram/references/authentication.md +35 -3
  150. package/skills/agent-kakaotalk/SKILL.md +1 -1
  151. package/skills/agent-line/SKILL.md +1 -1
  152. package/skills/agent-slack/SKILL.md +5 -5
  153. package/skills/agent-slack/references/authentication.md +8 -8
  154. package/skills/agent-slackbot/SKILL.md +1 -1
  155. package/skills/agent-teams/SKILL.md +6 -6
  156. package/skills/agent-teams/references/authentication.md +8 -8
  157. package/skills/agent-telegram/SKILL.md +1 -1
  158. package/skills/agent-webex/SKILL.md +35 -15
  159. package/skills/agent-webex/references/authentication.md +63 -9
  160. package/skills/agent-webex/references/common-patterns.md +6 -3
  161. package/skills/agent-whatsapp/SKILL.md +1 -1
  162. package/skills/agent-whatsappbot/SKILL.md +1 -1
  163. package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
  164. package/src/platforms/channeltalk/commands/auth.ts +38 -32
  165. package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
  166. package/src/platforms/channeltalk/ensure-auth.ts +6 -6
  167. package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
  168. package/src/platforms/channeltalk/token-extractor.ts +344 -30
  169. package/src/platforms/discord/commands/auth.test.ts +3 -3
  170. package/src/platforms/discord/commands/auth.ts +58 -54
  171. package/src/platforms/discord/ensure-auth.test.ts +3 -3
  172. package/src/platforms/discord/ensure-auth.ts +3 -3
  173. package/src/platforms/discord/token-extractor.test.ts +199 -27
  174. package/src/platforms/discord/token-extractor.ts +190 -17
  175. package/src/platforms/instagram/client.ts +2 -2
  176. package/src/platforms/instagram/commands/auth.ts +133 -14
  177. package/src/platforms/instagram/ensure-auth.ts +63 -12
  178. package/src/platforms/instagram/index.ts +1 -0
  179. package/src/platforms/instagram/token-extractor.test.ts +424 -0
  180. package/src/platforms/instagram/token-extractor.ts +478 -0
  181. package/src/platforms/kakaotalk/client.ts +3 -1
  182. package/src/platforms/kakaotalk/commands/auth.ts +14 -13
  183. package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
  184. package/src/platforms/line/client.ts +39 -14
  185. package/src/platforms/line/commands/auth.ts +7 -6
  186. package/src/platforms/slack/cli.test.ts +6 -5
  187. package/src/platforms/slack/commands/auth.test.ts +11 -7
  188. package/src/platforms/slack/commands/auth.ts +11 -10
  189. package/src/platforms/slack/token-extractor.test.ts +98 -1
  190. package/src/platforms/slack/token-extractor.ts +338 -26
  191. package/src/platforms/teams/commands/auth.ts +9 -8
  192. package/src/platforms/teams/ensure-auth.ts +3 -1
  193. package/src/platforms/teams/token-extractor.test.ts +136 -17
  194. package/src/platforms/teams/token-extractor.ts +182 -31
  195. package/src/platforms/telegram/client.test.ts +134 -0
  196. package/src/platforms/telegram/client.ts +27 -6
  197. package/src/platforms/telegram/commands/auth.ts +6 -5
  198. package/src/platforms/webex/client.test.ts +314 -0
  199. package/src/platforms/webex/client.ts +231 -1
  200. package/src/platforms/webex/commands/auth.ts +71 -4
  201. package/src/platforms/webex/commands/member.test.ts +10 -1
  202. package/src/platforms/webex/commands/message.test.ts +9 -5
  203. package/src/platforms/webex/commands/snapshot.test.ts +13 -4
  204. package/src/platforms/webex/commands/space.test.ts +12 -2
  205. package/src/platforms/webex/credential-manager.ts +1 -1
  206. package/src/platforms/webex/encryption.ts +53 -0
  207. package/src/platforms/webex/ensure-auth.test.ts +4 -0
  208. package/src/platforms/webex/ensure-auth.ts +27 -4
  209. package/src/platforms/webex/index.ts +2 -0
  210. package/src/platforms/webex/token-extractor.test.ts +327 -0
  211. package/src/platforms/webex/token-extractor.ts +460 -0
  212. package/src/platforms/webex/types.ts +8 -2
  213. package/src/platforms/webex/typings/node-jose.d.ts +27 -0
  214. package/src/platforms/whatsapp/client.ts +11 -7
  215. package/src/shared/utils/derived-key-cache.ts +1 -1
  216. package/src/shared/utils/error-handler.ts +4 -2
  217. package/src/shared/utils/stderr.ts +22 -0
@@ -1,18 +1,41 @@
1
1
  import { WebexClient } from './client'
2
2
  import { WebexCredentialManager } from './credential-manager'
3
+ import { WebexTokenExtractor } from './token-extractor'
3
4
 
4
5
  export async function ensureWebexAuth(): Promise<void> {
5
6
  try {
6
7
  const credManager = new WebexCredentialManager()
7
8
  const config = await credManager.loadConfig()
8
- if (!config) return
9
9
 
10
- const token = await credManager.getToken(config.clientId, config.clientSecret)
11
- if (!token) return
10
+ if (config) {
11
+ const token = await credManager.getToken(config.clientId, config.clientSecret)
12
+ if (token) {
13
+ const client = new WebexClient()
14
+ await client.login({ token })
15
+ await client.testAuth()
16
+ return
17
+ }
18
+ }
19
+
20
+ const extractor = new WebexTokenExtractor()
21
+ const extracted = await extractor.extract()
22
+ if (!extracted) return
12
23
 
13
24
  const client = new WebexClient()
14
- await client.login({ token })
25
+ await client.login({ token: extracted.accessToken })
15
26
  await client.testAuth()
27
+
28
+ await credManager.saveConfig({
29
+ accessToken: extracted.accessToken,
30
+ refreshToken: extracted.refreshToken ?? '',
31
+ expiresAt: extracted.expiresAt ?? 0,
32
+ tokenType: 'extracted',
33
+ deviceUrl: extracted.deviceUrl,
34
+ userId: extracted.userId,
35
+ encryptionKeys: extracted.encryptionKeys
36
+ ? Object.fromEntries(extracted.encryptionKeys)
37
+ : undefined,
38
+ })
16
39
  } catch {
17
40
  // Intentionally silent — best-effort preflight that should not block commands
18
41
  }
@@ -1,5 +1,7 @@
1
1
  export { WebexClient } from './client'
2
2
  export { WebexCredentialManager } from './credential-manager'
3
+ export { WebexTokenExtractor } from './token-extractor'
4
+ export type { ExtractedWebexToken } from './token-extractor'
3
5
  export { WebexError } from './types'
4
6
  export type {
5
7
  WebexConfig,
@@ -0,0 +1,327 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ import { WebexTokenExtractor } from './token-extractor'
7
+
8
+ function makeWebexStorageJson(overrides?: {
9
+ accessToken?: string
10
+ refreshToken?: string
11
+ expires?: number
12
+ deviceUrl?: string
13
+ }): string {
14
+ return JSON.stringify({
15
+ Credentials: {
16
+ '@': {
17
+ supertoken: {
18
+ access_token: overrides?.accessToken ?? 'ZDI3MGEyYzQtNmFlNS00NDNh_PF84_1eb65fdf-userId_orgId',
19
+ token_type: 'Bearer',
20
+ expires: overrides?.expires ?? Date.now() + 3600000,
21
+ refresh_token: overrides?.refreshToken ?? 'MDEyMzQ1Njc4OTAxMjM0NTY3_refresh',
22
+ scope: 'spark:all',
23
+ },
24
+ },
25
+ },
26
+ Device: {
27
+ '@': {
28
+ url: overrides?.deviceUrl ?? 'https://wdm-r.wbx2.com/wdm/api/v1/devices/test-device-id',
29
+ },
30
+ },
31
+ })
32
+ }
33
+
34
+ function createLevelDBDir(base: string, profileName: string = 'Default'): string {
35
+ const leveldbDir = join(base, profileName, 'Local Storage', 'leveldb')
36
+ mkdirSync(leveldbDir, { recursive: true })
37
+ return leveldbDir
38
+ }
39
+
40
+ describe('WebexTokenExtractor', () => {
41
+ let tempDir: string
42
+
43
+ beforeEach(() => {
44
+ tempDir = mkdtempSync(join(tmpdir(), 'webex-extract-test-'))
45
+ })
46
+
47
+ afterEach(() => {
48
+ rmSync(tempDir, { recursive: true, force: true })
49
+ })
50
+
51
+ describe('getBrowserProfileDirs', () => {
52
+ test('returns correct paths for darwin', () => {
53
+ const extractor = new WebexTokenExtractor('darwin')
54
+ const dirs = extractor.getBrowserProfileDirs()
55
+
56
+ if (dirs.length > 0) {
57
+ for (const dir of dirs) {
58
+ expect(dir).toContain('Local Storage/leveldb')
59
+ }
60
+ }
61
+ })
62
+
63
+ test('returns empty array for unsupported platform', () => {
64
+ const extractor = new WebexTokenExtractor('freebsd' as NodeJS.Platform)
65
+ expect(extractor.getBrowserProfileDirs()).toEqual([])
66
+ })
67
+
68
+ test('discovers Default and Profile N directories', () => {
69
+ const defaultDir = createLevelDBDir(tempDir, 'Default')
70
+ const profile1Dir = createLevelDBDir(tempDir, 'Profile 1')
71
+ const profile2Dir = createLevelDBDir(tempDir, 'Profile 2')
72
+
73
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
74
+ const dirs = extractor.getBrowserProfileDirs()
75
+
76
+ expect(dirs).toContain(defaultDir)
77
+ expect(dirs).toContain(profile1Dir)
78
+ expect(dirs).toContain(profile2Dir)
79
+ })
80
+
81
+ test('skips non-profile directories', () => {
82
+ createLevelDBDir(tempDir, 'Default')
83
+ mkdirSync(join(tempDir, 'Extensions', 'Local Storage', 'leveldb'), { recursive: true })
84
+ mkdirSync(join(tempDir, 'Crashpad'), { recursive: true })
85
+
86
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
87
+ const dirs = extractor.getBrowserProfileDirs()
88
+
89
+ expect(dirs).toHaveLength(1)
90
+ expect(dirs[0]).toContain('Default')
91
+ })
92
+
93
+ test('returns empty when base dir does not exist', () => {
94
+ const extractor = new WebexTokenExtractor('darwin', undefined, join(tempDir, 'nonexistent'))
95
+ expect(extractor.getBrowserProfileDirs()).toEqual([])
96
+ })
97
+ })
98
+
99
+ describe('extract', () => {
100
+ test('finds token from .log file', async () => {
101
+ const leveldbDir = createLevelDBDir(tempDir)
102
+ const webexJson = makeWebexStorageJson()
103
+ const entry = `_https://web.webex.com\x00webex-storage${webexJson}`
104
+ writeFileSync(join(leveldbDir, '000003.log'), entry)
105
+
106
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
107
+ const result = await extractor.extract()
108
+
109
+ expect(result).not.toBeNull()
110
+ expect(result!.accessToken).toBe('ZDI3MGEyYzQtNmFlNS00NDNh_PF84_1eb65fdf-userId_orgId')
111
+ expect(result!.refreshToken).toBe('MDEyMzQ1Njc4OTAxMjM0NTY3_refresh')
112
+ expect(result!.expiresAt).toBeGreaterThan(Date.now())
113
+ expect(result!.deviceUrl).toBe('https://wdm-r.wbx2.com/wdm/api/v1/devices/test-device-id')
114
+ })
115
+
116
+ test('finds token from .ldb file', async () => {
117
+ const leveldbDir = createLevelDBDir(tempDir)
118
+ const webexJson = makeWebexStorageJson()
119
+ writeFileSync(join(leveldbDir, '000005.ldb'), `\x00\x00${webexJson}\x00\x00`)
120
+
121
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
122
+ const result = await extractor.extract()
123
+
124
+ expect(result).not.toBeNull()
125
+ expect(result!.accessToken).toBe('ZDI3MGEyYzQtNmFlNS00NDNh_PF84_1eb65fdf-userId_orgId')
126
+ })
127
+
128
+ test('returns null when no browser dirs exist', async () => {
129
+ const extractor = new WebexTokenExtractor('darwin', undefined, join(tempDir, 'nonexistent'))
130
+ expect(await extractor.extract()).toBeNull()
131
+ })
132
+
133
+ test('returns null when LevelDB has no webex-storage data', async () => {
134
+ const leveldbDir = createLevelDBDir(tempDir)
135
+ writeFileSync(join(leveldbDir, '000003.log'), 'some random data without webex tokens')
136
+
137
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
138
+ expect(await extractor.extract()).toBeNull()
139
+ })
140
+
141
+ test('handles malformed JSON gracefully', async () => {
142
+ const leveldbDir = createLevelDBDir(tempDir)
143
+ writeFileSync(join(leveldbDir, '000003.log'), '{"Credentials": {"@": {"supertoken": {broken json')
144
+
145
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
146
+ expect(await extractor.extract()).toBeNull()
147
+ })
148
+
149
+ test('extracts all fields from nested Credentials structure', async () => {
150
+ const expires = Date.now() + 7200000
151
+ const leveldbDir = createLevelDBDir(tempDir)
152
+ const webexJson = makeWebexStorageJson({
153
+ accessToken: 'my-long-access-token-that-is-at-least-20-chars',
154
+ refreshToken: 'my-refresh-token-value',
155
+ expires,
156
+ deviceUrl: 'https://wdm-a.wbx2.com/wdm/api/v1/devices/abc-123',
157
+ })
158
+ writeFileSync(join(leveldbDir, '000003.log'), webexJson)
159
+
160
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
161
+ const result = await extractor.extract()
162
+
163
+ expect(result).not.toBeNull()
164
+ expect(result!.accessToken).toBe('my-long-access-token-that-is-at-least-20-chars')
165
+ expect(result!.refreshToken).toBe('my-refresh-token-value')
166
+ expect(result!.expiresAt).toBe(expires)
167
+ expect(result!.deviceUrl).toBe('https://wdm-a.wbx2.com/wdm/api/v1/devices/abc-123')
168
+ })
169
+
170
+ test('extracts deviceUrl when Device field is present', async () => {
171
+ const leveldbDir = createLevelDBDir(tempDir)
172
+ const webexJson = makeWebexStorageJson({
173
+ deviceUrl: 'https://wdm-eu.wbx2.com/wdm/api/v1/devices/eu-device-id',
174
+ })
175
+ writeFileSync(join(leveldbDir, '000003.log'), webexJson)
176
+
177
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
178
+ const result = await extractor.extract()
179
+
180
+ expect(result).not.toBeNull()
181
+ expect(result!.deviceUrl).toBe('https://wdm-eu.wbx2.com/wdm/api/v1/devices/eu-device-id')
182
+ })
183
+
184
+ test('returns token without deviceUrl when Device field is absent', async () => {
185
+ const leveldbDir = createLevelDBDir(tempDir)
186
+ const webexJson = JSON.stringify({
187
+ Credentials: {
188
+ '@': {
189
+ supertoken: {
190
+ access_token: 'token-without-device-url-at-least-twenty',
191
+ token_type: 'Bearer',
192
+ expires: Date.now() + 3600000,
193
+ },
194
+ },
195
+ },
196
+ })
197
+ writeFileSync(join(leveldbDir, '000003.log'), webexJson)
198
+
199
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
200
+ const result = await extractor.extract()
201
+
202
+ expect(result).not.toBeNull()
203
+ expect(result!.accessToken).toBe('token-without-device-url-at-least-twenty')
204
+ expect(result!.deviceUrl).toBeUndefined()
205
+ })
206
+
207
+ test('skips profiles without leveldb directory', async () => {
208
+ mkdirSync(join(tempDir, 'Default', 'Local Storage'), { recursive: true })
209
+ const profile1Dir = createLevelDBDir(tempDir, 'Profile 1')
210
+ const webexJson = makeWebexStorageJson()
211
+ writeFileSync(join(profile1Dir, '000003.log'), webexJson)
212
+
213
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
214
+ const result = await extractor.extract()
215
+
216
+ expect(result).not.toBeNull()
217
+ })
218
+
219
+ test('returns first valid token and stops scanning', async () => {
220
+ const dir1 = createLevelDBDir(tempDir, 'Default')
221
+ const dir2 = createLevelDBDir(tempDir, 'Profile 1')
222
+
223
+ const token1 = makeWebexStorageJson({ accessToken: 'first-valid-token-longer-than-twenty-chars' })
224
+ const token2 = makeWebexStorageJson({ accessToken: 'second-valid-token-longer-than-twenty-chars' })
225
+
226
+ writeFileSync(join(dir1, '000003.log'), token1)
227
+ writeFileSync(join(dir2, '000003.log'), token2)
228
+
229
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
230
+ const result = await extractor.extract()
231
+
232
+ expect(result!.accessToken).toBe('first-valid-token-longer-than-twenty-chars')
233
+ })
234
+
235
+ test('prefers .log files over .ldb files in same directory', async () => {
236
+ const leveldbDir = createLevelDBDir(tempDir)
237
+
238
+ const logToken = makeWebexStorageJson({ accessToken: 'from-log-file-token-at-least-twenty-chars' })
239
+ const ldbToken = makeWebexStorageJson({ accessToken: 'from-ldb-file-token-at-least-twenty-chars' })
240
+
241
+ writeFileSync(join(leveldbDir, '000003.log'), logToken)
242
+ writeFileSync(join(leveldbDir, '000005.ldb'), ldbToken)
243
+
244
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
245
+ const result = await extractor.extract()
246
+
247
+ expect(result!.accessToken).toBe('from-log-file-token-at-least-twenty-chars')
248
+ })
249
+
250
+ test('rejects tokens shorter than 20 characters', async () => {
251
+ const leveldbDir = createLevelDBDir(tempDir)
252
+ const webexJson = makeWebexStorageJson({ accessToken: 'short' })
253
+ writeFileSync(join(leveldbDir, '000003.log'), webexJson)
254
+
255
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
256
+ expect(await extractor.extract()).toBeNull()
257
+ })
258
+
259
+ test('handles binary framing around JSON', async () => {
260
+ const leveldbDir = createLevelDBDir(tempDir)
261
+ const webexJson = makeWebexStorageJson()
262
+ const binaryFrame = Buffer.concat([
263
+ Buffer.from([0x01, 0x02, 0x03, 0xff, 0xfe]),
264
+ Buffer.from(`_https://web.webex.com\x00webex-storage`),
265
+ Buffer.from(webexJson),
266
+ Buffer.from([0x00, 0x00, 0x01]),
267
+ ])
268
+ writeFileSync(join(leveldbDir, '000003.log'), binaryFrame)
269
+
270
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
271
+ const result = await extractor.extract()
272
+
273
+ expect(result).not.toBeNull()
274
+ expect(result!.accessToken).toBe('ZDI3MGEyYzQtNmFlNS00NDNh_PF84_1eb65fdf-userId_orgId')
275
+ })
276
+
277
+ test('handles supertoken at top level', async () => {
278
+ const leveldbDir = createLevelDBDir(tempDir)
279
+ const directJson = JSON.stringify({
280
+ supertoken: {
281
+ access_token: 'direct-supertoken-access-value-at-least-twenty',
282
+ token_type: 'Bearer',
283
+ expires: Date.now() + 3600000,
284
+ },
285
+ })
286
+ writeFileSync(join(leveldbDir, '000003.log'), directJson)
287
+
288
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
289
+ const result = await extractor.extract()
290
+
291
+ expect(result).not.toBeNull()
292
+ expect(result!.accessToken).toBe('direct-supertoken-access-value-at-least-twenty')
293
+ })
294
+
295
+ test('handles expires_in instead of expires', async () => {
296
+ const leveldbDir = createLevelDBDir(tempDir)
297
+ const now = Date.now()
298
+ const webexJson = JSON.stringify({
299
+ Credentials: {
300
+ '@': {
301
+ supertoken: {
302
+ access_token: 'token-with-expires-in-field-at-least-twenty',
303
+ token_type: 'Bearer',
304
+ expires_in: 3600,
305
+ },
306
+ },
307
+ },
308
+ })
309
+ writeFileSync(join(leveldbDir, '000003.log'), webexJson)
310
+
311
+ const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
312
+ const result = await extractor.extract()
313
+
314
+ expect(result).not.toBeNull()
315
+ expect(result!.expiresAt).toBeGreaterThanOrEqual(now + 3500000)
316
+ expect(result!.expiresAt).toBeLessThanOrEqual(now + 3700000)
317
+ })
318
+
319
+ test('calls debug log when provided', async () => {
320
+ const logs: string[] = []
321
+ const extractor = new WebexTokenExtractor('darwin', (msg) => logs.push(msg), join(tempDir, 'nonexistent'))
322
+ await extractor.extract()
323
+
324
+ expect(logs.some((l) => l.includes('No browser profile directories found'))).toBe(true)
325
+ })
326
+ })
327
+ })