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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
+
import * as jose from 'node-jose'
|
|
4
|
+
|
|
3
5
|
import { WebexClient } from './client'
|
|
6
|
+
import { WebexEncryptionService } from './encryption'
|
|
4
7
|
import { WebexError } from './types'
|
|
5
8
|
|
|
6
9
|
describe('WebexClient', () => {
|
|
@@ -56,6 +59,17 @@ describe('WebexClient', () => {
|
|
|
56
59
|
await expect(new WebexClient().login({ token: '' })).rejects.toThrow(WebexError)
|
|
57
60
|
await expect(new WebexClient().login({ token: '' })).rejects.toThrow('Token is required')
|
|
58
61
|
})
|
|
62
|
+
|
|
63
|
+
test('accepts deviceUrl and tokenType', async () => {
|
|
64
|
+
const client = await new WebexClient().login({
|
|
65
|
+
token: 'test-token',
|
|
66
|
+
deviceUrl: 'https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1',
|
|
67
|
+
tokenType: 'extracted',
|
|
68
|
+
})
|
|
69
|
+
expect(client).toBeInstanceOf(WebexClient)
|
|
70
|
+
expect((client as any).deviceUrl).toBe('https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1')
|
|
71
|
+
expect((client as any).tokenType).toBe('extracted')
|
|
72
|
+
})
|
|
59
73
|
})
|
|
60
74
|
|
|
61
75
|
describe('testAuth', () => {
|
|
@@ -84,6 +98,59 @@ describe('WebexClient', () => {
|
|
|
84
98
|
const client = await new WebexClient().login({ token: 'bad-token' })
|
|
85
99
|
await expect(client.testAuth()).rejects.toThrow(WebexError)
|
|
86
100
|
})
|
|
101
|
+
|
|
102
|
+
test('falls back to internal API when public API fails for extracted tokens', async () => {
|
|
103
|
+
// given - public API rejects, internal API succeeds
|
|
104
|
+
mockResponse({ message: 'Unauthorized' }, 401)
|
|
105
|
+
fetchResponses.push(
|
|
106
|
+
new Response(JSON.stringify({ id: 'conv-1', activities: { items: [] } }), {
|
|
107
|
+
status: 200,
|
|
108
|
+
headers: { 'Content-Type': 'application/json' },
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const client = await new WebexClient().login({
|
|
113
|
+
token: 'extracted-token',
|
|
114
|
+
deviceUrl: 'https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1',
|
|
115
|
+
tokenType: 'extracted',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// when
|
|
119
|
+
const person = await client.testAuth()
|
|
120
|
+
|
|
121
|
+
// then - succeeds via internal API
|
|
122
|
+
expect(fetchCalls.length).toBe(2)
|
|
123
|
+
expect(fetchCalls[0].url).toBe('https://webexapis.com/v1/people/me')
|
|
124
|
+
expect(fetchCalls[1].url).toContain('conv-r.wbx2.com/conversation/api/v1/conversations')
|
|
125
|
+
expect(person).toBeTruthy()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('throws when both public and internal APIs fail for extracted tokens', async () => {
|
|
129
|
+
// given - both APIs reject
|
|
130
|
+
mockResponse({ message: 'Unauthorized' }, 401)
|
|
131
|
+
fetchResponses.push(
|
|
132
|
+
new Response(JSON.stringify({ message: 'Unauthorized' }), {
|
|
133
|
+
status: 401,
|
|
134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const client = await new WebexClient().login({
|
|
139
|
+
token: 'bad-extracted-token',
|
|
140
|
+
deviceUrl: 'https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1',
|
|
141
|
+
tokenType: 'extracted',
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await expect(client.testAuth()).rejects.toThrow(WebexError)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('does not use internal API fallback for non-extracted tokens', async () => {
|
|
148
|
+
mockResponse({ message: 'Unauthorized' }, 401)
|
|
149
|
+
|
|
150
|
+
const client = await new WebexClient().login({ token: 'bad-token' })
|
|
151
|
+
await expect(client.testAuth()).rejects.toThrow(WebexError)
|
|
152
|
+
expect(fetchCalls.length).toBe(1)
|
|
153
|
+
})
|
|
87
154
|
})
|
|
88
155
|
|
|
89
156
|
describe('listSpaces', () => {
|
|
@@ -402,10 +469,11 @@ describe('WebexClient', () => {
|
|
|
402
469
|
})
|
|
403
470
|
|
|
404
471
|
const createExtractedClient = async () => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
472
|
+
return new WebexClient().login({
|
|
473
|
+
token: 'extracted-token',
|
|
474
|
+
deviceUrl: TEST_DEVICE_URL,
|
|
475
|
+
tokenType: 'extracted',
|
|
476
|
+
})
|
|
409
477
|
}
|
|
410
478
|
|
|
411
479
|
describe('sendMessage', () => {
|
|
@@ -419,7 +487,7 @@ describe('WebexClient', () => {
|
|
|
419
487
|
expect(fetchCalls[0].options?.method).toBe('POST')
|
|
420
488
|
})
|
|
421
489
|
|
|
422
|
-
test('body has verb, object type, displayName
|
|
490
|
+
test('body has verb, object type, and displayName (no content for plain text)', async () => {
|
|
423
491
|
mockResponse(mockActivity('Hello world'))
|
|
424
492
|
|
|
425
493
|
const client = await createExtractedClient()
|
|
@@ -429,7 +497,7 @@ describe('WebexClient', () => {
|
|
|
429
497
|
expect(body.verb).toBe('post')
|
|
430
498
|
expect(body.object.objectType).toBe('comment')
|
|
431
499
|
expect(body.object.displayName).toBe('Hello world')
|
|
432
|
-
expect(body.object.content).
|
|
500
|
+
expect(body.object.content).toBeUndefined()
|
|
433
501
|
})
|
|
434
502
|
|
|
435
503
|
test('body has target with decoded conv UUID and conversation type', async () => {
|
|
@@ -488,7 +556,7 @@ describe('WebexClient', () => {
|
|
|
488
556
|
expect(body.object.markdown).toBeUndefined()
|
|
489
557
|
})
|
|
490
558
|
|
|
491
|
-
test('
|
|
559
|
+
test('plain text messages omit content field', async () => {
|
|
492
560
|
mockResponse(mockActivity('Hello world'))
|
|
493
561
|
|
|
494
562
|
const client = await createExtractedClient()
|
|
@@ -496,7 +564,7 @@ describe('WebexClient', () => {
|
|
|
496
564
|
|
|
497
565
|
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
498
566
|
expect(body.object.displayName).toBe('Hello world')
|
|
499
|
-
expect(body.object.content).
|
|
567
|
+
expect(body.object.content).toBeUndefined()
|
|
500
568
|
})
|
|
501
569
|
})
|
|
502
570
|
|
|
@@ -620,8 +688,11 @@ describe('WebexClient', () => {
|
|
|
620
688
|
})
|
|
621
689
|
|
|
622
690
|
describe('editMessage', () => {
|
|
691
|
+
const mockEditActivity = (text: string, parentId = 'activity-123') =>
|
|
692
|
+
mockActivity(text, { parent: { id: parentId, type: 'edit' } })
|
|
693
|
+
|
|
623
694
|
test('posts activity with verb post and parent edit reference', async () => {
|
|
624
|
-
mockResponse(
|
|
695
|
+
mockResponse(mockEditActivity('Edited text'))
|
|
625
696
|
|
|
626
697
|
const client = await createExtractedClient()
|
|
627
698
|
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
@@ -631,8 +702,8 @@ describe('WebexClient', () => {
|
|
|
631
702
|
expect(body.parent).toEqual({ id: 'activity-123', type: 'edit' })
|
|
632
703
|
})
|
|
633
704
|
|
|
634
|
-
test('
|
|
635
|
-
mockResponse(
|
|
705
|
+
test('plain text edit populates both displayName and content to avoid auto-tombstone', async () => {
|
|
706
|
+
mockResponse(mockEditActivity('Edited text'))
|
|
636
707
|
|
|
637
708
|
const client = await createExtractedClient()
|
|
638
709
|
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
@@ -643,8 +714,18 @@ describe('WebexClient', () => {
|
|
|
643
714
|
expect(body.object.content).toBe('Edited text')
|
|
644
715
|
})
|
|
645
716
|
|
|
717
|
+
test('clientTempId uses -edit suffix to match Webex web client format', async () => {
|
|
718
|
+
mockResponse(mockEditActivity('Edited text'))
|
|
719
|
+
|
|
720
|
+
const client = await createExtractedClient()
|
|
721
|
+
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
722
|
+
|
|
723
|
+
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
724
|
+
expect(body.clientTempId).toMatch(/^tmp-\d+-edit$/)
|
|
725
|
+
})
|
|
726
|
+
|
|
646
727
|
test('target has decoded conv UUID', async () => {
|
|
647
|
-
mockResponse(
|
|
728
|
+
mockResponse(mockEditActivity('Edited text'))
|
|
648
729
|
|
|
649
730
|
const client = await createExtractedClient()
|
|
650
731
|
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
@@ -654,7 +735,7 @@ describe('WebexClient', () => {
|
|
|
654
735
|
})
|
|
655
736
|
|
|
656
737
|
test('markdown option converts content to HTML and strips displayName', async () => {
|
|
657
|
-
mockResponse(
|
|
738
|
+
mockResponse(mockEditActivity('italic text'))
|
|
658
739
|
|
|
659
740
|
const client = await createExtractedClient()
|
|
660
741
|
await client.editMessage('activity-123', TEST_ROOM_ID, '_italic text_', { markdown: true })
|
|
@@ -664,6 +745,69 @@ describe('WebexClient', () => {
|
|
|
664
745
|
expect(body.object.content).toBe('<em>italic text</em>')
|
|
665
746
|
expect(body.object.markdown).toBeUndefined()
|
|
666
747
|
})
|
|
748
|
+
|
|
749
|
+
test('tolerates responses that omit parent (minimal success shape)', async () => {
|
|
750
|
+
mockResponse(mockActivity('Edited text'))
|
|
751
|
+
|
|
752
|
+
const client = await createExtractedClient()
|
|
753
|
+
const message = await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
754
|
+
expect(message.id).toBe('activity-123')
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
test('throws when server returns activity linked to a different parent', async () => {
|
|
758
|
+
mockResponse(mockEditActivity('Edited text', 'activity-999'))
|
|
759
|
+
|
|
760
|
+
const client = await createExtractedClient()
|
|
761
|
+
await expect(client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')).rejects.toThrow(/Edit rejected/)
|
|
762
|
+
})
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
describe('encrypted send and edit', () => {
|
|
766
|
+
const TEST_KEY_URI = 'kms://kms-aore.wbx2.com/keys/test-key-id'
|
|
767
|
+
|
|
768
|
+
const decodeJweHeader = (jwe: string): Record<string, unknown> => {
|
|
769
|
+
const [header = ''] = jwe.split('.')
|
|
770
|
+
const padded = header + '='.repeat((4 - (header.length % 4)) % 4)
|
|
771
|
+
return JSON.parse(Buffer.from(padded, 'base64url').toString('utf8')) as Record<string, unknown>
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const createEncryptedClient = async () => {
|
|
775
|
+
const keystore = jose.JWK.createKeyStore()
|
|
776
|
+
const key = await keystore.generate('oct', 256, { alg: 'A256GCM' })
|
|
777
|
+
const rawKeys = new Map<string, string>([[TEST_KEY_URI, JSON.stringify({ jwk: key.toJSON(true) })]])
|
|
778
|
+
const service = new WebexEncryptionService(rawKeys)
|
|
779
|
+
const client = await createExtractedClient()
|
|
780
|
+
;(client as unknown as { encryption: WebexEncryptionService }).encryption = service
|
|
781
|
+
return client
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
test('plain text send omits content field on encrypted path (preserves prior fix)', async () => {
|
|
785
|
+
mockResponse({ id: TEST_CONV_UUID, defaultActivityEncryptionKeyUrl: TEST_KEY_URI })
|
|
786
|
+
mockResponse(mockActivity('Hello world'))
|
|
787
|
+
|
|
788
|
+
const client = await createEncryptedClient()
|
|
789
|
+
await client.sendMessage(TEST_ROOM_ID, 'Hello world')
|
|
790
|
+
|
|
791
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
792
|
+
expect(body.object.content).toBeUndefined()
|
|
793
|
+
expect(body.object.displayName.startsWith('eyJ')).toBe(true)
|
|
794
|
+
expect(body.encryptionKeyUrl).toBe(TEST_KEY_URI)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
test('plain text edit encrypts both displayName and content with kid in JWE header', async () => {
|
|
798
|
+
mockResponse({ id: TEST_CONV_UUID, defaultActivityEncryptionKeyUrl: TEST_KEY_URI })
|
|
799
|
+
mockResponse(mockActivity('Edited text', { parent: { id: 'activity-123', type: 'edit' } }))
|
|
800
|
+
|
|
801
|
+
const client = await createEncryptedClient()
|
|
802
|
+
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
803
|
+
|
|
804
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
805
|
+
expect(body.object.displayName.startsWith('eyJ')).toBe(true)
|
|
806
|
+
expect(body.object.content.startsWith('eyJ')).toBe(true)
|
|
807
|
+
expect(body.encryptionKeyUrl).toBe(TEST_KEY_URI)
|
|
808
|
+
expect(decodeJweHeader(body.object.displayName).kid).toBe(TEST_KEY_URI)
|
|
809
|
+
expect(decodeJweHeader(body.object.content).kid).toBe(TEST_KEY_URI)
|
|
810
|
+
})
|
|
667
811
|
})
|
|
668
812
|
|
|
669
813
|
describe('sendDirectMessage', () => {
|
|
@@ -21,12 +21,14 @@ export class WebexClient {
|
|
|
21
21
|
private globalRateLimitUntil: number = 0
|
|
22
22
|
private encryption: WebexEncryptionService | null = null
|
|
23
23
|
|
|
24
|
-
async login(credentials?: { token: string }): Promise<this> {
|
|
24
|
+
async login(credentials?: { token: string; deviceUrl?: string; tokenType?: string }): Promise<this> {
|
|
25
25
|
if (credentials) {
|
|
26
26
|
if (!credentials.token) {
|
|
27
27
|
throw new WebexError('Token is required', 'missing_token')
|
|
28
28
|
}
|
|
29
29
|
this.token = credentials.token
|
|
30
|
+
if (credentials.deviceUrl !== undefined) this.deviceUrl = credentials.deviceUrl
|
|
31
|
+
if (credentials.tokenType !== undefined) this.tokenType = credentials.tokenType
|
|
30
32
|
return this
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -161,9 +163,28 @@ export class WebexClient {
|
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
async testAuth(): Promise<WebexPerson> {
|
|
166
|
+
if (this.useInternalAPI) {
|
|
167
|
+
try {
|
|
168
|
+
return await this.request<WebexPerson>('GET', '/people/me')
|
|
169
|
+
} catch (err) {
|
|
170
|
+
const isAuthError = err instanceof WebexError && (err.code === 'http_401' || err.code === 'http_403')
|
|
171
|
+
if (!isAuthError) throw err
|
|
172
|
+
await this.testAuthInternal()
|
|
173
|
+
return { id: '', emails: [], displayName: '', orgId: '', type: 'person', created: '' } as WebexPerson
|
|
174
|
+
}
|
|
175
|
+
}
|
|
164
176
|
return this.request<WebexPerson>('GET', '/people/me')
|
|
165
177
|
}
|
|
166
178
|
|
|
179
|
+
private async testAuthInternal(): Promise<void> {
|
|
180
|
+
if (!this.deviceUrl) {
|
|
181
|
+
throw new WebexError('No device URL available for internal API validation', 'no_device_url')
|
|
182
|
+
}
|
|
183
|
+
await this.internalRequest<InternalConversation>(
|
|
184
|
+
'/conversations?participantsLimit=0&activitiesLimit=0&conversationsLimit=1',
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
167
188
|
async listSpaces(options?: { type?: string; max?: number }): Promise<WebexSpace[]> {
|
|
168
189
|
const params = new URLSearchParams()
|
|
169
190
|
if (options?.type) params.set('type', options.type)
|
|
@@ -248,10 +269,15 @@ export class WebexClient {
|
|
|
248
269
|
private async buildEncryptedObject(
|
|
249
270
|
convUuid: string,
|
|
250
271
|
text: string,
|
|
251
|
-
options?: { markdown?: boolean },
|
|
272
|
+
options?: { markdown?: boolean; forEdit?: boolean },
|
|
252
273
|
): Promise<{ object: Record<string, string>; encryptionKeyUrl?: string }> {
|
|
253
274
|
const displayName = options?.markdown ? stripMarkdown(text) : text
|
|
254
|
-
|
|
275
|
+
let content: string | undefined
|
|
276
|
+
if (options?.markdown) {
|
|
277
|
+
content = markdownToHtml(text)
|
|
278
|
+
} else if (options?.forEdit) {
|
|
279
|
+
content = text
|
|
280
|
+
}
|
|
255
281
|
|
|
256
282
|
if (this.encryption) {
|
|
257
283
|
const conv = await this.internalRequest<InternalConversation>(
|
|
@@ -260,21 +286,25 @@ export class WebexClient {
|
|
|
260
286
|
const keyUri = conv.defaultActivityEncryptionKeyUrl
|
|
261
287
|
if (keyUri) {
|
|
262
288
|
const encryptedDisplayName = await this.encryption.encryptText(keyUri, displayName)
|
|
263
|
-
const encryptedContent = await this.encryption.encryptText(keyUri, content)
|
|
264
|
-
if (encryptedDisplayName
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
encryptionKeyUrl: keyUri,
|
|
289
|
+
const encryptedContent = content ? await this.encryption.encryptText(keyUri, content) : undefined
|
|
290
|
+
if (encryptedDisplayName) {
|
|
291
|
+
const object: Record<string, string> = {
|
|
292
|
+
objectType: 'comment',
|
|
293
|
+
displayName: encryptedDisplayName,
|
|
294
|
+
}
|
|
295
|
+
if (encryptedContent) {
|
|
296
|
+
object.content = encryptedContent
|
|
272
297
|
}
|
|
298
|
+
return { object, encryptionKeyUrl: keyUri }
|
|
273
299
|
}
|
|
274
300
|
}
|
|
275
301
|
}
|
|
276
302
|
|
|
277
|
-
|
|
303
|
+
const object: Record<string, string> = { objectType: 'comment', displayName }
|
|
304
|
+
if (content) {
|
|
305
|
+
object.content = content
|
|
306
|
+
}
|
|
307
|
+
return { object }
|
|
278
308
|
}
|
|
279
309
|
|
|
280
310
|
private async sendMessageInternal(
|
|
@@ -349,6 +379,11 @@ export class WebexClient {
|
|
|
349
379
|
if (this.useInternalAPI) {
|
|
350
380
|
const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
|
|
351
381
|
const convId = activity.target?.id ?? ''
|
|
382
|
+
// Internal API responses don't carry the cluster shard (e.g. `us-west-2_r`) the
|
|
383
|
+
// public roomId encoding requires. The `unknown` placeholder is a sentinel — it
|
|
384
|
+
// round-trips through other internal API calls because they decode only the
|
|
385
|
+
// conversation UUID suffix. Callers that need a public-API-safe roomId should
|
|
386
|
+
// obtain it from `listSpaces()` or pass it through from a prior `sendMessage`.
|
|
352
387
|
const roomId = convId ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64') : ''
|
|
353
388
|
return this.activityToMessage(activity, roomId)
|
|
354
389
|
}
|
|
@@ -381,14 +416,17 @@ export class WebexClient {
|
|
|
381
416
|
): Promise<WebexMessage> {
|
|
382
417
|
if (this.useInternalAPI) {
|
|
383
418
|
const convUuid = this.decodeConvUuid(roomId)
|
|
384
|
-
const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text,
|
|
419
|
+
const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, {
|
|
420
|
+
...options,
|
|
421
|
+
forEdit: true,
|
|
422
|
+
})
|
|
385
423
|
|
|
386
424
|
const activity: Record<string, unknown> = {
|
|
387
425
|
verb: 'post',
|
|
388
426
|
object,
|
|
389
427
|
target: { id: convUuid, objectType: 'conversation' },
|
|
390
428
|
parent: { id: messageId, type: 'edit' },
|
|
391
|
-
clientTempId: `tmp-${Date.now()}`,
|
|
429
|
+
clientTempId: `tmp-${Date.now()}-edit`,
|
|
392
430
|
}
|
|
393
431
|
|
|
394
432
|
if (encryptionKeyUrl) {
|
|
@@ -399,6 +437,16 @@ export class WebexClient {
|
|
|
399
437
|
method: 'POST',
|
|
400
438
|
body: JSON.stringify(activity),
|
|
401
439
|
})
|
|
440
|
+
|
|
441
|
+
// Tolerate responses that omit `parent` (server may return minimal shape) —
|
|
442
|
+
// only fail on an explicit mismatch between the echoed parent and the edited id.
|
|
443
|
+
if (result.parent && result.parent.id !== messageId) {
|
|
444
|
+
throw new WebexError(
|
|
445
|
+
`Edit rejected: server linked the new activity ${result.id} to ${result.parent.id} instead of ${messageId}.`,
|
|
446
|
+
'edit_failed',
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
402
450
|
return this.activityToMessage(result, roomId)
|
|
403
451
|
}
|
|
404
452
|
const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
|
|
@@ -443,6 +491,7 @@ interface InternalActivity {
|
|
|
443
491
|
encryptionKeyUrl?: string
|
|
444
492
|
}
|
|
445
493
|
target?: { id: string; encryptionKeyUrl?: string }
|
|
494
|
+
parent?: { id: string; type: string }
|
|
446
495
|
published: string
|
|
447
496
|
encryptionKeyUrl?: string
|
|
448
497
|
}
|
|
@@ -3,7 +3,9 @@ import * as childProcess from 'node:child_process'
|
|
|
3
3
|
|
|
4
4
|
import { WebexClient } from '../client'
|
|
5
5
|
import { WebexCredentialManager } from '../credential-manager'
|
|
6
|
-
import {
|
|
6
|
+
import { WebexTokenExtractor } from '../token-extractor'
|
|
7
|
+
import { WebexError } from '../types'
|
|
8
|
+
import { extractAction, loginAction, logoutAction, statusAction } from './auth'
|
|
7
9
|
|
|
8
10
|
describe('auth commands', () => {
|
|
9
11
|
let consoleSpy: ReturnType<typeof spyOn>
|
|
@@ -208,6 +210,125 @@ describe('auth commands', () => {
|
|
|
208
210
|
})
|
|
209
211
|
})
|
|
210
212
|
|
|
213
|
+
describe('extractAction', () => {
|
|
214
|
+
test('passes deviceUrl and tokenType to client.login', async () => {
|
|
215
|
+
protoSpy(WebexTokenExtractor.prototype, 'extract').mockResolvedValue({
|
|
216
|
+
accessToken: 'extracted-token-at-least-twenty-chars',
|
|
217
|
+
refreshToken: 'refresh-token',
|
|
218
|
+
expiresAt: Date.now() + 3600000,
|
|
219
|
+
deviceUrl: 'https://wdm-r.wbx2.com/wdm/api/v1/devices/test-device-id',
|
|
220
|
+
userId: 'user-1',
|
|
221
|
+
})
|
|
222
|
+
const loginSpy = protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
223
|
+
protoSpy(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
224
|
+
protoSpy(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
|
|
225
|
+
|
|
226
|
+
await extractAction({ pretty: false })
|
|
227
|
+
|
|
228
|
+
expect(loginSpy).toHaveBeenCalledWith({
|
|
229
|
+
token: 'extracted-token-at-least-twenty-chars',
|
|
230
|
+
deviceUrl: 'https://wdm-r.wbx2.com/wdm/api/v1/devices/test-device-id',
|
|
231
|
+
tokenType: 'extracted',
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('attempts refresh when token is expired', async () => {
|
|
236
|
+
protoSpy(WebexTokenExtractor.prototype, 'extract').mockResolvedValue({
|
|
237
|
+
accessToken: 'expired-token-at-least-twenty-chars-',
|
|
238
|
+
refreshToken: 'valid-refresh-token',
|
|
239
|
+
expiresAt: Date.now() - 7200000,
|
|
240
|
+
})
|
|
241
|
+
const refreshSpy = protoSpy(WebexCredentialManager.prototype, 'refreshToken').mockResolvedValue({
|
|
242
|
+
accessToken: 'refreshed-token-at-least-twenty-ch',
|
|
243
|
+
refreshToken: 'new-refresh',
|
|
244
|
+
expiresAt: Date.now() + 3600000,
|
|
245
|
+
})
|
|
246
|
+
const loginSpy = protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
247
|
+
protoSpy(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
248
|
+
protoSpy(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
|
|
249
|
+
|
|
250
|
+
await extractAction({ pretty: false })
|
|
251
|
+
|
|
252
|
+
expect(refreshSpy).toHaveBeenCalled()
|
|
253
|
+
expect(loginSpy).toHaveBeenCalledWith(expect.objectContaining({ token: 'refreshed-token-at-least-twenty-ch' }))
|
|
254
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
255
|
+
const output = JSON.parse(lastCall)
|
|
256
|
+
expect(output.authenticated).toBe(true)
|
|
257
|
+
expect(output.refreshed).toBe(true)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('reports expired token with actionable hint when refresh fails', async () => {
|
|
261
|
+
protoSpy(WebexTokenExtractor.prototype, 'extract').mockResolvedValue({
|
|
262
|
+
accessToken: 'expired-token-at-least-twenty-chars-',
|
|
263
|
+
refreshToken: 'bad-refresh-token',
|
|
264
|
+
expiresAt: Date.now() - 7200000,
|
|
265
|
+
})
|
|
266
|
+
protoSpy(WebexCredentialManager.prototype, 'refreshToken').mockResolvedValue(null)
|
|
267
|
+
protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
268
|
+
protoSpy(WebexClient.prototype, 'testAuth').mockRejectedValue(new WebexError('Unauthorized', 'http_401'))
|
|
269
|
+
const exitSpy = protoSpy(process, 'exit').mockImplementation(() => undefined as never)
|
|
270
|
+
|
|
271
|
+
await extractAction({ pretty: false })
|
|
272
|
+
|
|
273
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
274
|
+
const output = JSON.parse(lastCall)
|
|
275
|
+
expect(output.error).toContain('expired')
|
|
276
|
+
expect(output.hint).toContain('web.webex.com')
|
|
277
|
+
expect(output.hint).toContain('not webex.com')
|
|
278
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('rethrows non-auth errors even when token is expired', async () => {
|
|
282
|
+
protoSpy(WebexTokenExtractor.prototype, 'extract').mockResolvedValue({
|
|
283
|
+
accessToken: 'expired-token-at-least-twenty-chars-',
|
|
284
|
+
refreshToken: 'bad-refresh-token',
|
|
285
|
+
expiresAt: Date.now() - 7200000,
|
|
286
|
+
})
|
|
287
|
+
protoSpy(WebexCredentialManager.prototype, 'refreshToken').mockResolvedValue(null)
|
|
288
|
+
protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
289
|
+
protoSpy(WebexClient.prototype, 'testAuth').mockRejectedValue(new Error('Network error'))
|
|
290
|
+
protoSpy(process, 'exit').mockImplementation(() => undefined as never)
|
|
291
|
+
|
|
292
|
+
await extractAction({ pretty: false })
|
|
293
|
+
|
|
294
|
+
const lastCall = consoleErrorSpy.mock.calls[consoleErrorSpy.mock.calls.length - 1]?.[0] as string | undefined
|
|
295
|
+
if (lastCall) {
|
|
296
|
+
const output = JSON.parse(lastCall)
|
|
297
|
+
expect(output.error).toContain('Network error')
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('rethrows non-expiry auth errors', async () => {
|
|
302
|
+
protoSpy(WebexTokenExtractor.prototype, 'extract').mockResolvedValue({
|
|
303
|
+
accessToken: 'valid-token-at-least-twenty-chars-xx',
|
|
304
|
+
expiresAt: Date.now() + 3600000,
|
|
305
|
+
})
|
|
306
|
+
protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
307
|
+
protoSpy(WebexClient.prototype, 'testAuth').mockRejectedValue(new Error('Network error'))
|
|
308
|
+
protoSpy(process, 'exit').mockImplementation(() => undefined as never)
|
|
309
|
+
|
|
310
|
+
await extractAction({ pretty: false })
|
|
311
|
+
|
|
312
|
+
const lastCall = consoleErrorSpy.mock.calls[consoleErrorSpy.mock.calls.length - 1]?.[0] as string | undefined
|
|
313
|
+
if (lastCall) {
|
|
314
|
+
const output = JSON.parse(lastCall)
|
|
315
|
+
expect(output.error).toContain('Network error')
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test('outputs no token found when extract returns null', async () => {
|
|
320
|
+
protoSpy(WebexTokenExtractor.prototype, 'extract').mockResolvedValue(null)
|
|
321
|
+
const exitSpy = protoSpy(process, 'exit').mockImplementation(() => undefined as never)
|
|
322
|
+
|
|
323
|
+
await extractAction({ pretty: false })
|
|
324
|
+
|
|
325
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
326
|
+
const output = JSON.parse(lastCall)
|
|
327
|
+
expect(output.error).toContain('No Webex token found')
|
|
328
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
211
332
|
describe('logoutAction', () => {
|
|
212
333
|
test('clears credentials when authenticated', async () => {
|
|
213
334
|
protoSpy(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue({
|
|
@@ -8,6 +8,7 @@ import { getWebexAppCredentials } from '../app-config'
|
|
|
8
8
|
import { WebexClient } from '../client'
|
|
9
9
|
import { WebexCredentialManager } from '../credential-manager'
|
|
10
10
|
import { WebexTokenExtractor } from '../token-extractor'
|
|
11
|
+
import { WebexError } from '../types'
|
|
11
12
|
|
|
12
13
|
interface ResolvedCredentials {
|
|
13
14
|
clientId: string
|
|
@@ -142,7 +143,8 @@ export async function statusAction(options: { pretty?: boolean }): Promise<void>
|
|
|
142
143
|
|
|
143
144
|
export async function extractAction(options: { pretty?: boolean; debug?: boolean }): Promise<void> {
|
|
144
145
|
try {
|
|
145
|
-
const
|
|
146
|
+
const debugLog = options.debug ? (msg: string) => debug(`[debug] ${msg}`) : undefined
|
|
147
|
+
const extractor = new WebexTokenExtractor(undefined, debugLog)
|
|
146
148
|
|
|
147
149
|
if (options.debug) {
|
|
148
150
|
debug('[debug] Searching browser profiles for Webex tokens...')
|
|
@@ -155,7 +157,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
|
|
|
155
157
|
formatOutput(
|
|
156
158
|
{
|
|
157
159
|
error:
|
|
158
|
-
'No Webex token found in any browser. Make sure you are logged in
|
|
160
|
+
'No Webex token found in any browser. Make sure you are logged in at https://web.webex.com (not webex.com) in Chrome, Edge, Arc, or Brave.',
|
|
159
161
|
hint: 'Run "auth login" for OAuth Device Grant flow, or --debug for more info.',
|
|
160
162
|
},
|
|
161
163
|
options.pretty,
|
|
@@ -165,30 +167,83 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
|
|
|
165
167
|
return
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
+
const isExpired = extracted.expiresAt != null && extracted.expiresAt > 0 && extracted.expiresAt < Date.now()
|
|
171
|
+
if (isExpired && options.debug) {
|
|
172
|
+
const agoMs = Date.now() - extracted.expiresAt!
|
|
173
|
+
const agoHours = Math.round(agoMs / 3_600_000)
|
|
174
|
+
debugLog?.(`Token expired ${agoHours > 0 ? `${agoHours}h ago` : 'recently'}.`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let activeToken = extracted.accessToken
|
|
178
|
+
let refreshedConfig: { accessToken: string; refreshToken: string; expiresAt: number } | null = null
|
|
179
|
+
|
|
180
|
+
if (isExpired && extracted.refreshToken) {
|
|
181
|
+
debugLog?.('Attempting token refresh...')
|
|
182
|
+
const credManager = new WebexCredentialManager()
|
|
183
|
+
const { clientId, clientSecret } = getWebexAppCredentials()
|
|
184
|
+
refreshedConfig = await credManager.refreshToken(extracted.refreshToken, clientId, clientSecret)
|
|
185
|
+
if (refreshedConfig) {
|
|
186
|
+
debugLog?.('Token refreshed successfully.')
|
|
187
|
+
activeToken = refreshedConfig.accessToken
|
|
188
|
+
} else {
|
|
189
|
+
debugLog?.('Token refresh failed. Will attempt validation with expired token.')
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const client = await new WebexClient().login({
|
|
194
|
+
token: activeToken,
|
|
195
|
+
deviceUrl: extracted.deviceUrl,
|
|
196
|
+
tokenType: 'extracted',
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
let person: { id: string; displayName: string; emails: string[] } | null = null
|
|
200
|
+
try {
|
|
201
|
+
const result = await client.testAuth()
|
|
202
|
+
if (result.id) {
|
|
203
|
+
person = { id: result.id, displayName: result.displayName, emails: result.emails }
|
|
204
|
+
}
|
|
205
|
+
} catch (authError) {
|
|
206
|
+
const isAuthFailure =
|
|
207
|
+
authError instanceof WebexError && (authError.code === 'http_401' || authError.code === 'http_403')
|
|
208
|
+
if (isExpired && isAuthFailure) {
|
|
209
|
+
console.log(
|
|
210
|
+
formatOutput(
|
|
211
|
+
{
|
|
212
|
+
error: 'Extracted browser token is expired and could not be refreshed.',
|
|
213
|
+
hint: 'Log in at https://web.webex.com (not webex.com) in your browser, then run "auth extract" again. Or use "auth login" for OAuth Device Grant flow.',
|
|
214
|
+
},
|
|
215
|
+
options.pretty,
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
process.exit(1)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
throw authError
|
|
222
|
+
}
|
|
170
223
|
|
|
171
224
|
const credManager = new WebexCredentialManager()
|
|
172
225
|
await credManager.saveConfig({
|
|
173
|
-
accessToken:
|
|
174
|
-
refreshToken: extracted.refreshToken ?? '',
|
|
175
|
-
expiresAt: extracted.expiresAt ?? 0,
|
|
226
|
+
accessToken: activeToken,
|
|
227
|
+
refreshToken: refreshedConfig?.refreshToken ?? extracted.refreshToken ?? '',
|
|
228
|
+
expiresAt: refreshedConfig?.expiresAt ?? extracted.expiresAt ?? 0,
|
|
176
229
|
tokenType: 'extracted',
|
|
177
230
|
deviceUrl: extracted.deviceUrl,
|
|
178
231
|
userId: extracted.userId,
|
|
179
232
|
encryptionKeys: extracted.encryptionKeys ? Object.fromEntries(extracted.encryptionKeys) : undefined,
|
|
180
233
|
})
|
|
181
234
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
235
|
+
const output: Record<string, unknown> = {
|
|
236
|
+
authenticated: true,
|
|
237
|
+
tokenType: 'extracted',
|
|
238
|
+
}
|
|
239
|
+
if (refreshedConfig) {
|
|
240
|
+
output['refreshed'] = true
|
|
241
|
+
}
|
|
242
|
+
if (person) {
|
|
243
|
+
output['user'] = person
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(formatOutput(output, options.pretty))
|
|
192
247
|
} catch (error) {
|
|
193
248
|
handleError(error as Error)
|
|
194
249
|
}
|