agent-messenger 2.21.0 → 2.23.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 (134) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +21 -0
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/webex/client.d.ts +25 -0
  5. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  6. package/dist/src/platforms/webex/client.js +115 -5
  7. package/dist/src/platforms/webex/client.js.map +1 -1
  8. package/dist/src/platforms/webex/commands/auth.d.ts +9 -1
  9. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  10. package/dist/src/platforms/webex/commands/auth.js +141 -25
  11. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  12. package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
  13. package/dist/src/platforms/webex/credential-manager.js +8 -4
  14. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  15. package/dist/src/platforms/webex/id-normalizer.d.ts +19 -0
  16. package/dist/src/platforms/webex/id-normalizer.d.ts.map +1 -0
  17. package/dist/src/platforms/webex/id-normalizer.js +60 -0
  18. package/dist/src/platforms/webex/id-normalizer.js.map +1 -0
  19. package/dist/src/platforms/webex/index.d.ts +4 -0
  20. package/dist/src/platforms/webex/index.d.ts.map +1 -1
  21. package/dist/src/platforms/webex/index.js +2 -0
  22. package/dist/src/platforms/webex/index.js.map +1 -1
  23. package/dist/src/platforms/webex/listener.d.ts +61 -0
  24. package/dist/src/platforms/webex/listener.d.ts.map +1 -0
  25. package/dist/src/platforms/webex/listener.js +222 -0
  26. package/dist/src/platforms/webex/listener.js.map +1 -0
  27. package/dist/src/platforms/webex/password-login.d.ts +18 -0
  28. package/dist/src/platforms/webex/password-login.d.ts.map +1 -0
  29. package/dist/src/platforms/webex/password-login.js +259 -0
  30. package/dist/src/platforms/webex/password-login.js.map +1 -0
  31. package/dist/src/platforms/webex/types.d.ts +2 -1
  32. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  33. package/dist/src/platforms/webex/types.js +1 -1
  34. package/dist/src/platforms/webex/types.js.map +1 -1
  35. package/dist/src/platforms/webex/wdm-discovery.d.ts.map +1 -0
  36. package/dist/src/platforms/{webexbot → webex}/wdm-discovery.js +3 -3
  37. package/dist/src/platforms/webex/wdm-discovery.js.map +1 -0
  38. package/dist/src/platforms/webexbot/cli.d.ts.map +1 -1
  39. package/dist/src/platforms/webexbot/cli.js +4 -1
  40. package/dist/src/platforms/webexbot/cli.js.map +1 -1
  41. package/dist/src/platforms/webexbot/client.d.ts +24 -0
  42. package/dist/src/platforms/webexbot/client.d.ts.map +1 -1
  43. package/dist/src/platforms/webexbot/client.js +81 -5
  44. package/dist/src/platforms/webexbot/client.js.map +1 -1
  45. package/dist/src/platforms/webexbot/commands/file.d.ts +22 -0
  46. package/dist/src/platforms/webexbot/commands/file.d.ts.map +1 -0
  47. package/dist/src/platforms/webexbot/commands/file.js +64 -0
  48. package/dist/src/platforms/webexbot/commands/file.js.map +1 -0
  49. package/dist/src/platforms/webexbot/commands/index.d.ts +3 -0
  50. package/dist/src/platforms/webexbot/commands/index.d.ts.map +1 -1
  51. package/dist/src/platforms/webexbot/commands/index.js +3 -0
  52. package/dist/src/platforms/webexbot/commands/index.js.map +1 -1
  53. package/dist/src/platforms/webexbot/commands/message.d.ts +7 -0
  54. package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -1
  55. package/dist/src/platforms/webexbot/commands/message.js +52 -1
  56. package/dist/src/platforms/webexbot/commands/message.js.map +1 -1
  57. package/dist/src/platforms/webexbot/commands/snapshot.d.ts +24 -0
  58. package/dist/src/platforms/webexbot/commands/snapshot.d.ts.map +1 -0
  59. package/dist/src/platforms/webexbot/commands/snapshot.js +37 -0
  60. package/dist/src/platforms/webexbot/commands/snapshot.js.map +1 -0
  61. package/dist/src/platforms/webexbot/commands/user.d.ts +30 -0
  62. package/dist/src/platforms/webexbot/commands/user.d.ts.map +1 -0
  63. package/dist/src/platforms/webexbot/commands/user.js +66 -0
  64. package/dist/src/platforms/webexbot/commands/user.js.map +1 -0
  65. package/dist/src/platforms/webexbot/index.d.ts +2 -0
  66. package/dist/src/platforms/webexbot/index.d.ts.map +1 -1
  67. package/dist/src/platforms/webexbot/index.js +1 -0
  68. package/dist/src/platforms/webexbot/index.js.map +1 -1
  69. package/dist/src/platforms/webexbot/listener.d.ts +3 -41
  70. package/dist/src/platforms/webexbot/listener.d.ts.map +1 -1
  71. package/dist/src/platforms/webexbot/listener.js +13 -208
  72. package/dist/src/platforms/webexbot/listener.js.map +1 -1
  73. package/dist/src/platforms/webexbot/types.d.ts +1 -18
  74. package/dist/src/platforms/webexbot/types.d.ts.map +1 -1
  75. package/dist/src/platforms/webexbot/types.js.map +1 -1
  76. package/docs/content/docs/cli/webex.mdx +38 -12
  77. package/docs/content/docs/cli/webexbot.mdx +2 -0
  78. package/docs/content/docs/sdk/webexbot.mdx +18 -0
  79. package/package.json +1 -1
  80. package/skills/agent-channeltalk/SKILL.md +1 -1
  81. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  82. package/skills/agent-discord/SKILL.md +1 -1
  83. package/skills/agent-discordbot/SKILL.md +1 -1
  84. package/skills/agent-instagram/SKILL.md +1 -1
  85. package/skills/agent-kakaotalk/SKILL.md +1 -1
  86. package/skills/agent-line/SKILL.md +1 -1
  87. package/skills/agent-slack/SKILL.md +1 -1
  88. package/skills/agent-slackbot/SKILL.md +1 -1
  89. package/skills/agent-teams/SKILL.md +1 -1
  90. package/skills/agent-telegram/SKILL.md +1 -1
  91. package/skills/agent-telegrambot/SKILL.md +1 -1
  92. package/skills/agent-webex/SKILL.md +76 -22
  93. package/skills/agent-webex/references/authentication.md +55 -14
  94. package/skills/agent-webex/references/common-patterns.md +5 -2
  95. package/skills/agent-webexbot/SKILL.md +60 -5
  96. package/skills/agent-webexbot/references/common-patterns.md +118 -0
  97. package/skills/agent-wechatbot/SKILL.md +1 -1
  98. package/skills/agent-whatsapp/SKILL.md +1 -1
  99. package/skills/agent-whatsappbot/SKILL.md +1 -1
  100. package/src/platforms/webex/cli.test.ts +31 -1
  101. package/src/platforms/webex/client.test.ts +67 -0
  102. package/src/platforms/webex/client.ts +136 -7
  103. package/src/platforms/webex/commands/auth.test.ts +189 -28
  104. package/src/platforms/webex/commands/auth.ts +194 -35
  105. package/src/platforms/webex/credential-manager.test.ts +40 -0
  106. package/src/platforms/webex/credential-manager.ts +7 -4
  107. package/src/platforms/webex/id-normalizer.test.ts +207 -0
  108. package/src/platforms/webex/id-normalizer.ts +76 -0
  109. package/src/platforms/webex/index.test.ts +6 -0
  110. package/src/platforms/webex/index.ts +4 -0
  111. package/src/platforms/webex/listener.test.ts +243 -0
  112. package/src/platforms/webex/listener.ts +285 -0
  113. package/src/platforms/webex/password-login.test.ts +193 -0
  114. package/src/platforms/webex/password-login.ts +332 -0
  115. package/src/platforms/webex/types.test.ts +16 -0
  116. package/src/platforms/webex/types.ts +2 -2
  117. package/src/platforms/{webexbot → webex}/wdm-discovery.ts +3 -3
  118. package/src/platforms/webexbot/cli.ts +6 -0
  119. package/src/platforms/webexbot/client.test.ts +322 -0
  120. package/src/platforms/webexbot/client.ts +104 -7
  121. package/src/platforms/webexbot/commands/file.ts +104 -0
  122. package/src/platforms/webexbot/commands/index.ts +3 -0
  123. package/src/platforms/webexbot/commands/message.ts +68 -2
  124. package/src/platforms/webexbot/commands/snapshot.ts +60 -0
  125. package/src/platforms/webexbot/commands/user.test.ts +77 -0
  126. package/src/platforms/webexbot/commands/user.ts +98 -0
  127. package/src/platforms/webexbot/index.ts +2 -0
  128. package/src/platforms/webexbot/listener.test.ts +37 -224
  129. package/src/platforms/webexbot/listener.ts +18 -250
  130. package/src/platforms/webexbot/types.ts +2 -23
  131. package/dist/src/platforms/webexbot/wdm-discovery.d.ts.map +0 -1
  132. package/dist/src/platforms/webexbot/wdm-discovery.js.map +0 -1
  133. /package/dist/src/platforms/{webexbot → webex}/wdm-discovery.d.ts +0 -0
  134. /package/src/platforms/{webexbot → webex}/wdm-discovery.test.ts +0 -0
