agent-messenger 2.2.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/README.md +1 -1
- package/bun.lock +25 -1
- package/dist/package.json +4 -2
- 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/webex/client.d.ts +2 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +66 -23
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +4 -0
- package/dist/src/platforms/webex/commands/auth.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 +4 -0
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +6 -5
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/webex/token-extractor.js +92 -43
- package/dist/src/platforms/webex/token-extractor.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +4 -0
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.js +2 -0
- package/dist/src/platforms/webex/types.js.map +1 -1
- package/docs/content/docs/cli/webex.mdx +4 -2
- package/package.json +4 -2
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webex/references/authentication.md +4 -3
- package/skills/agent-webex/references/common-patterns.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/line/client.ts +39 -14
- package/src/platforms/webex/client.ts +98 -26
- package/src/platforms/webex/commands/auth.ts +4 -0
- package/src/platforms/webex/encryption.ts +53 -0
- package/src/platforms/webex/ensure-auth.ts +4 -0
- package/src/platforms/webex/token-extractor.ts +107 -40
- package/src/platforms/webex/types.ts +4 -0
- package/src/platforms/webex/typings/node-jose.d.ts +27 -0
|
@@ -17,6 +17,8 @@ export interface ExtractedWebexToken {
|
|
|
17
17
|
refreshToken?: string
|
|
18
18
|
expiresAt?: number
|
|
19
19
|
deviceUrl?: string
|
|
20
|
+
userId?: string
|
|
21
|
+
encryptionKeys?: Map<string, string>
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
interface BrowserConfig {
|
|
@@ -73,6 +75,11 @@ const BROWSERS: BrowserConfig[] = [
|
|
|
73
75
|
|
|
74
76
|
const WEBEX_STORAGE_KEY = '_https://web.webex.com\x00\x01webex-web-client-bounded'
|
|
75
77
|
|
|
78
|
+
interface ScanResult {
|
|
79
|
+
token: ExtractedWebexToken | null
|
|
80
|
+
encryptionKeys: Map<string, string>
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
export class WebexTokenExtractor {
|
|
77
84
|
private platform: NodeJS.Platform
|
|
78
85
|
private baseDir: string | null
|
|
@@ -169,11 +176,17 @@ export class WebexTokenExtractor {
|
|
|
169
176
|
for (const leveldbDir of profileDirs) {
|
|
170
177
|
this.debug(`Scanning: ${leveldbDir}`)
|
|
171
178
|
|
|
172
|
-
const
|
|
173
|
-
?? this.
|
|
179
|
+
const result = await this.scanViaClassicLevelCopy(leveldbDir)
|
|
180
|
+
?? this.scanRawFiles(leveldbDir)
|
|
174
181
|
|
|
175
|
-
if (token) {
|
|
182
|
+
if (result?.token) {
|
|
176
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
|
+
|
|
177
190
|
return token
|
|
178
191
|
}
|
|
179
192
|
}
|
|
@@ -182,7 +195,7 @@ export class WebexTokenExtractor {
|
|
|
182
195
|
return null
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
private async
|
|
198
|
+
private async scanViaClassicLevelCopy(dbPath: string): Promise<ScanResult | null> {
|
|
186
199
|
const tempDir = join(tmpdir(), `webex-leveldb-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
187
200
|
|
|
188
201
|
try {
|
|
@@ -199,7 +212,7 @@ export class WebexTokenExtractor {
|
|
|
199
212
|
} catch {}
|
|
200
213
|
}
|
|
201
214
|
|
|
202
|
-
return await this.
|
|
215
|
+
return await this.copyAndScanLevelDB(tempDir)
|
|
203
216
|
} catch {
|
|
204
217
|
return null
|
|
205
218
|
} finally {
|
|
@@ -209,8 +222,11 @@ export class WebexTokenExtractor {
|
|
|
209
222
|
}
|
|
210
223
|
}
|
|
211
224
|
|
|
212
|
-
private async
|
|
225
|
+
private async copyAndScanLevelDB(dbPath: string): Promise<ScanResult | null> {
|
|
213
226
|
let db: ClassicLevel<string, Buffer> | null = null
|
|
227
|
+
let token: ExtractedWebexToken | null = null
|
|
228
|
+
const encryptionKeys = new Map<string, string>()
|
|
229
|
+
|
|
214
230
|
try {
|
|
215
231
|
db = new ClassicLevel(dbPath, { keyEncoding: 'utf8', valueEncoding: 'buffer' })
|
|
216
232
|
|
|
@@ -218,13 +234,21 @@ export class WebexTokenExtractor {
|
|
|
218
234
|
if (!key.includes('web.webex.com')) continue
|
|
219
235
|
|
|
220
236
|
const decoded = this.decodeLevelDBValue(value)
|
|
221
|
-
if (!decoded.includes('"supertoken"') && !decoded.includes('"Credentials"')) continue
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
}
|
|
225
248
|
}
|
|
226
249
|
} catch (e) {
|
|
227
250
|
this.debug(`ClassicLevel failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
251
|
+
return null
|
|
228
252
|
} finally {
|
|
229
253
|
if (db) {
|
|
230
254
|
try {
|
|
@@ -232,22 +256,15 @@ export class WebexTokenExtractor {
|
|
|
232
256
|
} catch {}
|
|
233
257
|
}
|
|
234
258
|
}
|
|
235
|
-
return null
|
|
236
|
-
}
|
|
237
259
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Chromium localStorage: 0x00 prefix = UTF-16LE, 0x01 prefix = Latin1/UTF-8
|
|
241
|
-
if (buf[0] === 0x00 && (buf.length - 1) % 2 === 0) {
|
|
242
|
-
return buf.subarray(1).toString('utf16le')
|
|
243
|
-
}
|
|
244
|
-
if (buf[0] === 0x01) {
|
|
245
|
-
return buf.subarray(1).toString('utf8')
|
|
246
|
-
}
|
|
247
|
-
return buf.toString('utf8')
|
|
260
|
+
if (!token) return null
|
|
261
|
+
return { token, encryptionKeys }
|
|
248
262
|
}
|
|
249
263
|
|
|
250
|
-
private
|
|
264
|
+
private scanRawFiles(leveldbDir: string): ScanResult | null {
|
|
265
|
+
let token: ExtractedWebexToken | null = null
|
|
266
|
+
const encryptionKeys = new Map<string, string>()
|
|
267
|
+
|
|
251
268
|
try {
|
|
252
269
|
const files = readdirSync(leveldbDir)
|
|
253
270
|
|
|
@@ -265,32 +282,48 @@ export class WebexTokenExtractor {
|
|
|
265
282
|
})
|
|
266
283
|
|
|
267
284
|
for (const file of sorted) {
|
|
268
|
-
const
|
|
269
|
-
|
|
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
|
+
}
|
|
270
308
|
}
|
|
271
309
|
} catch {
|
|
272
310
|
this.debug(`Failed to read directory: ${leveldbDir}`)
|
|
273
311
|
}
|
|
274
312
|
|
|
275
|
-
return null
|
|
313
|
+
if (!token) return null
|
|
314
|
+
return { token, encryptionKeys }
|
|
276
315
|
}
|
|
277
316
|
|
|
278
|
-
private
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const buffer = readFileSync(filePath)
|
|
284
|
-
return this.extractTokenFromBuffer(buffer)
|
|
285
|
-
} catch {
|
|
286
|
-
return null
|
|
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')
|
|
287
322
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return this.extractTokenFromString(buffer.toString('utf8'))
|
|
293
|
-
?? this.extractTokenFromString(this.stripNullBytes(buffer))
|
|
323
|
+
if (buf[0] === 0x01) {
|
|
324
|
+
return buf.subarray(1).toString('utf8')
|
|
325
|
+
}
|
|
326
|
+
return buf.toString('utf8')
|
|
294
327
|
}
|
|
295
328
|
|
|
296
329
|
private stripNullBytes(buffer: Buffer): string {
|
|
@@ -385,9 +418,43 @@ export class WebexTokenExtractor {
|
|
|
385
418
|
result.deviceUrl = deviceUrl
|
|
386
419
|
}
|
|
387
420
|
|
|
421
|
+
const userId = data?.Device?.['@']?.userId ?? null
|
|
422
|
+
if (userId && typeof userId === 'string') {
|
|
423
|
+
result.userId = userId
|
|
424
|
+
}
|
|
425
|
+
|
|
388
426
|
return result
|
|
389
427
|
} catch {
|
|
390
428
|
return null
|
|
391
429
|
}
|
|
392
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
|
+
}
|
|
393
460
|
}
|
|
@@ -57,6 +57,8 @@ export interface WebexConfig {
|
|
|
57
57
|
clientSecret?: string
|
|
58
58
|
tokenType?: 'oauth' | 'manual' | 'extracted'
|
|
59
59
|
deviceUrl?: string
|
|
60
|
+
userId?: string
|
|
61
|
+
encryptionKeys?: Record<string, string>
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export class WebexError extends Error {
|
|
@@ -126,4 +128,6 @@ export const WebexConfigSchema = z.object({
|
|
|
126
128
|
clientSecret: z.string().optional(),
|
|
127
129
|
tokenType: z.enum(['oauth', 'manual', 'extracted']).optional(),
|
|
128
130
|
deviceUrl: z.string().optional(),
|
|
131
|
+
userId: z.string().optional(),
|
|
132
|
+
encryptionKeys: z.record(z.string(), z.string()).optional(),
|
|
129
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
|
+
}
|