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
@@ -0,0 +1,460 @@
1
+ import {
2
+ copyFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ statSync,
9
+ } from 'node:fs'
10
+ import { homedir, tmpdir } from 'node:os'
11
+ import { join } from 'node:path'
12
+
13
+ import { ClassicLevel } from 'classic-level'
14
+
15
+ export interface ExtractedWebexToken {
16
+ accessToken: string
17
+ refreshToken?: string
18
+ expiresAt?: number
19
+ deviceUrl?: string
20
+ userId?: string
21
+ encryptionKeys?: Map<string, string>
22
+ }
23
+
24
+ interface BrowserConfig {
25
+ name: string
26
+ darwin: string
27
+ linux: string
28
+ win32: string
29
+ }
30
+
31
+ const BROWSERS: BrowserConfig[] = [
32
+ {
33
+ name: 'Chrome',
34
+ darwin: join('Google', 'Chrome'),
35
+ linux: 'google-chrome',
36
+ win32: join('Google', 'Chrome', 'User Data'),
37
+ },
38
+ {
39
+ name: 'Chrome Canary',
40
+ darwin: join('Google', 'Chrome Canary'),
41
+ linux: 'google-chrome-unstable',
42
+ win32: join('Google', 'Chrome SxS', 'User Data'),
43
+ },
44
+ {
45
+ name: 'Edge',
46
+ darwin: join('Microsoft Edge'),
47
+ linux: 'microsoft-edge',
48
+ win32: join('Microsoft', 'Edge', 'User Data'),
49
+ },
50
+ {
51
+ name: 'Arc',
52
+ darwin: join('Arc', 'User Data'),
53
+ linux: '',
54
+ win32: join('Arc', 'User Data'),
55
+ },
56
+ {
57
+ name: 'Brave',
58
+ darwin: join('BraveSoftware', 'Brave-Browser'),
59
+ linux: join('BraveSoftware', 'Brave-Browser'),
60
+ win32: join('BraveSoftware', 'Brave-Browser', 'User Data'),
61
+ },
62
+ {
63
+ name: 'Vivaldi',
64
+ darwin: 'Vivaldi',
65
+ linux: 'vivaldi',
66
+ win32: join('Vivaldi', 'User Data'),
67
+ },
68
+ {
69
+ name: 'Chromium',
70
+ darwin: 'Chromium',
71
+ linux: 'chromium',
72
+ win32: join('Chromium', 'User Data'),
73
+ },
74
+ ]
75
+
76
+ const WEBEX_STORAGE_KEY = '_https://web.webex.com\x00\x01webex-web-client-bounded'
77
+
78
+ interface ScanResult {
79
+ token: ExtractedWebexToken | null
80
+ encryptionKeys: Map<string, string>
81
+ }
82
+
83
+ export class WebexTokenExtractor {
84
+ private platform: NodeJS.Platform
85
+ private baseDir: string | null
86
+ private debugLog: ((message: string) => void) | null
87
+
88
+ constructor(platform?: NodeJS.Platform, debugLog?: (message: string) => void, baseDir?: string) {
89
+ this.platform = platform ?? process.platform
90
+ this.debugLog = debugLog ?? null
91
+ this.baseDir = baseDir ?? null
92
+ }
93
+
94
+ private debug(message: string): void {
95
+ this.debugLog?.(message)
96
+ }
97
+
98
+ getBrowserProfileDirs(): string[] {
99
+ if (this.baseDir) {
100
+ return this.discoverProfileDirs(this.baseDir)
101
+ }
102
+
103
+ const dirs: string[] = []
104
+
105
+ for (const browser of BROWSERS) {
106
+ const browserBase = this.getBrowserBasePath(browser)
107
+ if (!browserBase) continue
108
+
109
+ const profileDirs = this.discoverProfileDirs(browserBase)
110
+ dirs.push(...profileDirs)
111
+ }
112
+
113
+ return dirs
114
+ }
115
+
116
+ private getBrowserBasePath(browser: BrowserConfig): string | null {
117
+ let relative: string
118
+
119
+ switch (this.platform) {
120
+ case 'darwin':
121
+ relative = browser.darwin
122
+ if (!relative) return null
123
+ return join(homedir(), 'Library', 'Application Support', relative)
124
+ case 'linux':
125
+ relative = browser.linux
126
+ if (!relative) return null
127
+ return join(homedir(), '.config', relative)
128
+ case 'win32':
129
+ relative = browser.win32
130
+ if (!relative) return null
131
+ return join(
132
+ process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'),
133
+ relative,
134
+ )
135
+ default:
136
+ return null
137
+ }
138
+ }
139
+
140
+ private discoverProfileDirs(browserBase: string): string[] {
141
+ const dirs: string[] = []
142
+
143
+ if (!existsSync(browserBase)) return dirs
144
+
145
+ const defaultLeveldb = join(browserBase, 'Default', 'Local Storage', 'leveldb')
146
+ if (existsSync(defaultLeveldb)) {
147
+ dirs.push(defaultLeveldb)
148
+ }
149
+
150
+ try {
151
+ const entries = readdirSync(browserBase, { withFileTypes: true })
152
+ for (const entry of entries) {
153
+ if (!entry.isDirectory()) continue
154
+ if (!/^Profile \d+$/i.test(entry.name)) continue
155
+
156
+ const leveldb = join(browserBase, entry.name, 'Local Storage', 'leveldb')
157
+ if (existsSync(leveldb)) {
158
+ dirs.push(leveldb)
159
+ }
160
+ }
161
+ } catch {
162
+ // Ignore read errors
163
+ }
164
+
165
+ return dirs
166
+ }
167
+
168
+ async extract(): Promise<ExtractedWebexToken | null> {
169
+ const profileDirs = this.getBrowserProfileDirs()
170
+
171
+ if (profileDirs.length === 0) {
172
+ this.debug('No browser profile directories found')
173
+ return null
174
+ }
175
+
176
+ for (const leveldbDir of profileDirs) {
177
+ this.debug(`Scanning: ${leveldbDir}`)
178
+
179
+ const result = await this.scanViaClassicLevelCopy(leveldbDir)
180
+ ?? this.scanRawFiles(leveldbDir)
181
+
182
+ if (result?.token) {
183
+ this.debug(`Found token in: ${leveldbDir}`)
184
+
185
+ const token = result.token
186
+ if (result.encryptionKeys.size > 0) {
187
+ token.encryptionKeys = result.encryptionKeys
188
+ }
189
+
190
+ return token
191
+ }
192
+ }
193
+
194
+ this.debug('No Webex tokens found in any browser profile')
195
+ return null
196
+ }
197
+
198
+ private async scanViaClassicLevelCopy(dbPath: string): Promise<ScanResult | null> {
199
+ const tempDir = join(tmpdir(), `webex-leveldb-${Date.now()}-${Math.random().toString(36).slice(2)}`)
200
+
201
+ try {
202
+ mkdirSync(tempDir, { recursive: true })
203
+
204
+ const files = readdirSync(dbPath)
205
+ for (const file of files) {
206
+ if (file === 'LOCK') continue
207
+ const src = join(dbPath, file)
208
+ try {
209
+ if (statSync(src).isFile()) {
210
+ copyFileSync(src, join(tempDir, file))
211
+ }
212
+ } catch {}
213
+ }
214
+
215
+ return await this.copyAndScanLevelDB(tempDir)
216
+ } catch {
217
+ return null
218
+ } finally {
219
+ try {
220
+ rmSync(tempDir, { recursive: true, force: true })
221
+ } catch {}
222
+ }
223
+ }
224
+
225
+ private async copyAndScanLevelDB(dbPath: string): Promise<ScanResult | null> {
226
+ let db: ClassicLevel<string, Buffer> | null = null
227
+ let token: ExtractedWebexToken | null = null
228
+ const encryptionKeys = new Map<string, string>()
229
+
230
+ try {
231
+ db = new ClassicLevel(dbPath, { keyEncoding: 'utf8', valueEncoding: 'buffer' })
232
+
233
+ for await (const [key, value] of db.iterator()) {
234
+ if (!key.includes('web.webex.com')) continue
235
+
236
+ const decoded = this.decodeLevelDBValue(value)
237
+
238
+ if (!token && (decoded.includes('"supertoken"') || decoded.includes('"Credentials"'))) {
239
+ token = this.extractTokenFromString(decoded)
240
+ }
241
+
242
+ if (decoded.includes('"Encryption"') && decoded.includes('kms://')) {
243
+ const found = this.extractEncryptionKeysFromString(decoded)
244
+ for (const [uri, keyStr] of found) {
245
+ encryptionKeys.set(uri, keyStr)
246
+ }
247
+ }
248
+ }
249
+ } catch (e) {
250
+ this.debug(`ClassicLevel failed: ${e instanceof Error ? e.message : String(e)}`)
251
+ return null
252
+ } finally {
253
+ if (db) {
254
+ try {
255
+ await db.close()
256
+ } catch {}
257
+ }
258
+ }
259
+
260
+ if (!token) return null
261
+ return { token, encryptionKeys }
262
+ }
263
+
264
+ private scanRawFiles(leveldbDir: string): ScanResult | null {
265
+ let token: ExtractedWebexToken | null = null
266
+ const encryptionKeys = new Map<string, string>()
267
+
268
+ try {
269
+ const files = readdirSync(leveldbDir)
270
+
271
+ const sorted = [...files]
272
+ .filter((f) => f.endsWith('.log') || f.endsWith('.ldb'))
273
+ .sort((a, b) => {
274
+ const aIsLog = a.endsWith('.log') ? 0 : 1
275
+ const bIsLog = b.endsWith('.log') ? 0 : 1
276
+ if (aIsLog !== bIsLog) return aIsLog - bIsLog
277
+ try {
278
+ return statSync(join(leveldbDir, b)).mtimeMs - statSync(join(leveldbDir, a)).mtimeMs
279
+ } catch {
280
+ return 0
281
+ }
282
+ })
283
+
284
+ for (const file of sorted) {
285
+ const filePath = join(leveldbDir, file)
286
+ try {
287
+ const stat = statSync(filePath)
288
+ if (stat.size > 50 * 1024 * 1024) continue
289
+
290
+ const buffer = readFileSync(filePath)
291
+ const candidates = [buffer.toString('utf8'), this.stripNullBytes(buffer)]
292
+
293
+ for (const content of candidates) {
294
+ if (!token && (content.includes('"supertoken"') || content.includes('"Credentials"'))) {
295
+ token = this.extractTokenFromString(content)
296
+ }
297
+
298
+ if (content.includes('"Encryption"') && content.includes('kms://')) {
299
+ const found = this.extractEncryptionKeysFromString(content)
300
+ for (const [uri, keyStr] of found) {
301
+ encryptionKeys.set(uri, keyStr)
302
+ }
303
+ }
304
+ }
305
+ } catch {
306
+ // Skip unreadable files
307
+ }
308
+ }
309
+ } catch {
310
+ this.debug(`Failed to read directory: ${leveldbDir}`)
311
+ }
312
+
313
+ if (!token) return null
314
+ return { token, encryptionKeys }
315
+ }
316
+
317
+ private decodeLevelDBValue(buf: Buffer): string {
318
+ if (buf.length < 2) return buf.toString('utf8')
319
+ // Chromium localStorage: 0x00 prefix = UTF-16LE, 0x01 prefix = Latin1/UTF-8
320
+ if (buf[0] === 0x00 && (buf.length - 1) % 2 === 0) {
321
+ return buf.subarray(1).toString('utf16le')
322
+ }
323
+ if (buf[0] === 0x01) {
324
+ return buf.subarray(1).toString('utf8')
325
+ }
326
+ return buf.toString('utf8')
327
+ }
328
+
329
+ private stripNullBytes(buffer: Buffer): string {
330
+ const bytes: number[] = []
331
+ for (let i = 0; i < buffer.length; i++) {
332
+ if (buffer[i] !== 0) bytes.push(buffer[i]!)
333
+ }
334
+ return Buffer.from(bytes).toString('utf8')
335
+ }
336
+
337
+ private extractTokenFromString(content: string): ExtractedWebexToken | null {
338
+ const outerObjectMarkerIdx = content.indexOf('"Credentials"')
339
+ const innermostMarkerIdx = content.indexOf('"supertoken"')
340
+
341
+ const markerIdx =
342
+ outerObjectMarkerIdx !== -1 ? outerObjectMarkerIdx
343
+ : innermostMarkerIdx !== -1 ? innermostMarkerIdx
344
+ : -1
345
+
346
+ if (markerIdx === -1) return null
347
+
348
+ const json = this.extractJsonAroundIndex(content, markerIdx)
349
+ if (!json) return null
350
+
351
+ return this.parseWebexStorage(json)
352
+ }
353
+
354
+ private extractJsonAroundIndex(content: string, markerIdx: number): string | null {
355
+ let depth = 0
356
+ let start = -1
357
+ for (let i = markerIdx; i >= 0; i--) {
358
+ if (content[i] === '}') depth++
359
+ if (content[i] === '{') {
360
+ if (depth === 0) {
361
+ start = i
362
+ break
363
+ }
364
+ depth--
365
+ }
366
+ }
367
+ if (start === -1) return null
368
+
369
+ depth = 0
370
+ let end = -1
371
+ for (let i = start; i < content.length; i++) {
372
+ if (content[i] === '{') depth++
373
+ if (content[i] === '}') {
374
+ depth--
375
+ if (depth === 0) {
376
+ end = i + 1
377
+ break
378
+ }
379
+ }
380
+ }
381
+ if (end === -1) return null
382
+
383
+ return content.substring(start, end)
384
+ }
385
+
386
+ parseWebexStorage(jsonStr: string): ExtractedWebexToken | null {
387
+ try {
388
+ const data = JSON.parse(jsonStr)
389
+
390
+ const supertoken =
391
+ data?.Credentials?.['@']?.supertoken ??
392
+ data?.['@']?.supertoken ??
393
+ data?.supertoken ??
394
+ null
395
+
396
+ if (!supertoken?.access_token) return null
397
+
398
+ const accessToken = String(supertoken.access_token)
399
+ if (accessToken.length < 20) return null
400
+
401
+ const result: ExtractedWebexToken = { accessToken }
402
+
403
+ if (supertoken.refresh_token) {
404
+ result.refreshToken = String(supertoken.refresh_token)
405
+ }
406
+
407
+ if (typeof supertoken.expires === 'number') {
408
+ result.expiresAt = supertoken.expires
409
+ } else if (typeof supertoken.expires_in === 'number') {
410
+ result.expiresAt = Date.now() + supertoken.expires_in * 1000
411
+ }
412
+
413
+ const deviceUrl =
414
+ data?.Device?.['@']?.url ??
415
+ data?.['@']?.deviceUrl ??
416
+ null
417
+ if (deviceUrl && typeof deviceUrl === 'string') {
418
+ result.deviceUrl = deviceUrl
419
+ }
420
+
421
+ const userId = data?.Device?.['@']?.userId ?? null
422
+ if (userId && typeof userId === 'string') {
423
+ result.userId = userId
424
+ }
425
+
426
+ return result
427
+ } catch {
428
+ return null
429
+ }
430
+ }
431
+
432
+ private extractEncryptionKeysFromString(content: string): Map<string, string> {
433
+ const keys = new Map<string, string>()
434
+
435
+ if (!content.includes('"Encryption"') || !content.includes('kms://')) {
436
+ return keys
437
+ }
438
+
439
+ // Values in the Encryption map are double-encoded: a kms:// URI key maps to a
440
+ // JSON string whose content is the serialized key object {"uri":..., "jwk":{...}}.
441
+ // Pattern: "kms://..." : "{\"uri\":...,\"jwk\":{...}}"
442
+ const kmsPattern = /"(kms:\/\/[^"]+)"\s*:\s*("(?:[^"\\]|\\.)*")/g
443
+ let match: RegExpExecArray | null
444
+
445
+ while ((match = kmsPattern.exec(content)) !== null) {
446
+ const uri = match[1]!
447
+ const rawValue = match[2]!
448
+ try {
449
+ const innerStr = JSON.parse(rawValue) as unknown
450
+ if (typeof innerStr !== 'string') continue
451
+ const keyObj = JSON.parse(innerStr) as Record<string, unknown>
452
+ if (keyObj?.jwk) {
453
+ keys.set(uri, innerStr)
454
+ }
455
+ } catch {}
456
+ }
457
+
458
+ return keys
459
+ }
460
+ }
@@ -55,7 +55,10 @@ export interface WebexConfig {
55
55
  expiresAt: number
56
56
  clientId?: string
57
57
  clientSecret?: string
58
- tokenType?: 'oauth' | 'manual'
58
+ tokenType?: 'oauth' | 'manual' | 'extracted'
59
+ deviceUrl?: string
60
+ userId?: string
61
+ encryptionKeys?: Record<string, string>
59
62
  }
