agent-messenger 2.20.5 → 2.22.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 (137) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +8 -5
  3. package/dist/package.json +9 -1
  4. package/dist/src/cli.d.ts.map +1 -1
  5. package/dist/src/cli.js +3 -0
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/platforms/webex/client.d.ts +19 -0
  8. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  9. package/dist/src/platforms/webex/client.js +81 -1
  10. package/dist/src/platforms/webex/client.js.map +1 -1
  11. package/dist/src/platforms/webexbot/cli.d.ts +5 -0
  12. package/dist/src/platforms/webexbot/cli.d.ts.map +1 -0
  13. package/dist/src/platforms/webexbot/cli.js +33 -0
  14. package/dist/src/platforms/webexbot/cli.js.map +1 -0
  15. package/dist/src/platforms/webexbot/client.d.ts +61 -0
  16. package/dist/src/platforms/webexbot/client.d.ts.map +1 -0
  17. package/dist/src/platforms/webexbot/client.js +80 -0
  18. package/dist/src/platforms/webexbot/client.js.map +1 -0
  19. package/dist/src/platforms/webexbot/commands/auth.d.ts +28 -0
  20. package/dist/src/platforms/webexbot/commands/auth.d.ts.map +1 -0
  21. package/dist/src/platforms/webexbot/commands/auth.js +166 -0
  22. package/dist/src/platforms/webexbot/commands/auth.js.map +1 -0
  23. package/dist/src/platforms/webexbot/commands/file.d.ts +22 -0
  24. package/dist/src/platforms/webexbot/commands/file.d.ts.map +1 -0
  25. package/dist/src/platforms/webexbot/commands/file.js +64 -0
  26. package/dist/src/platforms/webexbot/commands/file.js.map +1 -0
  27. package/dist/src/platforms/webexbot/commands/index.d.ts +10 -0
  28. package/dist/src/platforms/webexbot/commands/index.d.ts.map +1 -0
  29. package/dist/src/platforms/webexbot/commands/index.js +10 -0
  30. package/dist/src/platforms/webexbot/commands/index.js.map +1 -0
  31. package/dist/src/platforms/webexbot/commands/listen.d.ts +12 -0
  32. package/dist/src/platforms/webexbot/commands/listen.d.ts.map +1 -0
  33. package/dist/src/platforms/webexbot/commands/listen.js +85 -0
  34. package/dist/src/platforms/webexbot/commands/listen.js.map +1 -0
  35. package/dist/src/platforms/webexbot/commands/member.d.ts +19 -0
  36. package/dist/src/platforms/webexbot/commands/member.d.ts.map +1 -0
  37. package/dist/src/platforms/webexbot/commands/member.js +33 -0
  38. package/dist/src/platforms/webexbot/commands/member.js.map +1 -0
  39. package/dist/src/platforms/webexbot/commands/message.d.ts +44 -0
  40. package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -0
  41. package/dist/src/platforms/webexbot/commands/message.js +193 -0
  42. package/dist/src/platforms/webexbot/commands/message.js.map +1 -0
  43. package/dist/src/platforms/webexbot/commands/shared.d.ts +9 -0
  44. package/dist/src/platforms/webexbot/commands/shared.d.ts.map +1 -0
  45. package/dist/src/platforms/webexbot/commands/shared.js +13 -0
  46. package/dist/src/platforms/webexbot/commands/shared.js.map +1 -0
  47. package/dist/src/platforms/webexbot/commands/snapshot.d.ts +24 -0
  48. package/dist/src/platforms/webexbot/commands/snapshot.d.ts.map +1 -0
  49. package/dist/src/platforms/webexbot/commands/snapshot.js +37 -0
  50. package/dist/src/platforms/webexbot/commands/snapshot.js.map +1 -0
  51. package/dist/src/platforms/webexbot/commands/space.d.ts +28 -0
  52. package/dist/src/platforms/webexbot/commands/space.d.ts.map +1 -0
  53. package/dist/src/platforms/webexbot/commands/space.js +61 -0
  54. package/dist/src/platforms/webexbot/commands/space.js.map +1 -0
  55. package/dist/src/platforms/webexbot/commands/user.d.ts +30 -0
  56. package/dist/src/platforms/webexbot/commands/user.d.ts.map +1 -0
  57. package/dist/src/platforms/webexbot/commands/user.js +66 -0
  58. package/dist/src/platforms/webexbot/commands/user.js.map +1 -0
  59. package/dist/src/platforms/webexbot/commands/whoami.d.ts +16 -0
  60. package/dist/src/platforms/webexbot/commands/whoami.d.ts.map +1 -0
  61. package/dist/src/platforms/webexbot/commands/whoami.js +29 -0
  62. package/dist/src/platforms/webexbot/commands/whoami.js.map +1 -0
  63. package/dist/src/platforms/webexbot/credential-manager.d.ts +17 -0
  64. package/dist/src/platforms/webexbot/credential-manager.d.ts.map +1 -0
  65. package/dist/src/platforms/webexbot/credential-manager.js +120 -0
  66. package/dist/src/platforms/webexbot/credential-manager.js.map +1 -0
  67. package/dist/src/platforms/webexbot/index.d.ts +9 -0
  68. package/dist/src/platforms/webexbot/index.d.ts.map +1 -0
  69. package/dist/src/platforms/webexbot/index.js +6 -0
  70. package/dist/src/platforms/webexbot/index.js.map +1 -0
  71. package/dist/src/platforms/webexbot/listener.d.ts +44 -0
  72. package/dist/src/platforms/webexbot/listener.d.ts.map +1 -0
  73. package/dist/src/platforms/webexbot/listener.js +214 -0
  74. package/dist/src/platforms/webexbot/listener.js.map +1 -0
  75. package/dist/src/platforms/webexbot/types.d.ts +60 -0
  76. package/dist/src/platforms/webexbot/types.d.ts.map +1 -0
  77. package/dist/src/platforms/webexbot/types.js +28 -0
  78. package/dist/src/platforms/webexbot/types.js.map +1 -0
  79. package/dist/src/platforms/webexbot/wdm-discovery.d.ts +4 -0
  80. package/dist/src/platforms/webexbot/wdm-discovery.d.ts.map +1 -0
  81. package/dist/src/platforms/webexbot/wdm-discovery.js +36 -0
  82. package/dist/src/platforms/webexbot/wdm-discovery.js.map +1 -0
  83. package/docs/content/docs/cli/meta.json +1 -0
  84. package/docs/content/docs/cli/webexbot.mdx +292 -0
  85. package/docs/content/docs/sdk/meta.json +1 -0
  86. package/docs/content/docs/sdk/webexbot.mdx +342 -0
  87. package/docs/src/app/page.tsx +115 -19
  88. package/package.json +9 -1
  89. package/skills/agent-channeltalk/SKILL.md +1 -1
  90. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  91. package/skills/agent-discord/SKILL.md +1 -1
  92. package/skills/agent-discordbot/SKILL.md +1 -1
  93. package/skills/agent-instagram/SKILL.md +1 -1
  94. package/skills/agent-kakaotalk/SKILL.md +1 -1
  95. package/skills/agent-line/SKILL.md +1 -1
  96. package/skills/agent-slack/SKILL.md +1 -1
  97. package/skills/agent-slackbot/SKILL.md +1 -1
  98. package/skills/agent-teams/SKILL.md +1 -1
  99. package/skills/agent-telegram/SKILL.md +1 -1
  100. package/skills/agent-telegrambot/SKILL.md +1 -1
  101. package/skills/agent-webex/SKILL.md +1 -1
  102. package/skills/agent-webexbot/SKILL.md +414 -0
  103. package/skills/agent-webexbot/references/authentication.md +225 -0
  104. package/skills/agent-webexbot/references/common-patterns.md +708 -0
  105. package/skills/agent-wechatbot/SKILL.md +1 -1
  106. package/skills/agent-whatsapp/SKILL.md +1 -1
  107. package/skills/agent-whatsappbot/SKILL.md +1 -1
  108. package/src/cli.ts +4 -0
  109. package/src/platforms/webex/client.test.ts +10 -0
  110. package/src/platforms/webex/client.ts +97 -3
  111. package/src/platforms/webex/typings/webex-message-handler.d.ts +360 -29
  112. package/src/platforms/webexbot/cli.ts +48 -0
  113. package/src/platforms/webexbot/client.test.ts +198 -0
  114. package/src/platforms/webexbot/client.ts +113 -0
  115. package/src/platforms/webexbot/commands/auth.test.ts +185 -0
  116. package/src/platforms/webexbot/commands/auth.ts +210 -0
  117. package/src/platforms/webexbot/commands/file.ts +104 -0
  118. package/src/platforms/webexbot/commands/index.ts +9 -0
  119. package/src/platforms/webexbot/commands/listen.test.ts +20 -0
  120. package/src/platforms/webexbot/commands/listen.ts +104 -0
  121. package/src/platforms/webexbot/commands/member.ts +51 -0
  122. package/src/platforms/webexbot/commands/message.ts +263 -0
  123. package/src/platforms/webexbot/commands/shared.ts +22 -0
  124. package/src/platforms/webexbot/commands/snapshot.ts +60 -0
  125. package/src/platforms/webexbot/commands/space.ts +88 -0
  126. package/src/platforms/webexbot/commands/user.test.ts +77 -0
  127. package/src/platforms/webexbot/commands/user.ts +98 -0
  128. package/src/platforms/webexbot/commands/whoami.ts +43 -0
  129. package/src/platforms/webexbot/credential-manager.test.ts +182 -0
  130. package/src/platforms/webexbot/credential-manager.ts +149 -0
  131. package/src/platforms/webexbot/index.ts +8 -0
  132. package/src/platforms/webexbot/listener.test.ts +234 -0
  133. package/src/platforms/webexbot/listener.ts +255 -0
  134. package/src/platforms/webexbot/types.test.ts +87 -0
  135. package/src/platforms/webexbot/types.ts +72 -0
  136. package/src/platforms/webexbot/wdm-discovery.test.ts +97 -0
  137. package/src/platforms/webexbot/wdm-discovery.ts +43 -0
