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.
Files changed (169) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +0 -11
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/channeltalk/commands/snapshot.d.ts +4 -2
  5. package/dist/src/platforms/channeltalk/commands/snapshot.d.ts.map +1 -1
  6. package/dist/src/platforms/channeltalk/commands/snapshot.js +86 -31
  7. package/dist/src/platforms/channeltalk/commands/snapshot.js.map +1 -1
  8. package/dist/src/platforms/channeltalkbot/commands/snapshot.d.ts +3 -1
  9. package/dist/src/platforms/channeltalkbot/commands/snapshot.d.ts.map +1 -1
  10. package/dist/src/platforms/channeltalkbot/commands/snapshot.js +110 -60
  11. package/dist/src/platforms/channeltalkbot/commands/snapshot.js.map +1 -1
  12. package/dist/src/platforms/discord/commands/snapshot.d.ts +1 -0
  13. package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
  14. package/dist/src/platforms/discord/commands/snapshot.js +48 -34
  15. package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
  16. package/dist/src/platforms/discordbot/commands/snapshot.d.ts +2 -0
  17. package/dist/src/platforms/discordbot/commands/snapshot.d.ts.map +1 -1
  18. package/dist/src/platforms/discordbot/commands/snapshot.js +46 -34
  19. package/dist/src/platforms/discordbot/commands/snapshot.js.map +1 -1
  20. package/dist/src/platforms/slack/commands/snapshot.d.ts.map +1 -1
  21. package/dist/src/platforms/slack/commands/snapshot.js +75 -55
  22. package/dist/src/platforms/slack/commands/snapshot.js.map +1 -1
  23. package/dist/src/platforms/teams/client.d.ts +9 -1
  24. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  25. package/dist/src/platforms/teams/client.js +69 -18
  26. package/dist/src/platforms/teams/client.js.map +1 -1
  27. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  28. package/dist/src/platforms/teams/commands/auth.js +7 -2
  29. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  30. package/dist/src/platforms/teams/commands/channel.d.ts.map +1 -1
  31. package/dist/src/platforms/teams/commands/channel.js +18 -3
  32. package/dist/src/platforms/teams/commands/channel.js.map +1 -1
  33. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
  34. package/dist/src/platforms/teams/commands/file.js +18 -3
  35. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  36. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  37. package/dist/src/platforms/teams/commands/message.js +24 -4
  38. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  39. package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
  40. package/dist/src/platforms/teams/commands/reaction.js +12 -2
  41. package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
  42. package/dist/src/platforms/teams/commands/snapshot.d.ts +1 -0
  43. package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
  44. package/dist/src/platforms/teams/commands/snapshot.js +50 -32
  45. package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
  46. package/dist/src/platforms/teams/commands/team.d.ts.map +1 -1
  47. package/dist/src/platforms/teams/commands/team.js +6 -1
  48. package/dist/src/platforms/teams/commands/team.js.map +1 -1
  49. package/dist/src/platforms/teams/commands/user.d.ts.map +1 -1
  50. package/dist/src/platforms/teams/commands/user.js +18 -3
  51. package/dist/src/platforms/teams/commands/user.js.map +1 -1
  52. package/dist/src/platforms/teams/commands/whoami.d.ts.map +1 -1
  53. package/dist/src/platforms/teams/commands/whoami.js +6 -1
  54. package/dist/src/platforms/teams/commands/whoami.js.map +1 -1
  55. package/dist/src/platforms/teams/credential-manager.d.ts +3 -1
  56. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  57. package/dist/src/platforms/teams/credential-manager.js +6 -1
  58. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  59. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  60. package/dist/src/platforms/teams/ensure-auth.js +7 -2
  61. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  62. package/dist/src/platforms/teams/token-extractor.d.ts +3 -1
  63. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  64. package/dist/src/platforms/teams/token-extractor.js +67 -10
  65. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  66. package/dist/src/platforms/teams/types.d.ts +17 -0
  67. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  68. package/dist/src/platforms/teams/types.js +2 -0
  69. package/dist/src/platforms/teams/types.js.map +1 -1
  70. package/dist/src/platforms/webex/client.d.ts +3 -0
  71. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  72. package/dist/src/platforms/webex/client.js +58 -13
  73. package/dist/src/platforms/webex/client.js.map +1 -1
  74. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  75. package/dist/src/platforms/webex/commands/auth.js +61 -10
  76. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  77. package/dist/src/platforms/webex/commands/snapshot.d.ts +1 -0
  78. package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -1
  79. package/dist/src/platforms/webex/commands/snapshot.js +14 -7
  80. package/dist/src/platforms/webex/commands/snapshot.js.map +1 -1
  81. package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
  82. package/dist/src/platforms/webex/credential-manager.js +18 -6
  83. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  84. package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
  85. package/dist/src/platforms/webex/encryption.js +3 -1
  86. package/dist/src/platforms/webex/encryption.js.map +1 -1
  87. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  88. package/dist/src/platforms/webex/ensure-auth.js +10 -2
  89. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  90. package/dist/src/platforms/webex/token-extractor.d.ts +1 -0
  91. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  92. package/dist/src/platforms/webex/token-extractor.js +21 -4
  93. package/dist/src/platforms/webex/token-extractor.js.map +1 -1
  94. package/docs/content/docs/agent-skills.mdx +0 -10
  95. package/docs/content/docs/cli/channeltalk.mdx +18 -8
  96. package/docs/content/docs/cli/channeltalkbot.mdx +16 -6
  97. package/docs/content/docs/cli/discord.mdx +23 -7
  98. package/docs/content/docs/cli/discordbot.mdx +23 -7
  99. package/docs/content/docs/cli/slack.mdx +24 -7
  100. package/docs/content/docs/cli/teams.mdx +24 -8
  101. package/docs/content/docs/cli/webex.mdx +15 -2
  102. package/e2e/webex.e2e.test.ts +57 -0
  103. package/package.json +1 -1
  104. package/skills/agent-channeltalk/SKILL.md +19 -9
  105. package/skills/agent-channeltalk/references/common-patterns.md +10 -9
  106. package/skills/agent-channeltalkbot/SKILL.md +19 -9
  107. package/skills/agent-channeltalkbot/references/common-patterns.md +10 -9
  108. package/skills/agent-discord/SKILL.md +18 -9
  109. package/skills/agent-discord/references/common-patterns.md +8 -7
  110. package/skills/agent-discordbot/SKILL.md +18 -9
  111. package/skills/agent-instagram/SKILL.md +1 -1
  112. package/skills/agent-kakaotalk/SKILL.md +1 -1
  113. package/skills/agent-line/SKILL.md +1 -1
  114. package/skills/agent-slack/SKILL.md +19 -10
  115. package/skills/agent-slack/references/common-patterns.md +4 -7
  116. package/skills/agent-slackbot/SKILL.md +1 -1
  117. package/skills/agent-teams/SKILL.md +18 -9
  118. package/skills/agent-teams/references/common-patterns.md +9 -7
  119. package/skills/agent-telegram/SKILL.md +1 -1
  120. package/skills/agent-webex/SKILL.md +13 -4
  121. package/skills/agent-webex/references/common-patterns.md +8 -2
  122. package/skills/agent-wechatbot/SKILL.md +1 -1
  123. package/skills/agent-whatsapp/SKILL.md +1 -1
  124. package/skills/agent-whatsappbot/SKILL.md +1 -1
  125. package/src/platforms/channeltalk/commands/snapshot.test.ts +58 -26
  126. package/src/platforms/channeltalk/commands/snapshot.ts +107 -33
  127. package/src/platforms/channeltalkbot/commands/snapshot.test.ts +26 -8
  128. package/src/platforms/channeltalkbot/commands/snapshot.ts +131 -64
  129. package/src/platforms/discord/commands/snapshot.test.ts +1 -1
  130. package/src/platforms/discord/commands/snapshot.ts +58 -42
  131. package/src/platforms/discordbot/commands/snapshot.test.ts +40 -18
  132. package/src/platforms/discordbot/commands/snapshot.ts +54 -37
  133. package/src/platforms/slack/commands/snapshot.test.ts +63 -8
  134. package/src/platforms/slack/commands/snapshot.ts +98 -66
  135. package/src/platforms/teams/client.test.ts +34 -30
  136. package/src/platforms/teams/client.ts +92 -20
  137. package/src/platforms/teams/commands/auth.test.ts +6 -2
  138. package/src/platforms/teams/commands/auth.ts +7 -2
  139. package/src/platforms/teams/commands/channel.test.ts +6 -6
  140. package/src/platforms/teams/commands/channel.ts +18 -3
  141. package/src/platforms/teams/commands/file.ts +18 -3
  142. package/src/platforms/teams/commands/message.ts +24 -4
  143. package/src/platforms/teams/commands/reaction.ts +12 -2
  144. package/src/platforms/teams/commands/snapshot.test.ts +1 -1
  145. package/src/platforms/teams/commands/snapshot.ts +59 -39
  146. package/src/platforms/teams/commands/team.test.ts +2 -2
  147. package/src/platforms/teams/commands/team.ts +6 -1
  148. package/src/platforms/teams/commands/user.ts +18 -3
  149. package/src/platforms/teams/commands/whoami.ts +6 -1
  150. package/src/platforms/teams/credential-manager.test.ts +25 -0
  151. package/src/platforms/teams/credential-manager.ts +13 -3
  152. package/src/platforms/teams/ensure-auth.test.ts +6 -1
  153. package/src/platforms/teams/ensure-auth.ts +7 -2
  154. package/src/platforms/teams/token-extractor.ts +77 -12
  155. package/src/platforms/teams/types.test.ts +17 -0
  156. package/src/platforms/teams/types.ts +6 -0
  157. package/src/platforms/webex/client.test.ts +157 -13
  158. package/src/platforms/webex/client.ts +64 -15
  159. package/src/platforms/webex/commands/auth.test.ts +122 -1
  160. package/src/platforms/webex/commands/auth.ts +72 -17
  161. package/src/platforms/webex/commands/snapshot.test.ts +14 -1
  162. package/src/platforms/webex/commands/snapshot.ts +17 -9
  163. package/src/platforms/webex/credential-manager.test.ts +63 -0
  164. package/src/platforms/webex/credential-manager.ts +22 -8
  165. package/src/platforms/webex/encryption.test.ts +54 -0
  166. package/src/platforms/webex/encryption.ts +3 -1
  167. package/src/platforms/webex/ensure-auth.ts +10 -2
  168. package/src/platforms/webex/token-extractor.test.ts +32 -3
  169. 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, title, type, lastActivity', async () => {
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: spaces.map((s) => ({
20
- id: s.id,
21
- title: s.title,
22
- type: s.type,
23
- lastActivity: s.lastActivity,
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 spaces overview for AI agents')
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' || config.tokenType === 'extracted') {
48
+ if (config.tokenType === 'manual') {
49
49
  return config.accessToken
50
50
  }
51
51
 
52
- if (config.expiresAt < Date.now() + 5 * 60 * 1000) {
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) return refreshed.accessToken
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
- const config: WebexConfig = {
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({ token })
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({ token: extracted.accessToken })
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('returns first valid token and stops scanning', async () => {
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 token1 = makeWebexStorageJson({ accessToken: 'first-valid-token-longer-than-twenty-chars' })
224
- const token2 = makeWebexStorageJson({ accessToken: 'second-valid-token-longer-than-twenty-chars' })
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
- return token
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
- this.debug('No Webex tokens found in any browser profile')
181
- return null
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> {