@@ -0,0 +1,332 @@
1
+ import { createHash, randomBytes } from 'node:crypto'
2
+
3
+ import { WebexError } from './types'
4
+
5
+ export const WEB_CLIENT_ID = 'C64ab04639eefee4798f58e7bc3fe01d47161be0d97ff0d31e040a6ffe66d7f0a'
6
+ export const WEB_CLIENT_SECRET = 'f4261a01a4111b3b3b1710583073cae9cd7104517e7f78800c43d01eea133782'
7
+
8
+ const REDIRECT_URI = 'https://web.webex.com'
9
+ const DEFAULT_IDBROKER_HOST = 'https://idbroker.webex.com'
10
+ const SCOPE =
11
+ 'webexsquare:get_conversation Identity:SCIM identity:things_read spark:kms spark:people_read spark:people_write spark:organizations_read spark:rooms_read spark:rooms_write spark:memberships_read spark:calls_read spark:calls_write webexsquare:admin'
12
+ const U2C_URL = 'https://u2c.svc.webex.com/u2c/api/v1/limited/catalog'
13
+ const MAX_REDIRECTS = 12
14
+
15
+ export interface PasswordLoginOptions {
16
+ idbrokerHost?: string
17
+ }
18
+
19
+ export interface PasswordLoginResult {
20
+ accessToken: string
21
+ refreshToken: string
22
+ expiresAt: number
23
+ deviceUrl: string
24
+ userId: string
25
+ }
26
+
27
+ interface TokenResponse {
28
+ access_token: string
29
+ refresh_token: string
30
+ expires_in: number
31
+ refresh_token_expires_in?: number
32
+ scope?: string
33
+ token_type?: string
34
+ }
35
+
36
+ interface U2CDiscovery {
37
+ idbrokerHost: string
38
+ wdmHost?: string
39
+ }
40
+
41
+ interface RedirectResult {
42
+ response: Response
43
+ url: string
44
+ }
45
+
46
+ export async function loginWithPassword(
47
+ email: string,
48
+ password: string,
49
+ options?: PasswordLoginOptions,
50
+ ): Promise<PasswordLoginResult> {
51
+ const normalizedEmail = email.toLowerCase()
52
+ const emailHash = sha256Hex(normalizedEmail)
53
+ const discovery = await discoverU2C(emailHash)
54
+ const idbrokerHost = normalizeOrigin(options?.idbrokerHost ?? discovery.idbrokerHost)
55
+ const pkce = createPkcePair()
56
+ const state = base64url(Buffer.from(JSON.stringify({ csrf_token: crypto.randomUUID(), emailhash: emailHash })))
57
+ const cookieJar = new CookieJar()
58
+
59
+ const authorizeUrl = `${idbrokerHost}/idb/oauth2/v1/authorize?${new URLSearchParams({
60
+ response_type: 'code',
61
+ cisKeepMeSignedInOption: '1',
62
+ state,
63
+ cisService: 'webex',
64
+ emailHash,
65
+ code_challenge: pkce.challenge,
66
+ code_challenge_method: 'S256',
67
+ client_id: WEB_CLIENT_ID,
68
+ redirect_uri: REDIRECT_URI,
69
+ scope: SCOPE,
70
+ }).toString()}`
71
+
72
+ const authorizeResult = await followRedirects(authorizeUrl, { method: 'GET' }, cookieJar)
73
+ const clusterHost = new URL(authorizeResult.url).origin
74
+ // Reject before touching credentials: if authorize redirected off Webex (an SSO
75
+ // IdP), posting IDToken1/IDToken2 here would leak the password to that host.
76
+ if (!isWebexHost(new URL(clusterHost).host)) {
77
+ throw new WebexError('SSO/IdP login is not supported for headless password login', 'sso_required')
78
+ }
79
+ const loginPageHtml = await authorizeResult.response.text()
80
+ const fields = parseInputFields(loginPageHtml)
81
+ fields.set('IDToken0', '')
82
+ fields.set('IDToken1', email)
83
+ fields.set('IDToken2', password)
84
+ fields.set('IDButton', 'Sign In')
85
+ fields.set('loginid', email)
86
+
87
+ const loginBody = new URLSearchParams()
88
+ for (const [key, value] of fields) loginBody.set(key, value)
89
+
90
+ const loginResult = await followRedirects(
91
+ `${clusterHost}/idb/UI/Login`,
92
+ {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
95
+ body: loginBody.toString(),
96
+ },
97
+ cookieJar,
98
+ )
99
+ const code = getAuthorizationCode(loginResult.url)
100
+
101
+ if (code === null) {
102
+ await throwLoginFailure(loginResult)
103
+ throw new WebexError('Login failed: invalid credentials or unexpected response', 'login_failed')
104
+ }
105
+
106
+ const token = await exchangeCode(clusterHost, code, pkce.verifier)
107
+ const device = await registerDevice(discovery.wdmHost, token.access_token)
108
+
109
+ return {
110
+ accessToken: token.access_token,
111
+ refreshToken: token.refresh_token,
112
+ expiresAt: Date.now() + token.expires_in * 1000,
113
+ deviceUrl: device.deviceUrl,
114
+ userId: device.userId,
115
+ }
116
+ }
117
+
118
+ export function createPkcePair(verifier?: string): { verifier: string; challenge: string } {
119
+ const resolvedVerifier = verifier ?? base64url(randomBytes(64))
120
+ return {
121
+ verifier: resolvedVerifier,
122
+ challenge: base64url(createHash('sha256').update(resolvedVerifier).digest()),
123
+ }
124
+ }
125
+
126
+ async function discoverU2C(emailHash: string): Promise<U2CDiscovery> {
127
+ try {
128
+ const url = `${U2C_URL}?${new URLSearchParams({ format: 'hostmap', emailhash: emailHash }).toString()}`
129
+ const response = await fetch(url)
130
+ if (!response.ok) return { idbrokerHost: DEFAULT_IDBROKER_HOST }
131
+ const catalog = (await response.json()) as { serviceLinks?: Record<string, string> }
132
+ // Match the exact `idbroker` serviceLink key: a substring scan would wrongly
133
+ // pick `idbroker-guest`, routing login to a cluster the user has no account on.
134
+ const links = catalog.serviceLinks ?? {}
135
+ return {
136
+ idbrokerHost: typeof links.idbroker === 'string' ? normalizeOrigin(links.idbroker) : DEFAULT_IDBROKER_HOST,
137
+ wdmHost: typeof links.wdm === 'string' ? normalizeOrigin(links.wdm) : undefined,
138
+ }
139
+ } catch {
140
+ return { idbrokerHost: DEFAULT_IDBROKER_HOST }
141
+ }
142
+ }
143
+
144
+ async function exchangeCode(clusterHost: string, code: string, verifier: string): Promise<TokenResponse> {
145
+ const response = await fetch(`${clusterHost}/idb/oauth2/v1/access_token`, {
146
+ method: 'POST',
147
+ headers: {
148
+ Authorization: `Basic ${Buffer.from(`${WEB_CLIENT_ID}:${WEB_CLIENT_SECRET}`).toString('base64')}`,
149
+ 'Content-Type': 'application/x-www-form-urlencoded',
150
+ },
151
+ body: new URLSearchParams({
152
+ grant_type: 'authorization_code',
153
+ redirect_uri: REDIRECT_URI,
154
+ code,
155
+ code_verifier: verifier,
156
+ self_contained_token: 'true',
157
+ }).toString(),
158
+ })
159
+
160
+ if (!response.ok) {
161
+ throw new WebexError(`Token exchange failed: HTTP ${response.status}`, 'token_exchange_failed')
162
+ }
163
+
164
+ const token = (await response.json()) as Partial<TokenResponse>
165
+ if (!token.access_token || !token.refresh_token || typeof token.expires_in !== 'number') {
166
+ throw new WebexError('Token exchange failed: incomplete response', 'token_exchange_failed')
167
+ }
168
+
169
+ return token as TokenResponse
170
+ }
171
+
172
+ async function registerDevice(
173
+ wdmHost: string | undefined,
174
+ accessToken: string,
175
+ ): Promise<{ deviceUrl: string; userId: string }> {
176
+ // deviceUrl is required: without it WebexClient falls back to the plaintext
177
+ // public API instead of the internal KMS-encrypted path password tokens need.
178
+ if (!wdmHost) {
179
+ throw new WebexError('Webex device registration service was not found in the catalog', 'device_registration_failed')
180
+ }
181
+
182
+ const response = await fetch(`${normalizeOrigin(wdmHost)}/wdm/api/v1/devices`, {
183
+ method: 'POST',
184
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
185
+ body: JSON.stringify({
186
+ deviceName: 'agent-messenger',
187
+ name: 'agent-messenger',
188
+ deviceType: 'UNKNOWN',
189
+ model: 'agent-messenger',
190
+ localizedModel: 'agent-messenger',
191
+ systemName: 'agent-messenger',
192
+ systemVersion: '1.0',
193
+ }),
194
+ })
195
+ if (!response.ok) {
196
+ throw new WebexError(`Webex device registration failed: HTTP ${response.status}`, 'device_registration_failed')
197
+ }
198
+ const data = (await response.json()) as { url?: string; userId?: string }
199
+ if (!data.url || !data.userId) {
200
+ throw new WebexError('Webex device registration returned an incomplete device', 'device_registration_failed')
201
+ }
202
+ return { deviceUrl: data.url, userId: data.userId }
203
+ }
204
+
205
+ async function followRedirects(url: string, init: RequestInit, cookieJar: CookieJar): Promise<RedirectResult> {
206
+ let currentUrl = url
207
+ let response = await fetchWithCookies(currentUrl, init, cookieJar)
208
+ let redirects = 0
209
+
210
+ while (isRedirect(response) && redirects++ < MAX_REDIRECTS) {
211
+ const location = response.headers.get('location')
212
+ if (!location) break
213
+ currentUrl = new URL(location, currentUrl).toString()
214
+ if (currentUrl.startsWith(REDIRECT_URI) && /[?&]code=/.test(currentUrl)) {
215
+ return { response, url: currentUrl }
216
+ }
217
+ response = await fetchWithCookies(currentUrl, { method: 'GET' }, cookieJar)
218
+ }
219
+
220
+ return { response, url: currentUrl }
221
+ }
222
+
223
+ async function fetchWithCookies(url: string, init: RequestInit, cookieJar: CookieJar): Promise<Response> {
224
+ const headers = new Headers(init.headers)
225
+ // Only attach/store session cookies on Webex hosts so a redirect to an external
226
+ // IdP (SSO) never receives the Webex session cookies.
227
+ const isWebex = isWebexHost(new URL(url).host)
228
+ const cookie = cookieJar.header()
229
+ if (cookie && isWebex) headers.set('Cookie', cookie)
230
+ const response = await fetch(url, { ...init, redirect: 'manual', headers })
231
+ if (isWebex) cookieJar.apply(response)
232
+ return response
233
+ }
234
+
235
+ function parseInputFields(html: string): Map<string, string> {
236
+ const fields = new Map<string, string>()
237
+ for (const input of html.match(/<input[^>]*>/gi) ?? []) {
238
+ const attrs = parseAttributes(input)
239
+ const name = attrs.get('name')
240
+ if (name && !fields.has(name)) {
241
+ fields.set(name, decodeEntities(attrs.get('value') ?? ''))
242
+ }
243
+ }
244
+ return fields
245
+ }
246
+
247
+ function parseAttributes(tag: string): Map<string, string> {
248
+ const attrs = new Map<string, string>()
249
+ const attrPattern = /([:\w-]+)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/g
250
+ let match = attrPattern.exec(tag)
251
+ while (match) {
252
+ const rawValue = match[2]
253
+ const value = rawValue.startsWith('"') || rawValue.startsWith("'") ? rawValue.slice(1, -1) : rawValue
254
+ attrs.set(match[1], decodeEntities(value))
255
+ match = attrPattern.exec(tag)
256
+ }
257
+ return attrs
258
+ }
259
+
260
+ function decodeEntities(value: string): string {
261
+ return value
262
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16)))
263
+ .replace(/&#(\d+);/g, (_, decimal: string) => String.fromCodePoint(parseInt(decimal, 10)))
264
+ .replace(/&amp;/g, '&')
265
+ .replace(/&quot;/g, '"')
266
+ .replace(/&#39;/g, "'")
267
+ .replace(/&lt;/g, '<')
268
+ .replace(/&gt;/g, '>')
269
+ }
270
+
271
+ async function throwLoginFailure(loginResult: RedirectResult): Promise<never> {
272
+ const finalHost = new URL(loginResult.url).host
273
+ if (!isWebexHost(finalHost)) {
274
+ throw new WebexError('SSO/IdP login is not supported for headless password login', 'sso_required')
275
+ }
276
+
277
+ const html = await loginResult.response.text()
278
+ if (/verification code|passcode|one-time|security code|\bOTP\b|\bMFA\b/i.test(html)) {
279
+ throw new WebexError('Account requires MFA; headless password login is not supported', 'mfa_required')
280
+ }
281
+
282
+ throw new WebexError('Login failed: invalid credentials or unexpected response', 'login_failed')
283
+ }
284
+
285
+ function getAuthorizationCode(url: string): string | null {
286
+ const code = new URL(url).searchParams.get('code')
287
+ return code ? decodeURIComponent(code) : null
288
+ }
289
+
290
+ function isRedirect(response: Response): boolean {
291
+ return response.status >= 300 && response.status < 400
292
+ }
293
+
294
+ function isWebexHost(host: string): boolean {
295
+ return host === 'webex.com' || host.endsWith('.webex.com') || host === 'wbx2.com' || host.endsWith('.wbx2.com')
296
+ }
297
+
298
+ function normalizeOrigin(value: string): string {
299
+ return new URL(value).origin
300
+ }
301
+
302
+ function sha256Hex(value: string): string {
303
+ return createHash('sha256').update(value).digest('hex')
304
+ }
305
+
306
+ function base64url(buffer: Buffer): string {
307
+ return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
308
+ }
309
+
310
+ class CookieJar {
311
+ private cookies = new Map<string, string>()
312
+
313
+ apply(response: Response): void {
314
+ for (const cookie of getSetCookies(response.headers)) {
315
+ const [pair] = cookie.split(';')
316
+ const index = pair.indexOf('=')
317
+ if (index > 0) this.cookies.set(pair.slice(0, index).trim(), pair.slice(index + 1).trim())
318
+ }
319
+ }
320
+
321
+ header(): string {
322
+ return [...this.cookies.entries()].map(([key, value]) => `${key}=${value}`).join('; ')
323
+ }
324
+ }
325
+
326
+ function getSetCookies(headers: Headers): string[] {
327
+ const headersWithSetCookie = headers as Headers & { getSetCookie?: () => string[] }
328
+ const cookies = headersWithSetCookie.getSetCookie?.()
329
+ if (cookies?.length) return cookies
330
+ const single = headers.get('set-cookie')
331
+ return single ? [single] : []
332
+ }
@@ -266,6 +266,22 @@ it('WebexConfigSchema validates config with tokenType manual', () => {
266
266
  }
267
267
  })