@@ -0,0 +1,198 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
+
3
+ import { WebexBotClient } from './client'
4
+
5
+ describe('WebexBotClient', () => {
6
+ const originalFetch = globalThis.fetch
7
+ let fetchCalls: Array<{ url: string; options?: RequestInit }> = []
8
+ let fetchResponses: Response[] = []
9
+ let fetchIndex = 0
10
+
11
+ beforeEach(() => {
12
+ fetchCalls = []
13
+ fetchResponses = []
14
+ fetchIndex = 0
15
+ ;(globalThis as { fetch: unknown }).fetch = async (
16
+ url: string | URL | Request,
17
+ options?: RequestInit,
18
+ ): Promise<Response> => {
19
+ fetchCalls.push({ url: url.toString(), options })
20
+ const response = fetchResponses[fetchIndex]
21
+ fetchIndex++
22
+ if (!response) {
23
+ throw new Error('No mock response configured')
24
+ }
25
+ return response
26
+ }
27
+ })
28
+
29
+ afterEach(() => {
30
+ globalThis.fetch = originalFetch
31
+ })
32
+
33
+ const mockResponse = (body: unknown, status = 200) => {
34
+ fetchResponses.push(
35
+ new Response(JSON.stringify(body), {
36
+ status,
37
+ headers: { 'Content-Type': 'application/json' },
38
+ }),
39
+ )
40
+ }
41
+
42
+ describe('listMessages', () => {
43
+ it('limits group-space history to messages that mention the bot', async () => {
44
+ mockResponse({ id: 'group-room', title: 'Team', type: 'group' })
45
+ mockResponse({ items: [{ id: 'msg-1', roomId: 'group-room', roomType: 'group' }] })
46
+
47
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
48
+ const messages = await client.listMessages('group-room', { max: 5 })
49
+
50
+ expect(messages).toHaveLength(1)
51
+ expect(fetchCalls[0].url).toBe('https://webexapis.com/v1/rooms/group-room')
52
+
53
+ const messagesUrl = new URL(fetchCalls[1].url)
54
+ expect(messagesUrl.origin + messagesUrl.pathname).toBe('https://webexapis.com/v1/messages')
55
+ expect(messagesUrl.searchParams.get('roomId')).toBe('group-room')
56
+ expect(messagesUrl.searchParams.get('max')).toBe('5')
57
+ expect(messagesUrl.searchParams.get('mentionedPeople')).toBe('me')
58
+ })
59
+
60
+ it('does not add mentionedPeople for direct spaces', async () => {
61
+ mockResponse({ id: 'direct-room', title: 'DM', type: 'direct' })
62
+ mockResponse({ items: [{ id: 'msg-1', roomId: 'direct-room', roomType: 'direct' }] })
63
+
64
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
65
+ await client.listMessages('direct-room', { max: 5 })
66
+
67
+ const messagesUrl = new URL(fetchCalls[1].url)
68
+ expect(messagesUrl.searchParams.get('roomId')).toBe('direct-room')
69
+ expect(messagesUrl.searchParams.get('mentionedPeople')).toBeNull()
70
+ })
71
+ })
72
+
73
+ describe('sendMessage threading', () => {
74
+ it('includes parentId in the request body when threading', async () => {
75
+ mockResponse({ id: 'msg-1', roomId: 'room-1', roomType: 'group' })
76
+
77
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
78
+ await client.sendMessage('room-1', 'reply text', { parentId: 'parent-1' })
79
+
80
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
81
+ expect(body.roomId).toBe('room-1')
82
+ expect(body.text).toBe('reply text')
83
+ expect(body.parentId).toBe('parent-1')
84
+ })
85
+ })
86
+
87
+ describe('listReplies', () => {
88
+ it('queries messages filtered by parentId', async () => {
89
+ mockResponse({ items: [{ id: 'reply-1', roomId: 'room-1', roomType: 'group', parentId: 'parent-1' }] })
90
+
91
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
92
+ const replies = await client.listReplies('room-1', 'parent-1', { max: 10 })
93
+
94
+ expect(replies).toHaveLength(1)
95
+ const url = new URL(fetchCalls[0].url)
96
+ expect(url.searchParams.get('roomId')).toBe('room-1')
97
+ expect(url.searchParams.get('parentId')).toBe('parent-1')
98
+ expect(url.searchParams.get('max')).toBe('10')
99
+ })
100
+ })
101
+
102
+ describe('getPerson', () => {
103
+ it('fetches a person by id', async () => {
104
+ mockResponse({ id: 'person-1', emails: ['a@b.com'], displayName: 'Alice', type: 'person' })
105
+
106
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
107
+ const person = await client.getPerson('person-1')
108
+
109
+ expect(person.displayName).toBe('Alice')
110
+ expect(fetchCalls[0].url).toBe('https://webexapis.com/v1/people/person-1')
111
+ })
112
+ })
113
+
114
+ describe('uploadFile', () => {
115
+ it('posts multipart form data to messages', async () => {
116
+ mockResponse({
117
+ id: 'msg-1',
118
+ roomId: 'room-1',
119
+ roomType: 'group',
120
+ files: ['https://webexapis.com/v1/contents/c1'],
121
+ })
122
+
123
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
124
+ const message = await client.uploadFile(
125
+ 'room-1',
126
+ { content: new Blob(['hello']), filename: 'note.txt' },
127
+ { text: 'see attached' },
128
+ )
129
+
130
+ expect(message.files).toEqual(['https://webexapis.com/v1/contents/c1'])
131
+ expect(fetchCalls[0].url).toBe('https://webexapis.com/v1/messages')
132
+ expect(fetchCalls[0].options?.method).toBe('POST')
133
+ const body = fetchCalls[0].options?.body as FormData
134
+ expect(body).toBeInstanceOf(FormData)
135
+ expect(body.get('roomId')).toBe('room-1')
136
+ expect(body.get('text')).toBe('see attached')
137
+ })
138
+ })
139
+
140
+ describe('downloadContent', () => {
141
+ it('returns binary data with filename parsed from Content-Disposition', async () => {
142
+ fetchResponses.push(
143
+ new Response('binary-bytes', {
144
+ status: 200,
145
+ headers: {
146
+ 'Content-Disposition': 'attachment; filename="report.pdf"',
147
+ 'Content-Type': 'application/pdf',
148
+ },
149
+ }),
150
+ )
151
+
152
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
153
+ const result = await client.downloadContent('https://webexapis.com/v1/contents/c1')
154
+
155
+ expect(result.filename).toBe('report.pdf')
156
+ expect(result.contentType).toBe('application/pdf')
157
+ expect(fetchCalls[0].url).toBe('https://webexapis.com/v1/contents/c1')
158
+ })
159
+
160
+ it('builds the contents URL from a bare content id', async () => {
161
+ fetchResponses.push(new Response('data', { status: 200, headers: {} }))
162
+
163
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
164
+ const result = await client.downloadContent('abc123')
165
+
166
+ expect(fetchCalls[0].url).toBe('https://webexapis.com/v1/contents/abc123')
167
+ expect(result.filename).toBe('abc123')
168
+ })
169
+
170
+ it('refuses to download from a non-Webex host', async () => {
171
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
172
+
173
+ await expect(client.downloadContent('https://attacker.example/file')).rejects.toThrow(/untrusted/i)
174
+ expect(fetchCalls).toHaveLength(0)
175
+ })
176
+
177
+ it('refuses to download over plain http from the Webex host', async () => {
178
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
179
+
180
+ await expect(client.downloadContent('http://webexapis.com/v1/contents/c1')).rejects.toThrow(/untrusted/i)
181
+ expect(fetchCalls).toHaveLength(0)
182
+ })
183
+
184
+ it('sanitizes a path-traversal filename from Content-Disposition', async () => {
185
+ fetchResponses.push(
186
+ new Response('data', {
187
+ status: 200,
188
+ headers: { 'Content-Disposition': 'attachment; filename="../../etc/passwd"' },
189
+ }),
190
+ )
191
+
192
+ const client = await new WebexBotClient().login({ token: 'bot-token' })
193
+ const result = await client.downloadContent('https://webexapis.com/v1/contents/c1')
194
+
195
+ expect(result.filename).toBe('passwd')
196
+ })
197
+ })
198
+ })
@@ -0,0 +1,113 @@
1
+ import { WebexClient } from '../webex/client'
2
+ import type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from '../webex/types'
3
+ import { WebexBotError } from './types'
4
+
5
+ export class WebexBotClient {
6
+ private client = new WebexClient()
7
+ private token: string | null = null
8
+
9
+ async login(credentials?: { token: string }): Promise<this> {
10
+ if (credentials) {
11
+ if (!credentials.token) {
12
+ throw new WebexBotError('Token is required', 'missing_token')
13
+ }
14
+ this.token = credentials.token
15
+ await this.client.login({ token: credentials.token })
16
+ return this
17
+ }
18
+
19
+ const { WebexBotCredentialManager } = await import('./credential-manager')
20
+ const credManager = new WebexBotCredentialManager()
21
+ const creds = await credManager.getCredentials()
22
+ if (!creds?.token) {
23
+ throw new WebexBotError('No Webex bot credentials found. Run "auth set <token>" first.', 'no_credentials')
24
+ }
25
+ return this.login({ token: creds.token })
26
+ }
27
+
28
+ getToken(): string {
29
+ if (!this.token) {
30
+ throw new WebexBotError('Not authenticated. Call .login() first.', 'not_authenticated')
31
+ }
32
+ return this.token
33
+ }
34
+
35
+ async testAuth(): Promise<WebexPerson> {
36
+ return this.client.testAuth()
37
+ }
38
+
39
+ async listSpaces(options?: { type?: string; max?: number }): Promise<WebexSpace[]> {
40
+ return this.client.listSpaces(options)
41
+ }
42
+
43
+ async getSpace(spaceId: string): Promise<WebexSpace> {
44
+ return this.client.getSpace(spaceId)
45
+ }
46
+
47
+ async sendMessage(
48
+ roomId: string,
49
+ text: string,
50
+ options?: { markdown?: boolean; parentId?: string; files?: string[] },
51
+ ): Promise<WebexMessage> {
52
+ return this.client.sendMessage(roomId, text, options)
53
+ }
54
+
55
+ async sendDirectMessage(personEmail: string, text: string, options?: { markdown?: boolean }): Promise<WebexMessage> {
56
+ return this.client.sendDirectMessage(personEmail, text, options)
57
+ }
58
+
59
+ async listMessages(roomId: string, options?: { max?: number; parentId?: string }): Promise<WebexMessage[]> {
60
+ const space = await this.client.getSpace(roomId)
61
+ const messageOptions = space.type === 'group' ? { ...options, mentionedPeople: 'me' } : options
62
+ return this.client.listMessages(roomId, messageOptions)
63
+ }
64
+
65
+ async listReplies(roomId: string, parentId: string, options?: { max?: number }): Promise<WebexMessage[]> {
66
+ return this.client.listMessages(roomId, { ...options, parentId })
67
+ }
68
+
69
+ async getMessage(messageId: string): Promise<WebexMessage> {
70
+ return this.client.getMessage(messageId)
71
+ }
72
+
73
+ async deleteMessage(messageId: string): Promise<void> {
74
+ return this.client.deleteMessage(messageId)
75
+ }
76
+
77
+ async editMessage(
78
+ messageId: string,
79
+ roomId: string,
80
+ text: string,
81
+ options?: { markdown?: boolean },
82
+ ): Promise<WebexMessage> {
83
+ return this.client.editMessage(messageId, roomId, text, options)
84
+ }
85
+
86
+ async listPeople(options?: { email?: string; displayName?: string; max?: number }): Promise<WebexPerson[]> {
87
+ return this.client.listPeople(options)
88
+ }
89
+
90
+ async getPerson(personId: string): Promise<WebexPerson> {
91
+ return this.client.getPerson(personId)
92
+ }
93
+
94
+ async listMyMemberships(options?: { max?: number }): Promise<WebexMembership[]> {
95
+ return this.client.listMyMemberships(options)
96
+ }
97
+
98
+ async listMemberships(roomId: string, options?: { max?: number }): Promise<WebexMembership[]> {
99
+ return this.client.listMemberships(roomId, options)
100
+ }
101
+
102
+ async uploadFile(
103
+ roomId: string,
104
+ file: { content: Blob; filename: string },
105
+ options?: { text?: string; markdown?: boolean; parentId?: string },
106
+ ): Promise<WebexMessage> {
107
+ return this.client.uploadFile(roomId, file, options)
108
+ }
109
+
110
+ async downloadContent(contentRef: string): Promise<{ data: ArrayBuffer; filename: string; contentType: string }> {
111
+ return this.client.downloadContent(contentRef)
112
+ }
113
+ }
@@ -0,0 +1,185 @@
1
+ import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
2
+ import { existsSync, rmSync } from 'node:fs'
3
+ import { mkdir } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ import type { WebexPerson } from '../../webex/types'
8
+
9
+ const mockTestAuth = mock(() =>
10
+ Promise.resolve({
11
+ id: 'bot123',
12
+ emails: ['bot@example.com'],
13
+ displayName: 'Test Bot',
14
+ orgId: 'org123',
15
+ type: 'bot' as const,
16
+ created: '2024-01-01T00:00:00Z',
17
+ }),
18
+ )
19
+
20
+ mock.module('../client', () => ({
21
+ WebexBotClient: class MockWebexBotClient {
22
+ async login(_credentials?: { token: string }): Promise<this> {
23
+ return this
24
+ }
25
+ testAuth = mockTestAuth
26
+ },
27
+ }))
28
+
29
+ import { WebexBotCredentialManager } from '../credential-manager'
30
+ import { clearAction, listAction, removeAction, setAction, statusAction, useAction } from './auth'
31
+
32
+ describe('webexbot auth commands', () => {
33
+ let tempDir: string
34
+ let originalEnv: NodeJS.ProcessEnv
35
+
36
+ beforeEach(async () => {
37
+ tempDir = join(tmpdir(), `webexbot-auth-test-${Date.now()}`)
38
+ await mkdir(tempDir, { recursive: true })
39
+ originalEnv = { ...process.env }
40
+ delete process.env.E2E_WEBEXBOT_TOKEN
41
+ mockTestAuth.mockClear()
42
+ })
43
+
44
+ afterEach(() => {
45
+ if (existsSync(tempDir)) {
46
+ rmSync(tempDir, { recursive: true })
47
+ }
48
+ process.env = originalEnv
49
+ })
50
+
51
+ it('validates and stores bot token with default bot_id from auth', async () => {
52
+ const manager = new WebexBotCredentialManager(tempDir)
53
+
54
+ const result = await setAction('token123', { _credManager: manager })
55
+
56
+ expect(result.success).toBe(true)
57
+ expect(result.bot_id).toBe('bot123')
58
+ expect(result.bot_name).toBe('Test Bot')
59
+
60
+ const creds = await manager.getCredentials()
61
+ expect(creds?.token).toBe('token123')
62
+ expect(creds?.bot_id).toBe('bot123')
63
+ })
64
+
65
+ it('uses --bot flag as bot_id', async () => {
66
+ const manager = new WebexBotCredentialManager(tempDir)
67
+
68
+ const result = await setAction('token123', { bot: 'mybot', _credManager: manager })
69
+
70
+ expect(result.bot_id).toBe('mybot')
71
+ const creds = await manager.getCredentials('mybot')
72
+ expect(creds?.token).toBe('token123')
73
+ })
74
+
75
+ it('rejects user tokens', async () => {
76
+ mockTestAuth.mockImplementationOnce(() =>
77
+ Promise.resolve({
78
+ id: 'user123',
79
+ emails: ['user@example.com'],
80
+ displayName: 'Test User',
81
+ orgId: 'org123',
82
+ type: 'person',
83
+ created: '2024-01-01T00:00:00Z',
84
+ } satisfies WebexPerson),
85
+ )
86
+
87
+ const manager = new WebexBotCredentialManager(tempDir)
88
+
89
+ const result = await setAction('token123', { _credManager: manager })
90
+
91
+ expect(result.error).toBeDefined()
92
+ expect(result.error).toContain('not a bot token')
93
+ })
94
+
95
+ it('handles client errors', async () => {
96
+ mockTestAuth.mockImplementationOnce(() => Promise.reject(new Error('Invalid token')))
97
+
98
+ const manager = new WebexBotCredentialManager(tempDir)
99
+
100
+ const result = await setAction('invalid', { _credManager: manager })
101
+
102
+ expect(result.error).toBeDefined()
103
+ expect(result.error).toContain('Invalid token')
104
+ })
105
+
106
+ it('clearAction removes all stored credentials', async () => {
107
+ const manager = new WebexBotCredentialManager(tempDir)
108
+ await manager.setCredentials({ token: 'token123', bot_id: 'mybot', bot_name: 'My Bot' })
109
+
110
+ const result = await clearAction({ _credManager: manager })
111
+
112
+ expect(result.success).toBe(true)
113
+ expect(await manager.getCredentials()).toBeNull()
114
+ })
115
+
116
+ it('statusAction returns no credentials when none set', async () => {
117
+ const manager = new WebexBotCredentialManager(tempDir)
118
+
119
+ const result = await statusAction({ _credManager: manager })
120
+
121
+ expect(result.valid).toBe(false)
122
+ expect(result.error).toBeDefined()
123
+ })
124
+
125
+ it('statusAction returns valid status for current bot', async () => {
126
+ const manager = new WebexBotCredentialManager(tempDir)
127
+ await manager.setCredentials({ token: 'token123', bot_id: 'mybot', bot_name: 'My Bot' })
128
+
129
+ const result = await statusAction({ _credManager: manager })
130
+
131
+ expect(result.valid).toBe(true)
132
+ expect(result.bot_id).toBe('bot123')
133
+ expect(result.bot_name).toBe('Test Bot')
134
+ })
135
+
136
+ it('statusAction returns invalid when token test fails', async () => {
137
+ mockTestAuth.mockImplementationOnce(() => Promise.reject(new Error('Unauthorized')))
138
+
139
+ const manager = new WebexBotCredentialManager(tempDir)
140
+ await manager.setCredentials({ token: 'invalid-token', bot_id: 'mybot', bot_name: 'My Bot' })
141
+
142
+ const result = await statusAction({ _credManager: manager })
143
+
144
+ expect(result.valid).toBe(false)
145
+ })
146
+
147
+ it('listAction returns all stored bots', async () => {
148
+ const manager = new WebexBotCredentialManager(tempDir)
149
+ await manager.setCredentials({ token: 'token1', bot_id: 'bot1', bot_name: 'Bot 1' })
150
+ await manager.setCredentials({ token: 'token2', bot_id: 'bot2', bot_name: 'Bot 2' })
151
+
152
+ const result = await listAction({ _credManager: manager })
153
+
154
+ expect(result.bots).toHaveLength(2)
155
+ expect(result.bots?.find((b) => b.bot_id === 'bot2')?.is_current).toBe(true)
156
+ })
157
+
158
+ it('useAction switches current bot', async () => {
159
+ const manager = new WebexBotCredentialManager(tempDir)
160
+ await manager.setCredentials({ token: 'token1', bot_id: 'bot1', bot_name: 'Bot 1' })
161
+ await manager.setCredentials({ token: 'token2', bot_id: 'bot2', bot_name: 'Bot 2' })
162
+
163
+ const result = await useAction('bot1', { _credManager: manager })
164
+
165
+ expect(result.success).toBe(true)
166
+ expect(result.bot_id).toBe('bot1')
167
+ })
168
+
169
+ it('useAction returns error for unknown bot', async () => {
170
+ const manager = new WebexBotCredentialManager(tempDir)
171
+
172
+ const result = await useAction('nonexistent', { _credManager: manager })
173
+ expect(result.error).toBeDefined()
174
+ })
175
+
176
+ it('removeAction removes a stored bot', async () => {
177
+ const manager = new WebexBotCredentialManager(tempDir)
178
+ await manager.setCredentials({ token: 'token1', bot_id: 'bot1', bot_name: 'Bot 1' })
179
+
180
+ const result = await removeAction('bot1', { _credManager: manager })
181
+
182
+ expect(result.success).toBe(true)
183
+ expect(await manager.getCredentials('bot1')).toBeNull()
184
+ })
185
+ })