60
63
 
61
64
  export class WebexError extends Error {
@@ -123,5 +126,8 @@ export const WebexConfigSchema = z.object({
123
126
  expiresAt: z.number(),
124
127
  clientId: z.string().optional(),
125
128
  clientSecret: z.string().optional(),
126
- tokenType: z.enum(['oauth', 'manual']).optional(),
129
+ tokenType: z.enum(['oauth', 'manual', 'extracted']).optional(),
130
+ deviceUrl: z.string().optional(),
131
+ userId: z.string().optional(),
132
+ encryptionKeys: z.record(z.string(), z.string()).optional(),
127
133
  })
@@ -0,0 +1,27 @@
1
+ export {}
2
+
3
+ declare module 'node-jose' {
4
+ namespace JWE {
5
+ interface JWERecipient {
6
+ key: JWK.Key
7
+ header?: Record<string, string | null>
8
+ reference?: string | null | boolean
9
+ }
10
+
11
+ function createEncrypt(options: EncryptOptions, recipient: JWERecipient): Encryptor
12
+
13
+ interface Encryptor {
14
+ final(data: string | Buffer, encoding?: BufferEncoding): Promise<string>
15
+ }
16
+
17
+ function createDecrypt(key: JWK.Key): Decryptor
18
+
19
+ interface Decryptor {
20
+ decrypt(input: string | Buffer): Promise<DecryptResult>
21
+ }
22
+
23
+ interface DecryptResult {
24
+ plaintext: Buffer
25
+ }
26
+ }
27
+ }
@@ -25,10 +25,14 @@ import {
25
25
 
26
26
  const MAX_MESSAGES_PER_CHAT = 500
27
27
 
28
- function toTimestampMs(ts: number | { toNumber(): number } | null | undefined): number {
28
+ function toTimestampMs(ts: unknown): number {
29
29
  if (ts == null) return 0
30
- if (typeof ts === 'object') return ts.toNumber() * 1000
31
- return ts * 1000
30
+ if (typeof ts === 'number') return ts * 1000
31
+ if (typeof ts === 'object' && ts !== null && 'toNumber' in ts && typeof (ts as Record<string, unknown>).toNumber === 'function') {
32
+ return (ts as { toNumber(): number }).toNumber() * 1000
33
+ }
34
+ const n = Number(ts)
35
+ return Number.isNaN(n) ? 0 : n * 1000
32
36
  }
33
37
 
34
38
  function resolveJid(input: string): string {
@@ -443,8 +447,8 @@ export class WhatsAppClient {
443
447
  const sorted = chats.sort((a, b) => {
444
448
  const aTime = a.conversationTimestamp
445
449
  const bTime = b.conversationTimestamp
446
- const aMs = toTimestampMs(aTime as number | { toNumber(): number } | null | undefined)
447
- const bMs = toTimestampMs(bTime as number | { toNumber(): number } | null | undefined)
450
+ const aMs = toTimestampMs(aTime)
451
+ const bMs = toTimestampMs(bTime)
448
452
  return bMs - aMs
449
453
  })
450
454
 
@@ -474,8 +478,8 @@ export class WhatsAppClient {
474
478
 
475
479
  const msgs = this.messages.get(resolvedJid) ?? []
476
480
  const sorted = [...msgs].sort((a, b) => {
477
- const aMs = toTimestampMs(a.messageTimestamp as number | { toNumber(): number } | null | undefined)
478
- const bMs = toTimestampMs(b.messageTimestamp as number | { toNumber(): number } | null | undefined)
481
+ const aMs = toTimestampMs(a.messageTimestamp)
482
+ const bMs = toTimestampMs(b.messageTimestamp)
479
483
  return aMs - bMs
480
484
  })
481
485
 
@@ -3,7 +3,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
- export type Platform = 'slack' | 'discord' | 'teams'
6
+ export type Platform = 'slack' | 'discord' | 'teams' | 'webex'
7
7
 
8
8
  /**
9
9
  * Caches derived encryption keys to avoid repeated macOS Keychain prompts.
@@ -1,4 +1,6 @@
1
- export function handleError(error: Error): void {
2
- console.error(JSON.stringify({ error: error.message }))
1
+ import { error } from './stderr'
2
+
3
+ export function handleError(err: Error): void {
4
+ error(JSON.stringify({ error: err.message }))
3
5
  process.exit(1)
4
6
  }
@@ -0,0 +1,22 @@
1
+ const isTTY = process.stderr.isTTY ?? false
2
+
3
+ const RESET = isTTY ? '\x1b[0m' : ''
4
+ const RED = isTTY ? '\x1b[31m' : ''
5
+ const YELLOW = isTTY ? '\x1b[33m' : ''
6
+ const DIM = isTTY ? '\x1b[2m' : ''
7
+
8
+ export function info(message: string): void {
9
+ process.stderr.write(`${message}\n`)
10
+ }
11
+
12
+ export function warn(message: string): void {
13
+ process.stderr.write(`${YELLOW}${message}${RESET}\n`)
14
+ }
15
+
16
+ export function error(message: string): void {
17
+ process.stderr.write(`${RED}${message}${RESET}\n`)
18
+ }
19
+
20
+ export function debug(message: string): void {
21
+ process.stderr.write(`${DIM}${message}${RESET}\n`)
22
+ }