268
268
 
269
+ it('WebexConfigSchema validates config with tokenType password', () => {
270
+ const result = WebexConfigSchema.safeParse({
271
+ accessToken: 'test',
272
+ refreshToken: 'test',
273
+ expiresAt: 1234567890,
274
+ tokenType: 'password',
275
+ deviceUrl: 'https://wdm-test.wbx2.com/wdm/api/v1/devices/device-1',
276
+ userId: 'user-1',
277
+ encryptionKeys: {},
278
+ })
279
+ expect(result.success).toBe(true)
280
+ if (result.success) {
281
+ expect(result.data.tokenType).toBe('password')
282
+ }
283
+ })
284
+
269
285
  it('WebexConfigSchema rejects invalid tokenType', () => {
270
286
  const result = WebexConfigSchema.safeParse({
271
287
  accessToken: 'test',
@@ -55,7 +55,7 @@ export interface WebexConfig {
55
55
  expiresAt: number
56
56
  clientId?: string
57
57
  clientSecret?: string
58
- tokenType?: 'oauth' | 'manual' | 'extracted'
58
+ tokenType?: 'oauth' | 'manual' | 'extracted' | 'password'
59
59
  deviceUrl?: string
60
60
  userId?: string
61
61
  encryptionKeys?: Record<string, string>
@@ -126,7 +126,7 @@ export const WebexConfigSchema = z.object({
126
126
  expiresAt: z.number(),
127
127
  clientId: z.string().optional(),
128
128
  clientSecret: z.string().optional(),
129
- tokenType: z.enum(['oauth', 'manual', 'extracted']).optional(),
129
+ tokenType: z.enum(['oauth', 'manual', 'extracted', 'password']).optional(),
130
130
  deviceUrl: z.string().optional(),
131
131
  userId: z.string().optional(),
132
132
  encryptionKeys: z.record(z.string(), z.string()).optional(),
@@ -1,6 +1,6 @@
1
1
  import type { FetchFunction, FetchRequest, FetchResponse } from 'webex-message-handler'
2
2
 
3
- import { WebexBotError } from './types'
3
+ import { WebexError } from './types'
4
4
 
5
5
  const U2C_CATALOG_URL = 'https://u2c.wbx2.com/u2c/api/v1/catalog?format=hostmap'
6
6
  const HARDCODED_WDM_DEVICES_URL = 'https://wdm-a.wbx2.com/wdm/api/v1/devices'
@@ -14,13 +14,13 @@ const HARDCODED_WDM_DEVICES_URL = 'https://wdm-a.wbx2.com/wdm/api/v1/devices'
14
14
  export async function discoverWdmDevicesUrl(token: string): Promise<string> {
15
15
  const response = await fetch(U2C_CATALOG_URL, { headers: { Authorization: `Bearer ${token}` } })
16
16
  if (!response.ok) {
17
- throw new WebexBotError(`Failed to discover Webex WDM cluster: HTTP ${response.status}`, 'wdm_discovery_failed')
17
+ throw new WebexError(`Failed to discover Webex WDM cluster: HTTP ${response.status}`, 'wdm_discovery_failed')
18
18
  }
19
19
 
20
20
  const catalog = (await response.json()) as { serviceLinks?: { wdm?: string } }
21
21
  const wdm = catalog.serviceLinks?.wdm
22
22
  if (!wdm) {
23
- throw new WebexBotError('Webex U2C catalog did not include serviceLinks.wdm', 'wdm_discovery_failed')
23
+ throw new WebexError('Webex U2C catalog did not include serviceLinks.wdm', 'wdm_discovery_failed')
24
24
  }
25
25
 
26
26
  return `${wdm.replace(/\/$/, '')}/devices`
@@ -5,10 +5,13 @@ import { Command } from 'commander'
5
5
  import pkg from '../../../package.json' with { type: 'json' }
6
6
  import {
7
7
  authCommand,
8
+ fileCommand,
8
9
  listenCommand,
9
10
  memberCommand,
10
11
  messageCommand,
12
+ snapshotCommand,
11
13
  spaceCommand,
14
+ userCommand,
12
15
  whoamiCommand,
13
16
  } from './commands/index'
14
17
 
@@ -35,6 +38,9 @@ program.addCommand(whoamiCommand)
35
38
  program.addCommand(messageCommand)
36
39
  program.addCommand(spaceCommand)
37
40
  program.addCommand(memberCommand)
41
+ program.addCommand(userCommand)
42
+ program.addCommand(fileCommand)
43
+ program.addCommand(snapshotCommand)
38
44
  program.addCommand(listenCommand)
39
45
 
40
46
  program.parseAsync(process.argv)