agent-messenger 2.8.0 → 2.10.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 +0 -11
- package/dist/package.json +1 -1
- package/dist/src/platforms/channeltalk/commands/snapshot.d.ts +4 -2
- package/dist/src/platforms/channeltalk/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/commands/snapshot.js +86 -31
- package/dist/src/platforms/channeltalk/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/snapshot.d.ts +3 -1
- package/dist/src/platforms/channeltalkbot/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/snapshot.js +110 -60
- package/dist/src/platforms/channeltalkbot/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/discord/commands/snapshot.d.ts +1 -0
- package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/snapshot.js +48 -34
- package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/snapshot.d.ts +2 -0
- package/dist/src/platforms/discordbot/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/snapshot.js +46 -34
- package/dist/src/platforms/discordbot/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/slack/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/snapshot.js +75 -55
- package/dist/src/platforms/slack/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/teams/client.d.ts +9 -1
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +69 -18
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +7 -2
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/channel.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/channel.js +18 -3
- package/dist/src/platforms/teams/commands/channel.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +18 -3
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +24 -4
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.js +12 -2
- package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
- package/dist/src/platforms/teams/commands/snapshot.d.ts +1 -0
- package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/snapshot.js +50 -32
- package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/teams/commands/team.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/team.js +6 -1
- package/dist/src/platforms/teams/commands/team.js.map +1 -1
- package/dist/src/platforms/teams/commands/user.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/user.js +18 -3
- package/dist/src/platforms/teams/commands/user.js.map +1 -1
- package/dist/src/platforms/teams/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/whoami.js +6 -1
- package/dist/src/platforms/teams/commands/whoami.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts +3 -1
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +6 -1
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +7 -2
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/token-extractor.d.ts +3 -1
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +67 -10
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/types.d.ts +17 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +2 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +3 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +58 -13
- 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 +61 -10
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/commands/snapshot.d.ts +1 -0
- package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/snapshot.js +14 -7
- package/dist/src/platforms/webex/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/webex/credential-manager.js +18 -6
- package/dist/src/platforms/webex/credential-manager.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
- package/dist/src/platforms/webex/encryption.js +3 -1
- package/dist/src/platforms/webex/encryption.js.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.js +10 -2
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +1 -0
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/webex/token-extractor.js +21 -4
- package/dist/src/platforms/webex/token-extractor.js.map +1 -1
- package/docs/content/docs/agent-skills.mdx +0 -10
- package/docs/content/docs/cli/channeltalk.mdx +18 -8
- package/docs/content/docs/cli/channeltalkbot.mdx +16 -6
- package/docs/content/docs/cli/discord.mdx +23 -7
- package/docs/content/docs/cli/discordbot.mdx +23 -7
- package/docs/content/docs/cli/slack.mdx +24 -7
- package/docs/content/docs/cli/teams.mdx +24 -8
- package/docs/content/docs/cli/webex.mdx +15 -2
- package/e2e/webex.e2e.test.ts +57 -0
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +19 -9
- package/skills/agent-channeltalk/references/common-patterns.md +10 -9
- package/skills/agent-channeltalkbot/SKILL.md +19 -9
- package/skills/agent-channeltalkbot/references/common-patterns.md +10 -9
- package/skills/agent-discord/SKILL.md +18 -9
- package/skills/agent-discord/references/common-patterns.md +8 -7
- package/skills/agent-discordbot/SKILL.md +18 -9
- 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 +19 -10
- package/skills/agent-slack/references/common-patterns.md +4 -7
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +18 -9
- package/skills/agent-teams/references/common-patterns.md +9 -7
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +13 -4
- package/skills/agent-webex/references/common-patterns.md +8 -2
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/channeltalk/commands/snapshot.test.ts +58 -26
- package/src/platforms/channeltalk/commands/snapshot.ts +107 -33
- package/src/platforms/channeltalkbot/commands/snapshot.test.ts +26 -8
- package/src/platforms/channeltalkbot/commands/snapshot.ts +131 -64
- package/src/platforms/discord/commands/snapshot.test.ts +1 -1
- package/src/platforms/discord/commands/snapshot.ts +58 -42
- package/src/platforms/discordbot/commands/snapshot.test.ts +40 -18
- package/src/platforms/discordbot/commands/snapshot.ts +54 -37
- package/src/platforms/slack/commands/snapshot.test.ts +63 -8
- package/src/platforms/slack/commands/snapshot.ts +98 -66
- package/src/platforms/teams/client.test.ts +34 -30
- package/src/platforms/teams/client.ts +92 -20
- package/src/platforms/teams/commands/auth.test.ts +6 -2
- package/src/platforms/teams/commands/auth.ts +7 -2
- package/src/platforms/teams/commands/channel.test.ts +6 -6
- package/src/platforms/teams/commands/channel.ts +18 -3
- package/src/platforms/teams/commands/file.ts +18 -3
- package/src/platforms/teams/commands/message.ts +24 -4
- package/src/platforms/teams/commands/reaction.ts +12 -2
- package/src/platforms/teams/commands/snapshot.test.ts +1 -1
- package/src/platforms/teams/commands/snapshot.ts +59 -39
- package/src/platforms/teams/commands/team.test.ts +2 -2
- package/src/platforms/teams/commands/team.ts +6 -1
- package/src/platforms/teams/commands/user.ts +18 -3
- package/src/platforms/teams/commands/whoami.ts +6 -1
- package/src/platforms/teams/credential-manager.test.ts +25 -0
- package/src/platforms/teams/credential-manager.ts +13 -3
- package/src/platforms/teams/ensure-auth.test.ts +6 -1
- package/src/platforms/teams/ensure-auth.ts +7 -2
- package/src/platforms/teams/token-extractor.ts +77 -12
- package/src/platforms/teams/types.test.ts +17 -0
- package/src/platforms/teams/types.ts +6 -0
- package/src/platforms/webex/client.test.ts +157 -13
- package/src/platforms/webex/client.ts +64 -15
- package/src/platforms/webex/commands/auth.test.ts +122 -1
- package/src/platforms/webex/commands/auth.ts +72 -17
- package/src/platforms/webex/commands/snapshot.test.ts +14 -1
- package/src/platforms/webex/commands/snapshot.ts +17 -9
- package/src/platforms/webex/credential-manager.test.ts +63 -0
- package/src/platforms/webex/credential-manager.ts +22 -8
- package/src/platforms/webex/encryption.test.ts +54 -0
- package/src/platforms/webex/encryption.ts +3 -1
- package/src/platforms/webex/ensure-auth.ts +10 -2
- package/src/platforms/webex/token-extractor.test.ts +32 -3
- package/src/platforms/webex/token-extractor.ts +26 -5
|
@@ -79,9 +79,21 @@ describe('snapshot command', () => {
|
|
|
79
79
|
consoleSpy.mockRestore()
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
test('returns spaces with id
|
|
82
|
+
test('brief snapshot returns spaces with id and title only', async () => {
|
|
83
83
|
await snapshotAction({})
|
|
84
84
|
|
|
85
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
86
|
+
const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
|
|
87
|
+
expect(output.spaces).toHaveLength(1)
|
|
88
|
+
expect(output.spaces[0].id).toBe('space-1')
|
|
89
|
+
expect(output.spaces[0].title).toBe('General')
|
|
90
|
+
expect(output.spaces[0].type).toBeUndefined()
|
|
91
|
+
expect(output.hint).toBeDefined()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('full snapshot returns spaces with id, title, type, lastActivity', async () => {
|
|
95
|
+
await snapshotAction({ full: true })
|
|
96
|
+
|
|
85
97
|
expect(consoleSpy).toHaveBeenCalled()
|
|
86
98
|
const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
|
|
87
99
|
expect(output.spaces).toHaveLength(1)
|
|
@@ -89,6 +101,7 @@ describe('snapshot command', () => {
|
|
|
89
101
|
expect(output.spaces[0].title).toBe('General')
|
|
90
102
|
expect(output.spaces[0].type).toBe('group')
|
|
91
103
|
expect(output.spaces[0].lastActivity).toBe('2024-01-15T00:00:00.000Z')
|
|
104
|
+
expect(output.hint).toBeUndefined()
|
|
92
105
|
})
|
|
93
106
|
|
|
94
107
|
test('filters spaces to only those in my memberships', async () => {
|
|
@@ -5,7 +5,7 @@ import { formatOutput } from '@/shared/utils/output'
|
|
|
5
5
|
|
|
6
6
|
import { WebexClient } from '../client'
|
|
7
7
|
|
|
8
|
-
export async function snapshotAction(options: { pretty?: boolean }): Promise<void> {
|
|
8
|
+
export async function snapshotAction(options: { full?: boolean; pretty?: boolean }): Promise<void> {
|
|
9
9
|
try {
|
|
10
10
|
const client = await new WebexClient().login()
|
|
11
11
|
|
|
@@ -15,13 +15,19 @@ export async function snapshotAction(options: { pretty?: boolean }): Promise<voi
|
|
|
15
15
|
const allSpaces = await client.listSpaces({ max: 100 })
|
|
16
16
|
const spaces = allSpaces.filter((s) => myRoomIds.has(s.id))
|
|
17
17
|
|
|
18
|
-
const snapshot = {
|
|
19
|
-
spaces:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
const snapshot: Record<string, any> = {
|
|
19
|
+
spaces: options.full
|
|
20
|
+
? spaces.map((s) => ({
|
|
21
|
+
id: s.id,
|
|
22
|
+
title: s.title,
|
|
23
|
+
type: s.type,
|
|
24
|
+
lastActivity: s.lastActivity,
|
|
25
|
+
}))
|
|
26
|
+
: spaces.map((s) => ({ id: s.id, title: s.title })),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!options.full) {
|
|
30
|
+
snapshot.hint = "Use 'message list <space>' for messages, 'space info <space>' for space details."
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
console.log(formatOutput(snapshot, options.pretty))
|
|
@@ -31,10 +37,12 @@ export async function snapshotAction(options: { pretty?: boolean }): Promise<voi
|
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export const snapshotCommand = new Command('snapshot')
|
|
34
|
-
.description('Get workspace
|
|
40
|
+
.description('Get workspace overview for AI agents (brief by default, use --full for comprehensive data)')
|
|
41
|
+
.option('--full', 'Include full space details (verbose)')
|
|
35
42
|
.option('--pretty', 'Pretty print JSON output')
|
|
36
43
|
.action(async (options) => {
|
|
37
44
|
await snapshotAction({
|
|
45
|
+
full: options.full,
|
|
38
46
|
pretty: options.pretty,
|
|
39
47
|
})
|
|
40
48
|
})
|
|
@@ -291,6 +291,69 @@ describe('WebexCredentialManager', () => {
|
|
|
291
291
|
expect(loaded?.clientSecret).toBe('my-client-secret')
|
|
292
292
|
})
|
|
293
293
|
|
|
294
|
+
test('getToken tries refresh for expired extracted tokens', async () => {
|
|
295
|
+
const originalFetch = globalThis.fetch
|
|
296
|
+
globalThis.fetch = mock(() =>
|
|
297
|
+
Promise.resolve(
|
|
298
|
+
new Response(
|
|
299
|
+
JSON.stringify({
|
|
300
|
+
access_token: 'refreshed-extracted-token',
|
|
301
|
+
refresh_token: 'new-refresh',
|
|
302
|
+
expires_in: 3600,
|
|
303
|
+
}),
|
|
304
|
+
{ status: 200 },
|
|
305
|
+
),
|
|
306
|
+
),
|
|
307
|
+
) as typeof fetch
|
|
308
|
+
|
|
309
|
+
await credManager.saveConfig({
|
|
310
|
+
accessToken: 'expired-extracted-token',
|
|
311
|
+
refreshToken: 'extracted-refresh',
|
|
312
|
+
expiresAt: Date.now() - 1000,
|
|
313
|
+
tokenType: 'extracted',
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const token = await credManager.getToken()
|
|
317
|
+
expect(token).toBe('refreshed-extracted-token')
|
|
318
|
+
|
|
319
|
+
const config = await credManager.loadConfig()
|
|
320
|
+
expect(config?.tokenType).toBe('extracted')
|
|
321
|
+
expect(config?.accessToken).toBe('refreshed-extracted-token')
|
|
322
|
+
|
|
323
|
+
globalThis.fetch = originalFetch
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('getToken returns expired extracted token when refresh fails', async () => {
|
|
327
|
+
const originalFetch = globalThis.fetch
|
|
328
|
+
globalThis.fetch = mock(() =>
|
|
329
|
+
Promise.resolve(new Response('{"error":"invalid_grant"}', { status: 400 })),
|
|
330
|
+
) as typeof fetch
|
|
331
|
+
|
|
332
|
+
await credManager.saveConfig({
|
|
333
|
+
accessToken: 'expired-extracted-token',
|
|
334
|
+
refreshToken: 'bad-refresh',
|
|
335
|
+
expiresAt: Date.now() - 1000,
|
|
336
|
+
tokenType: 'extracted',
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const token = await credManager.getToken()
|
|
340
|
+
expect(token).toBe('expired-extracted-token')
|
|
341
|
+
|
|
342
|
+
globalThis.fetch = originalFetch
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('getToken returns non-expired extracted token without refresh', async () => {
|
|
346
|
+
await credManager.saveConfig({
|
|
347
|
+
accessToken: 'valid-extracted-token',
|
|
348
|
+
refreshToken: 'refresh',
|
|
349
|
+
expiresAt: Date.now() + 3600000,
|
|
350
|
+
tokenType: 'extracted',
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const token = await credManager.getToken()
|
|
354
|
+
expect(token).toBe('valid-extracted-token')
|
|
355
|
+
})
|
|
356
|
+
|
|
294
357
|
test('loadConfig backward compat — old config without clientId/clientSecret', async () => {
|
|
295
358
|
// Write raw JSON without clientId/clientSecret fields
|
|
296
359
|
const credPath = join(tempDir, 'webex-credentials.json')
|
|
@@ -45,16 +45,33 @@ export class WebexCredentialManager {
|
|
|
45
45
|
const config = await this.loadConfig()
|
|
46
46
|
if (!config) return null
|
|
47
47
|
|
|
48
|
-
if (config.tokenType === 'manual'
|
|
48
|
+
if (config.tokenType === 'manual') {
|
|
49
49
|
return config.accessToken
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
const isExpired = config.expiresAt > 0 && config.expiresAt < Date.now() + 5 * 60 * 1000
|
|
53
|
+
|
|
54
|
+
if (config.tokenType === 'extracted') {
|
|
55
|
+
if (isExpired && config.refreshToken) {
|
|
56
|
+
const builtinCreds = getWebexAppCredentials()
|
|
57
|
+
const refreshed = await this.refreshToken(config.refreshToken, builtinCreds.clientId, builtinCreds.clientSecret)
|
|
58
|
+
if (refreshed) {
|
|
59
|
+
await this.saveConfig({ ...config, ...refreshed, tokenType: 'extracted' })
|
|
60
|
+
return refreshed.accessToken
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return config.accessToken
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isExpired) {
|
|
53
67
|
const builtinCreds = getWebexAppCredentials()
|
|
54
68
|
const resolvedClientId = clientId ?? config.clientId ?? builtinCreds.clientId
|
|
55
69
|
const resolvedClientSecret = clientSecret ?? config.clientSecret ?? builtinCreds.clientSecret
|
|
56
70
|
const refreshed = await this.refreshToken(config.refreshToken, resolvedClientId, resolvedClientSecret)
|
|
57
|
-
if (refreshed)
|
|
71
|
+
if (refreshed) {
|
|
72
|
+
await this.saveConfig({ ...config, ...refreshed })
|
|
73
|
+
return refreshed.accessToken
|
|
74
|
+
}
|
|
58
75
|
return null
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -82,14 +99,11 @@ export class WebexCredentialManager {
|
|
|
82
99
|
expires_in: number
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
|
|
102
|
+
return {
|
|
86
103
|
accessToken: data.access_token,
|
|
87
104
|
refreshToken: data.refresh_token,
|
|
88
105
|
expiresAt: Date.now() + data.expires_in * 1000,
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
await this.saveConfig(config)
|
|
92
|
-
return config
|
|
106
|
+
} satisfies Pick<WebexConfig, 'accessToken' | 'refreshToken' | 'expiresAt'>
|
|
93
107
|
} catch {
|
|
94
108
|
return null
|
|
95
109
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import * as jose from 'node-jose'
|
|
4
|
+
|
|
5
|
+
import { WebexEncryptionService } from './encryption'
|
|
6
|
+
|
|
7
|
+
const decodeJweHeader = (jwe: string): Record<string, unknown> => {
|
|
8
|
+
const [header = ''] = jwe.split('.')
|
|
9
|
+
const padded = header + '='.repeat((4 - (header.length % 4)) % 4)
|
|
10
|
+
const json = Buffer.from(padded, 'base64url').toString('utf8')
|
|
11
|
+
return JSON.parse(json) as Record<string, unknown>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const createKeyring = async (keyUri: string) => {
|
|
15
|
+
const keystore = jose.JWK.createKeyStore()
|
|
16
|
+
const key = await keystore.generate('oct', 256, { alg: 'A256GCM' })
|
|
17
|
+
const jwk = key.toJSON(true)
|
|
18
|
+
const rawKeys = new Map<string, string>()
|
|
19
|
+
rawKeys.set(keyUri, JSON.stringify({ jwk }))
|
|
20
|
+
return new WebexEncryptionService(rawKeys)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('WebexEncryptionService', () => {
|
|
24
|
+
const keyUri = 'kms://kms-aore.wbx2.com/keys/7819829b-5e0d-4139-9cad-1b6fe7aee533'
|
|
25
|
+
|
|
26
|
+
test('encryptText emits JWE with alg, enc, and kid JOSE headers', async () => {
|
|
27
|
+
const service = await createKeyring(keyUri)
|
|
28
|
+
|
|
29
|
+
const jwe = await service.encryptText(keyUri, 'hello world')
|
|
30
|
+
|
|
31
|
+
expect(jwe).not.toBeNull()
|
|
32
|
+
const header = decodeJweHeader(jwe as string)
|
|
33
|
+
expect(header.alg).toBe('dir')
|
|
34
|
+
expect(header.enc).toBe('A256GCM')
|
|
35
|
+
expect(header.kid).toBe(keyUri)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('encryptText returns null when key is unknown', async () => {
|
|
39
|
+
const service = await createKeyring(keyUri)
|
|
40
|
+
|
|
41
|
+
const jwe = await service.encryptText('kms://other/keys/missing', 'hello')
|
|
42
|
+
|
|
43
|
+
expect(jwe).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('decryptText round-trips plaintext encrypted by encryptText', async () => {
|
|
47
|
+
const service = await createKeyring(keyUri)
|
|
48
|
+
|
|
49
|
+
const jwe = await service.encryptText(keyUri, 'round trip')
|
|
50
|
+
const plaintext = await service.decryptText(keyUri, jwe as string)
|
|
51
|
+
|
|
52
|
+
expect(plaintext).toBe('round trip')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -30,9 +30,11 @@ export class WebexEncryptionService {
|
|
|
30
30
|
if (!key) return null
|
|
31
31
|
|
|
32
32
|
try {
|
|
33
|
+
// Webex desktop/web clients auto-tombstone edit activities whose JWE is missing
|
|
34
|
+
// `kid` — they can't resolve the KMS key and treat the activity as malformed.
|
|
33
35
|
return await jose.JWE.createEncrypt(
|
|
34
36
|
{ format: 'compact', contentAlg: 'A256GCM' },
|
|
35
|
-
{ key, header: { alg: 'dir' }, reference: null },
|
|
37
|
+
{ key, header: { alg: 'dir', kid: keyUri }, reference: null },
|
|
36
38
|
).final(plaintext, 'utf8')
|
|
37
39
|
} catch {
|
|
38
40
|
return null
|
|
@@ -11,7 +11,11 @@ export async function ensureWebexAuth(): Promise<void> {
|
|
|
11
11
|
const token = await credManager.getToken(config.clientId, config.clientSecret)
|
|
12
12
|
if (token) {
|
|
13
13
|
const client = new WebexClient()
|
|
14
|
-
await client.login({
|
|
14
|
+
await client.login({
|
|
15
|
+
token,
|
|
16
|
+
deviceUrl: config.deviceUrl,
|
|
17
|
+
tokenType: config.tokenType,
|
|
18
|
+
})
|
|
15
19
|
await client.testAuth()
|
|
16
20
|
return
|
|
17
21
|
}
|
|
@@ -22,7 +26,11 @@ export async function ensureWebexAuth(): Promise<void> {
|
|
|
22
26
|
if (!extracted) return
|
|
23
27
|
|
|
24
28
|
const client = new WebexClient()
|
|
25
|
-
await client.login({
|
|
29
|
+
await client.login({
|
|
30
|
+
token: extracted.accessToken,
|
|
31
|
+
deviceUrl: extracted.deviceUrl,
|
|
32
|
+
tokenType: 'extracted',
|
|
33
|
+
})
|
|
26
34
|
await client.testAuth()
|
|
27
35
|
|
|
28
36
|
await credManager.saveConfig({
|
|
@@ -216,12 +216,41 @@ describe('WebexTokenExtractor', () => {
|
|
|
216
216
|
expect(result).not.toBeNull()
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
-
test('
|
|
219
|
+
test('prefers token with latest expiry across profiles', async () => {
|
|
220
220
|
const dir1 = createLevelDBDir(tempDir, 'Default')
|
|
221
221
|
const dir2 = createLevelDBDir(tempDir, 'Profile 1')
|
|
222
222
|
|
|
223
|
-
const
|
|
224
|
-
|
|
223
|
+
const expiredToken = makeWebexStorageJson({
|
|
224
|
+
accessToken: 'expired-token-longer-than-twenty-chars-xx',
|
|
225
|
+
expires: Date.now() - 3600000,
|
|
226
|
+
})
|
|
227
|
+
const freshToken = makeWebexStorageJson({
|
|
228
|
+
accessToken: 'fresh-token-longer-than-twenty-chars-xxx',
|
|
229
|
+
expires: Date.now() + 3600000,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
writeFileSync(join(dir1, '000003.log'), expiredToken)
|
|
233
|
+
writeFileSync(join(dir2, '000003.log'), freshToken)
|
|
234
|
+
|
|
235
|
+
const extractor = new WebexTokenExtractor('darwin', undefined, tempDir)
|
|
236
|
+
const result = await extractor.extract()
|
|
237
|
+
|
|
238
|
+
expect(result!.accessToken).toBe('fresh-token-longer-than-twenty-chars-xxx')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('returns first token when all have same expiry', async () => {
|
|
242
|
+
const dir1 = createLevelDBDir(tempDir, 'Default')
|
|
243
|
+
const dir2 = createLevelDBDir(tempDir, 'Profile 1')
|
|
244
|
+
|
|
245
|
+
const expires = Date.now() + 3600000
|
|
246
|
+
const token1 = makeWebexStorageJson({
|
|
247
|
+
accessToken: 'first-valid-token-longer-than-twenty-chars',
|
|
248
|
+
expires,
|
|
249
|
+
})
|
|
250
|
+
const token2 = makeWebexStorageJson({
|
|
251
|
+
accessToken: 'second-valid-token-longer-than-twenty-chars',
|
|
252
|
+
expires,
|
|
253
|
+
})
|
|
225
254
|
|
|
226
255
|
writeFileSync(join(dir1, '000003.log'), token1)
|
|
227
256
|
writeFileSync(join(dir2, '000003.log'), token2)
|
|
@@ -160,25 +160,46 @@ export class WebexTokenExtractor {
|
|
|
160
160
|
return null
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
let best: { token: ExtractedWebexToken; source: string } | null = null
|
|
164
|
+
|
|
163
165
|
for (const leveldbDir of profileDirs) {
|
|
164
166
|
this.debug(`Scanning: ${leveldbDir}`)
|
|
165
167
|
|
|
166
168
|
const result = (await this.scanViaClassicLevelCopy(leveldbDir)) ?? this.scanRawFiles(leveldbDir)
|
|
167
169
|
|
|
168
170
|
if (result?.token) {
|
|
169
|
-
this.debug(`Found token in: ${leveldbDir}`)
|
|
170
|
-
|
|
171
171
|
const token = result.token
|
|
172
172
|
if (result.encryptionKeys.size > 0) {
|
|
173
173
|
token.encryptionKeys = result.encryptionKeys
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
this.debug(
|
|
177
|
+
`Found token in: ${leveldbDir} (expires: ${token.expiresAt ? new Date(token.expiresAt).toISOString() : 'unknown'}, length: ${token.accessToken.length})`,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if (!best || this.isTokenFresher(token, best.token)) {
|
|
181
|
+
best = { token, source: leveldbDir }
|
|
182
|
+
}
|
|
177
183
|
}
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
if (!best) {
|
|
187
|
+
this.debug('No Webex tokens found in any browser profile')
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.debug(`Selected token from: ${best.source}`)
|
|
192
|
+
return best.token
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private isTokenFresher(candidate: ExtractedWebexToken, current: ExtractedWebexToken): boolean {
|
|
196
|
+
const candidateExpiry = candidate.expiresAt ?? 0
|
|
197
|
+
const currentExpiry = current.expiresAt ?? 0
|
|
198
|
+
if (candidateExpiry > 0 && currentExpiry > 0) {
|
|
199
|
+
return candidateExpiry > currentExpiry
|
|
200
|
+
}
|
|
201
|
+
if (candidateExpiry > 0 && currentExpiry === 0) return true
|
|
202
|
+
return false
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
private async scanViaClassicLevelCopy(dbPath: string): Promise<ScanResult | null> {
|