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.
- package/.claude-plugin/plugin.json +1 -1
- package/.env.template +35 -17
- package/README.md +7 -7
- package/bun.lock +31 -7
- package/dist/package.json +5 -3
- package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/commands/auth.js +35 -28
- package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/channeltalk/ensure-auth.js +6 -6
- package/dist/src/platforms/channeltalk/ensure-auth.js.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts +23 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.js +299 -29
- package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
- package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/auth.js +57 -49
- package/dist/src/platforms/discord/commands/auth.js.map +1 -1
- package/dist/src/platforms/discord/ensure-auth.js +3 -3
- package/dist/src/platforms/discord/ensure-auth.js.map +1 -1
- package/dist/src/platforms/discord/token-extractor.d.ts +6 -1
- package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/discord/token-extractor.js +167 -14
- package/dist/src/platforms/discord/token-extractor.js.map +1 -1
- package/dist/src/platforms/instagram/client.d.ts +2 -0
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +2 -2
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +107 -14
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/ensure-auth.js +57 -11
- package/dist/src/platforms/instagram/ensure-auth.js.map +1 -1
- package/dist/src/platforms/instagram/index.d.ts +1 -0
- package/dist/src/platforms/instagram/index.d.ts.map +1 -1
- package/dist/src/platforms/instagram/index.js +1 -0
- package/dist/src/platforms/instagram/index.js.map +1 -1
- package/dist/src/platforms/instagram/token-extractor.d.ts +44 -0
- package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -0
- package/dist/src/platforms/instagram/token-extractor.js +407 -0
- package/dist/src/platforms/instagram/token-extractor.js.map +1 -0
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +2 -1
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.js +14 -13
- package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js +2 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
- package/dist/src/platforms/line/client.d.ts.map +1 -1
- package/dist/src/platforms/line/client.js +36 -9
- package/dist/src/platforms/line/client.js.map +1 -1
- package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/line/commands/auth.js +6 -5
- package/dist/src/platforms/line/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/auth.js +11 -10
- package/dist/src/platforms/slack/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
- package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/slack/token-extractor.js +300 -23
- package/dist/src/platforms/slack/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +9 -8
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +2 -1
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +161 -29
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/telegram/client.d.ts.map +1 -1
- package/dist/src/platforms/telegram/client.js +25 -7
- package/dist/src/platforms/telegram/client.js.map +1 -1
- package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/telegram/commands/auth.js +6 -5
- package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +12 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +168 -1
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts +4 -0
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +50 -4
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/credential-manager.js +1 -1
- package/dist/src/platforms/webex/credential-manager.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts +10 -0
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
- package/dist/src/platforms/webex/encryption.js +49 -0
- package/dist/src/platforms/webex/encryption.js.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.js +25 -5
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
- package/dist/src/platforms/webex/index.d.ts +2 -0
- package/dist/src/platforms/webex/index.d.ts.map +1 -1
- package/dist/src/platforms/webex/index.js +1 -0
- package/dist/src/platforms/webex/index.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +29 -0
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
- package/dist/src/platforms/webex/token-extractor.js +393 -0
- package/dist/src/platforms/webex/token-extractor.js.map +1 -0
- package/dist/src/platforms/webex/types.d.ts +8 -1
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.js +4 -1
- package/dist/src/platforms/webex/types.js.map +1 -1
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.js +6 -2
- package/dist/src/platforms/whatsapp/client.js.map +1 -1
- package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
- package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
- package/dist/src/shared/utils/error-handler.d.ts +1 -1
- package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
- package/dist/src/shared/utils/error-handler.js +3 -2
- package/dist/src/shared/utils/error-handler.js.map +1 -1
- package/dist/src/shared/utils/stderr.d.ts +5 -0
- package/dist/src/shared/utils/stderr.d.ts.map +1 -0
- package/dist/src/shared/utils/stderr.js +18 -0
- package/dist/src/shared/utils/stderr.js.map +1 -0
- package/docs/content/docs/cli/channeltalk.mdx +7 -7
- package/docs/content/docs/cli/discord.mdx +3 -3
- package/docs/content/docs/cli/instagram.mdx +28 -6
- package/docs/content/docs/cli/slack.mdx +2 -2
- package/docs/content/docs/cli/teams.mdx +6 -4
- package/docs/content/docs/cli/webex.mdx +32 -11
- package/e2e/README.md +132 -8
- package/e2e/channeltalk.e2e.test.ts +2 -7
- package/e2e/channeltalkbot.e2e.test.ts +2 -6
- package/e2e/config.ts +172 -10
- package/e2e/helpers.ts +7 -0
- package/e2e/instagram.e2e.test.ts +97 -0
- package/e2e/kakaotalk.e2e.test.ts +74 -0
- package/e2e/line.e2e.test.ts +92 -0
- package/e2e/teams.e2e.test.ts +46 -1
- package/e2e/telegram.e2e.test.ts +84 -0
- package/e2e/webex.e2e.test.ts +190 -0
- package/e2e/whatsapp.e2e.test.ts +90 -0
- package/e2e/whatsappbot.e2e.test.ts +78 -0
- package/package.json +5 -3
- package/skills/agent-channeltalk/SKILL.md +9 -9
- package/skills/agent-channeltalk/references/authentication.md +21 -18
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +5 -5
- package/skills/agent-discord/references/authentication.md +8 -8
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +51 -9
- package/skills/agent-instagram/references/authentication.md +35 -3
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +5 -5
- package/skills/agent-slack/references/authentication.md +8 -8
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +6 -6
- package/skills/agent-teams/references/authentication.md +8 -8
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +35 -15
- package/skills/agent-webex/references/authentication.md +63 -9
- package/skills/agent-webex/references/common-patterns.md +6 -3
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
- package/src/platforms/channeltalk/commands/auth.ts +38 -32
- package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
- package/src/platforms/channeltalk/ensure-auth.ts +6 -6
- package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
- package/src/platforms/channeltalk/token-extractor.ts +344 -30
- package/src/platforms/discord/commands/auth.test.ts +3 -3
- package/src/platforms/discord/commands/auth.ts +58 -54
- package/src/platforms/discord/ensure-auth.test.ts +3 -3
- package/src/platforms/discord/ensure-auth.ts +3 -3
- package/src/platforms/discord/token-extractor.test.ts +199 -27
- package/src/platforms/discord/token-extractor.ts +190 -17
- package/src/platforms/instagram/client.ts +2 -2
- package/src/platforms/instagram/commands/auth.ts +133 -14
- package/src/platforms/instagram/ensure-auth.ts +63 -12
- package/src/platforms/instagram/index.ts +1 -0
- package/src/platforms/instagram/token-extractor.test.ts +424 -0
- package/src/platforms/instagram/token-extractor.ts +478 -0
- package/src/platforms/kakaotalk/client.ts +3 -1
- package/src/platforms/kakaotalk/commands/auth.ts +14 -13
- package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
- package/src/platforms/line/client.ts +39 -14
- package/src/platforms/line/commands/auth.ts +7 -6
- package/src/platforms/slack/cli.test.ts +6 -5
- package/src/platforms/slack/commands/auth.test.ts +11 -7
- package/src/platforms/slack/commands/auth.ts +11 -10
- package/src/platforms/slack/token-extractor.test.ts +98 -1
- package/src/platforms/slack/token-extractor.ts +338 -26
- package/src/platforms/teams/commands/auth.ts +9 -8
- package/src/platforms/teams/ensure-auth.ts +3 -1
- package/src/platforms/teams/token-extractor.test.ts +136 -17
- package/src/platforms/teams/token-extractor.ts +182 -31
- package/src/platforms/telegram/client.test.ts +134 -0
- package/src/platforms/telegram/client.ts +27 -6
- package/src/platforms/telegram/commands/auth.ts +6 -5
- package/src/platforms/webex/client.test.ts +314 -0
- package/src/platforms/webex/client.ts +231 -1
- package/src/platforms/webex/commands/auth.ts +71 -4
- package/src/platforms/webex/commands/member.test.ts +10 -1
- package/src/platforms/webex/commands/message.test.ts +9 -5
- package/src/platforms/webex/commands/snapshot.test.ts +13 -4
- package/src/platforms/webex/commands/space.test.ts +12 -2
- package/src/platforms/webex/credential-manager.ts +1 -1
- package/src/platforms/webex/encryption.ts +53 -0
- package/src/platforms/webex/ensure-auth.test.ts +4 -0
- package/src/platforms/webex/ensure-auth.ts +27 -4
- package/src/platforms/webex/index.ts +2 -0
- package/src/platforms/webex/token-extractor.test.ts +327 -0
- package/src/platforms/webex/token-extractor.ts +460 -0
- package/src/platforms/webex/types.ts +8 -2
- package/src/platforms/webex/typings/node-jose.d.ts +27 -0
- package/src/platforms/whatsapp/client.ts +11 -7
- package/src/shared/utils/derived-key-cache.ts +1 -1
- package/src/shared/utils/error-handler.ts +4 -2
- 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:
|
|
28
|
+
function toTimestampMs(ts: unknown): number {
|
|
29
29
|
if (ts == null) return 0
|
|
30
|
-
if (typeof ts === '
|
|
31
|
-
|
|
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
|
|
447
|
-
const bMs = toTimestampMs(bTime
|
|
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
|
|
478
|
-
const bMs = toTimestampMs(b.messageTimestamp
|
|
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.
|
|
@@ -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
|
+
}
|