agent-messenger 2.1.0 → 2.3.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/.env.template +35 -17
- package/README.md +7 -7
- package/bun.lock +31 -7
- package/dist/package.json +5 -3
- package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/commands/auth.js +35 -28
- package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/channeltalk/ensure-auth.js +6 -6
- package/dist/src/platforms/channeltalk/ensure-auth.js.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts +23 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.js +299 -29
- package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
- package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/auth.js +57 -49
- package/dist/src/platforms/discord/commands/auth.js.map +1 -1
- package/dist/src/platforms/discord/ensure-auth.js +3 -3
- package/dist/src/platforms/discord/ensure-auth.js.map +1 -1
- package/dist/src/platforms/discord/token-extractor.d.ts +6 -1
- package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/discord/token-extractor.js +167 -14
- package/dist/src/platforms/discord/token-extractor.js.map +1 -1
- package/dist/src/platforms/instagram/client.d.ts +2 -0
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +2 -2
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +107 -14
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/ensure-auth.js +57 -11
- package/dist/src/platforms/instagram/ensure-auth.js.map +1 -1
- package/dist/src/platforms/instagram/index.d.ts +1 -0
- package/dist/src/platforms/instagram/index.d.ts.map +1 -1
- package/dist/src/platforms/instagram/index.js +1 -0
- package/dist/src/platforms/instagram/index.js.map +1 -1
- package/dist/src/platforms/instagram/token-extractor.d.ts +44 -0
- package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -0
- package/dist/src/platforms/instagram/token-extractor.js +407 -0
- package/dist/src/platforms/instagram/token-extractor.js.map +1 -0
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +2 -1
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.js +14 -13
- package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js +2 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
- package/dist/src/platforms/line/client.d.ts.map +1 -1
- package/dist/src/platforms/line/client.js +36 -9
- package/dist/src/platforms/line/client.js.map +1 -1
- package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/line/commands/auth.js +6 -5
- package/dist/src/platforms/line/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/auth.js +11 -10
- package/dist/src/platforms/slack/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
- package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/slack/token-extractor.js +300 -23
- package/dist/src/platforms/slack/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +9 -8
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +2 -1
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +161 -29
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/telegram/client.d.ts.map +1 -1
- package/dist/src/platforms/telegram/client.js +25 -7
- package/dist/src/platforms/telegram/client.js.map +1 -1
- package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/telegram/commands/auth.js +6 -5
- package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +12 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +168 -1
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts +4 -0
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +50 -4
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/credential-manager.js +1 -1
- package/dist/src/platforms/webex/credential-manager.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts +10 -0
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
- package/dist/src/platforms/webex/encryption.js +49 -0
- package/dist/src/platforms/webex/encryption.js.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.js +25 -5
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
- package/dist/src/platforms/webex/index.d.ts +2 -0
- package/dist/src/platforms/webex/index.d.ts.map +1 -1
- package/dist/src/platforms/webex/index.js +1 -0
- package/dist/src/platforms/webex/index.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +29 -0
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
- package/dist/src/platforms/webex/token-extractor.js +393 -0
- package/dist/src/platforms/webex/token-extractor.js.map +1 -0
- package/dist/src/platforms/webex/types.d.ts +8 -1
- package/dist/src/platforms/webex/types.d.ts.map +1 -1
- package/dist/src/platforms/webex/types.js +4 -1
- package/dist/src/platforms/webex/types.js.map +1 -1
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.js +6 -2
- package/dist/src/platforms/whatsapp/client.js.map +1 -1
- package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
- package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
- package/dist/src/shared/utils/error-handler.d.ts +1 -1
- package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
- package/dist/src/shared/utils/error-handler.js +3 -2
- package/dist/src/shared/utils/error-handler.js.map +1 -1
- package/dist/src/shared/utils/stderr.d.ts +5 -0
- package/dist/src/shared/utils/stderr.d.ts.map +1 -0
- package/dist/src/shared/utils/stderr.js +18 -0
- package/dist/src/shared/utils/stderr.js.map +1 -0
- package/docs/content/docs/cli/channeltalk.mdx +7 -7
- package/docs/content/docs/cli/discord.mdx +3 -3
- package/docs/content/docs/cli/instagram.mdx +28 -6
- package/docs/content/docs/cli/slack.mdx +2 -2
- package/docs/content/docs/cli/teams.mdx +6 -4
- package/docs/content/docs/cli/webex.mdx +32 -11
- package/e2e/README.md +132 -8
- package/e2e/channeltalk.e2e.test.ts +2 -7
- package/e2e/channeltalkbot.e2e.test.ts +2 -6
- package/e2e/config.ts +172 -10
- package/e2e/helpers.ts +7 -0
- package/e2e/instagram.e2e.test.ts +97 -0
- package/e2e/kakaotalk.e2e.test.ts +74 -0
- package/e2e/line.e2e.test.ts +92 -0
- package/e2e/teams.e2e.test.ts +46 -1
- package/e2e/telegram.e2e.test.ts +84 -0
- package/e2e/webex.e2e.test.ts +190 -0
- package/e2e/whatsapp.e2e.test.ts +90 -0
- package/e2e/whatsappbot.e2e.test.ts +78 -0
- package/package.json +5 -3
- package/skills/agent-channeltalk/SKILL.md +9 -9
- package/skills/agent-channeltalk/references/authentication.md +21 -18
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +5 -5
- package/skills/agent-discord/references/authentication.md +8 -8
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +51 -9
- package/skills/agent-instagram/references/authentication.md +35 -3
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +5 -5
- package/skills/agent-slack/references/authentication.md +8 -8
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +6 -6
- package/skills/agent-teams/references/authentication.md +8 -8
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +35 -15
- package/skills/agent-webex/references/authentication.md +63 -9
- package/skills/agent-webex/references/common-patterns.md +6 -3
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
- package/src/platforms/channeltalk/commands/auth.ts +38 -32
- package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
- package/src/platforms/channeltalk/ensure-auth.ts +6 -6
- package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
- package/src/platforms/channeltalk/token-extractor.ts +344 -30
- package/src/platforms/discord/commands/auth.test.ts +3 -3
- package/src/platforms/discord/commands/auth.ts +58 -54
- package/src/platforms/discord/ensure-auth.test.ts +3 -3
- package/src/platforms/discord/ensure-auth.ts +3 -3
- package/src/platforms/discord/token-extractor.test.ts +199 -27
- package/src/platforms/discord/token-extractor.ts +190 -17
- package/src/platforms/instagram/client.ts +2 -2
- package/src/platforms/instagram/commands/auth.ts +133 -14
- package/src/platforms/instagram/ensure-auth.ts +63 -12
- package/src/platforms/instagram/index.ts +1 -0
- package/src/platforms/instagram/token-extractor.test.ts +424 -0
- package/src/platforms/instagram/token-extractor.ts +478 -0
- package/src/platforms/kakaotalk/client.ts +3 -1
- package/src/platforms/kakaotalk/commands/auth.ts +14 -13
- package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
- package/src/platforms/line/client.ts +39 -14
- package/src/platforms/line/commands/auth.ts +7 -6
- package/src/platforms/slack/cli.test.ts +6 -5
- package/src/platforms/slack/commands/auth.test.ts +11 -7
- package/src/platforms/slack/commands/auth.ts +11 -10
- package/src/platforms/slack/token-extractor.test.ts +98 -1
- package/src/platforms/slack/token-extractor.ts +338 -26
- package/src/platforms/teams/commands/auth.ts +9 -8
- package/src/platforms/teams/ensure-auth.ts +3 -1
- package/src/platforms/teams/token-extractor.test.ts +136 -17
- package/src/platforms/teams/token-extractor.ts +182 -31
- package/src/platforms/telegram/client.test.ts +134 -0
- package/src/platforms/telegram/client.ts +27 -6
- package/src/platforms/telegram/commands/auth.ts +6 -5
- package/src/platforms/webex/client.test.ts +314 -0
- package/src/platforms/webex/client.ts +231 -1
- package/src/platforms/webex/commands/auth.ts +71 -4
- package/src/platforms/webex/commands/member.test.ts +10 -1
- package/src/platforms/webex/commands/message.test.ts +9 -5
- package/src/platforms/webex/commands/snapshot.test.ts +13 -4
- package/src/platforms/webex/commands/space.test.ts +12 -2
- package/src/platforms/webex/credential-manager.ts +1 -1
- package/src/platforms/webex/encryption.ts +53 -0
- package/src/platforms/webex/ensure-auth.test.ts +4 -0
- package/src/platforms/webex/ensure-auth.ts +27 -4
- package/src/platforms/webex/index.ts +2 -0
- package/src/platforms/webex/token-extractor.test.ts +327 -0
- package/src/platforms/webex/token-extractor.ts +460 -0
- package/src/platforms/webex/types.ts +8 -2
- package/src/platforms/webex/typings/node-jose.d.ts +27 -0
- package/src/platforms/whatsapp/client.ts +11 -7
- package/src/shared/utils/derived-key-cache.ts +1 -1
- package/src/shared/utils/error-handler.ts +4 -2
- package/src/shared/utils/stderr.ts +22 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
|
|
2
2
|
import { WebexError } from './types'
|
|
3
3
|
import { WebexCredentialManager } from './credential-manager'
|
|
4
|
+
import { WebexEncryptionService } from './encryption'
|
|
4
5
|
|
|
5
6
|
const BASE_URL = 'https://webexapis.com/v1'
|
|
6
7
|
const MAX_RETRIES = 3
|
|
@@ -13,8 +14,11 @@ interface RateLimitBucket {
|
|
|
13
14
|
|
|
14
15
|
export class WebexClient {
|
|
15
16
|
private token: string | null = null
|
|
17
|
+
private deviceUrl: string | null = null
|
|
18
|
+
private tokenType: string | null = null
|
|
16
19
|
private buckets: Map<string, RateLimitBucket> = new Map()
|
|
17
20
|
private globalRateLimitUntil: number = 0
|
|
21
|
+
private encryption: WebexEncryptionService | null = null
|
|
18
22
|
|
|
19
23
|
async login(credentials?: { token: string }): Promise<this> {
|
|
20
24
|
if (credentials) {
|
|
@@ -36,7 +40,18 @@ export class WebexClient {
|
|
|
36
40
|
'no_credentials',
|
|
37
41
|
)
|
|
38
42
|
}
|
|
39
|
-
|
|
43
|
+
this.deviceUrl = config?.deviceUrl ?? null
|
|
44
|
+
this.tokenType = config?.tokenType ?? null
|
|
45
|
+
await this.login({ token })
|
|
46
|
+
|
|
47
|
+
if (this.tokenType === 'extracted' && config?.encryptionKeys) {
|
|
48
|
+
const keysMap = new Map(Object.entries(config.encryptionKeys))
|
|
49
|
+
if (keysMap.size > 0) {
|
|
50
|
+
this.encryption = new WebexEncryptionService(keysMap)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return this
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
private ensureAuth(): string {
|
|
@@ -175,22 +190,170 @@ export class WebexClient {
|
|
|
175
190
|
text: string,
|
|
176
191
|
options?: { markdown?: boolean },
|
|
177
192
|
): Promise<WebexMessage> {
|
|
193
|
+
if (this.useInternalAPI) {
|
|
194
|
+
return this.sendMessageInternal(roomId, text, options)
|
|
195
|
+
}
|
|
178
196
|
const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
|
|
179
197
|
return this.request<WebexMessage>('POST', '/messages', body)
|
|
180
198
|
}
|
|
181
199
|
|
|
200
|
+
private get useInternalAPI(): boolean {
|
|
201
|
+
return this.tokenType === 'extracted' && this.deviceUrl !== null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private get convBaseUrl(): string {
|
|
205
|
+
const match = this.deviceUrl?.match(/wdm(-[a-z0-9]+)\.wbx2\.com/)
|
|
206
|
+
return `https://conv${match?.[1] ?? ''}.wbx2.com/conversation/api/v1`
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private get internalHeaders(): Record<string, string> {
|
|
210
|
+
return {
|
|
211
|
+
Authorization: `Bearer ${this.ensureAuth()}`,
|
|
212
|
+
'Content-Type': 'application/json',
|
|
213
|
+
'cisco-device-url': this.deviceUrl!,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private decodeConvUuid(roomId: string): string {
|
|
218
|
+
return Buffer.from(roomId, 'base64').toString('utf8').split('/').pop() ?? roomId
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async internalRequest<T>(path: string, init?: RequestInit): Promise<T> {
|
|
222
|
+
const response = await fetch(`${this.convBaseUrl}${path}`, {
|
|
223
|
+
...init,
|
|
224
|
+
headers: { ...this.internalHeaders, ...(init?.headers as Record<string, string>) },
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
|
|
229
|
+
throw new WebexError(
|
|
230
|
+
errorBody?.message ?? `HTTP ${response.status}`,
|
|
231
|
+
`http_${response.status}`,
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (response.status === 204) return undefined as T
|
|
236
|
+
return response.json() as Promise<T>
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async activityToMessage(a: InternalActivity, roomId: string): Promise<WebexMessage> {
|
|
240
|
+
let text = a.object?.content ?? a.object?.displayName
|
|
241
|
+
|
|
242
|
+
if (this.encryption && text?.startsWith('eyJ')) {
|
|
243
|
+
const keyUrl = a.encryptionKeyUrl ?? a.object?.encryptionKeyUrl
|
|
244
|
+
if (keyUrl) {
|
|
245
|
+
const decrypted = await this.encryption.decryptText(keyUrl, text)
|
|
246
|
+
if (decrypted !== null) {
|
|
247
|
+
text = decrypted
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
id: a.id,
|
|
254
|
+
roomId,
|
|
255
|
+
roomType: 'group' as const,
|
|
256
|
+
text,
|
|
257
|
+
personId: a.actor?.entryUUID ?? a.actor?.id ?? '',
|
|
258
|
+
personEmail: a.actor?.emailAddress ?? '',
|
|
259
|
+
created: a.published,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private async buildEncryptedObject(
|
|
264
|
+
convUuid: string,
|
|
265
|
+
text: string,
|
|
266
|
+
options?: { markdown?: boolean },
|
|
267
|
+
): Promise<{ object: Record<string, string>; encryptionKeyUrl?: string }> {
|
|
268
|
+
const buildObject = (content: string): Record<string, string> =>
|
|
269
|
+
options?.markdown
|
|
270
|
+
? { objectType: 'comment', displayName: content, content, markdown: content }
|
|
271
|
+
: { objectType: 'comment', displayName: content, content }
|
|
272
|
+
|
|
273
|
+
if (this.encryption) {
|
|
274
|
+
const conv = await this.internalRequest<InternalConversation>(
|
|
275
|
+
`/conversations/${convUuid}?activitiesLimit=0&participantsLimit=0`,
|
|
276
|
+
)
|
|
277
|
+
const keyUri = conv.defaultActivityEncryptionKeyUrl
|
|
278
|
+
if (keyUri) {
|
|
279
|
+
const encrypted = await this.encryption.encryptText(keyUri, text)
|
|
280
|
+
if (encrypted) {
|
|
281
|
+
return { object: buildObject(encrypted), encryptionKeyUrl: keyUri }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { object: buildObject(text) }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private async sendMessageInternal(
|
|
290
|
+
roomId: string,
|
|
291
|
+
text: string,
|
|
292
|
+
options?: { markdown?: boolean },
|
|
293
|
+
): Promise<WebexMessage> {
|
|
294
|
+
const convUuid = this.decodeConvUuid(roomId)
|
|
295
|
+
const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, options)
|
|
296
|
+
|
|
297
|
+
const activity: Record<string, unknown> = {
|
|
298
|
+
verb: 'post',
|
|
299
|
+
object,
|
|
300
|
+
target: { id: convUuid, objectType: 'conversation' },
|
|
301
|
+
clientTempId: `tmp-${Date.now()}`,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (encryptionKeyUrl) {
|
|
305
|
+
activity['encryptionKeyUrl'] = encryptionKeyUrl
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const result = await this.internalRequest<InternalActivity>('/activities', {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
body: JSON.stringify(activity),
|
|
311
|
+
})
|
|
312
|
+
return this.activityToMessage(result, roomId)
|
|
313
|
+
}
|
|
314
|
+
|
|
182
315
|
async sendDirectMessage(
|
|
183
316
|
personEmail: string,
|
|
184
317
|
text: string,
|
|
185
318
|
options?: { markdown?: boolean },
|
|
186
319
|
): Promise<WebexMessage> {
|
|
320
|
+
if (this.useInternalAPI) {
|
|
321
|
+
const roomId = await this.findDirectRoomByEmail(personEmail)
|
|
322
|
+
if (!roomId) {
|
|
323
|
+
throw new WebexError(`No existing direct conversation with ${personEmail}`, 'not_found')
|
|
324
|
+
}
|
|
325
|
+
return this.sendMessageInternal(roomId, text, options)
|
|
326
|
+
}
|
|
187
327
|
const body = options?.markdown
|
|
188
328
|
? { toPersonEmail: personEmail, markdown: text }
|
|
189
329
|
: { toPersonEmail: personEmail, text }
|
|
190
330
|
return this.request<WebexMessage>('POST', '/messages', body)
|
|
191
331
|
}
|
|
192
332
|
|
|
333
|
+
private async findDirectRoomByEmail(email: string): Promise<string | null> {
|
|
334
|
+
const rooms = await this.request<{ items: WebexSpace[] }>('GET', `/rooms?type=direct&max=100`)
|
|
335
|
+
for (const room of rooms.items) {
|
|
336
|
+
const members = await this.request<{ items: WebexMembership[] }>(
|
|
337
|
+
'GET',
|
|
338
|
+
`/memberships?roomId=${room.id}&max=10`,
|
|
339
|
+
)
|
|
340
|
+
if (members.items.some((m) => m.personEmail === email)) {
|
|
341
|
+
return room.id
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
|
|
193
347
|
async listMessages(roomId: string, options?: { max?: number }): Promise<WebexMessage[]> {
|
|
348
|
+
if (this.useInternalAPI) {
|
|
349
|
+
const convUuid = this.decodeConvUuid(roomId)
|
|
350
|
+
const max = options?.max ?? 50
|
|
351
|
+
const conv = await this.internalRequest<InternalConversation>(
|
|
352
|
+
`/conversations/${convUuid}?activitiesLimit=${max}&participantsLimit=0`,
|
|
353
|
+
)
|
|
354
|
+
const activities = (conv.activities?.items ?? []).filter((a) => a.verb === 'post')
|
|
355
|
+
return Promise.all(activities.map((a) => this.activityToMessage(a, roomId)))
|
|
356
|
+
}
|
|
194
357
|
const params = new URLSearchParams()
|
|
195
358
|
params.set('roomId', roomId)
|
|
196
359
|
params.set('max', String(options?.max ?? 50))
|
|
@@ -199,10 +362,32 @@ export class WebexClient {
|
|
|
199
362
|
}
|
|
200
363
|
|
|
201
364
|
async getMessage(messageId: string): Promise<WebexMessage> {
|
|
365
|
+
if (this.useInternalAPI) {
|
|
366
|
+
const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
|
|
367
|
+
const convId = activity.target?.id ?? ''
|
|
368
|
+
const roomId = convId
|
|
369
|
+
? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64')
|
|
370
|
+
: ''
|
|
371
|
+
return this.activityToMessage(activity, roomId)
|
|
372
|
+
}
|
|
202
373
|
return this.request<WebexMessage>('GET', `/messages/${messageId}`)
|
|
203
374
|
}
|
|
204
375
|
|
|
205
376
|
async deleteMessage(messageId: string): Promise<void> {
|
|
377
|
+
if (this.useInternalAPI) {
|
|
378
|
+
const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
|
|
379
|
+
const convId = activity.target?.id
|
|
380
|
+
if (!convId) throw new WebexError('Cannot determine conversation for activity', 'internal_error')
|
|
381
|
+
await this.internalRequest<unknown>('/activities', {
|
|
382
|
+
method: 'POST',
|
|
383
|
+
body: JSON.stringify({
|
|
384
|
+
verb: 'delete',
|
|
385
|
+
object: { id: messageId, objectType: 'activity' },
|
|
386
|
+
target: { id: convId, objectType: 'conversation' },
|
|
387
|
+
}),
|
|
388
|
+
})
|
|
389
|
+
return
|
|
390
|
+
}
|
|
206
391
|
return this.request<void>('DELETE', `/messages/${messageId}`)
|
|
207
392
|
}
|
|
208
393
|
|
|
@@ -212,6 +397,28 @@ export class WebexClient {
|
|
|
212
397
|
text: string,
|
|
213
398
|
options?: { markdown?: boolean },
|
|
214
399
|
): Promise<WebexMessage> {
|
|
400
|
+
if (this.useInternalAPI) {
|
|
401
|
+
const convUuid = this.decodeConvUuid(roomId)
|
|
402
|
+
const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, options)
|
|
403
|
+
|
|
404
|
+
const activity: Record<string, unknown> = {
|
|
405
|
+
verb: 'post',
|
|
406
|
+
object,
|
|
407
|
+
target: { id: convUuid, objectType: 'conversation' },
|
|
408
|
+
parent: { id: messageId, type: 'edit' },
|
|
409
|
+
clientTempId: `tmp-${Date.now()}`,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (encryptionKeyUrl) {
|
|
413
|
+
activity['encryptionKeyUrl'] = encryptionKeyUrl
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const result = await this.internalRequest<InternalActivity>('/activities', {
|
|
417
|
+
method: 'POST',
|
|
418
|
+
body: JSON.stringify(activity),
|
|
419
|
+
})
|
|
420
|
+
return this.activityToMessage(result, roomId)
|
|
421
|
+
}
|
|
215
422
|
const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
|
|
216
423
|
return this.request<WebexMessage>('PUT', `/messages/${messageId}`, body)
|
|
217
424
|
}
|
|
@@ -245,3 +452,26 @@ export class WebexClient {
|
|
|
245
452
|
return data.items
|
|
246
453
|
}
|
|
247
454
|
}
|
|
455
|
+
|
|
456
|
+
interface InternalActivity {
|
|
457
|
+
id: string
|
|
458
|
+
verb: string
|
|
459
|
+
actor?: { displayName?: string; emailAddress?: string; entryUUID?: string; id?: string }
|
|
460
|
+
object?: {
|
|
461
|
+
content?: string
|
|
462
|
+
displayName?: string
|
|
463
|
+
objectType?: string
|
|
464
|
+
encryptionKeyUrl?: string
|
|
465
|
+
}
|
|
466
|
+
target?: { id: string; encryptionKeyUrl?: string }
|
|
467
|
+
published: string
|
|
468
|
+
encryptionKeyUrl?: string
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
interface InternalConversation {
|
|
472
|
+
id: string
|
|
473
|
+
activities?: { items: InternalActivity[] }
|
|
474
|
+
defaultActivityEncryptionKeyUrl?: string
|
|
475
|
+
kmsResourceObjectUrl?: string
|
|
476
|
+
encryptionKeyUrl?: string
|
|
477
|
+
}
|
|
@@ -2,10 +2,12 @@ import { Command } from 'commander'
|
|
|
2
2
|
|
|
3
3
|
import { handleError } from '@/shared/utils/error-handler'
|
|
4
4
|
import { formatOutput } from '@/shared/utils/output'
|
|
5
|
+
import { info, debug } from '@/shared/utils/stderr'
|
|
5
6
|
|
|
6
7
|
import { getWebexAppCredentials } from '../app-config'
|
|
7
8
|
import { WebexClient } from '../client'
|
|
8
9
|
import { WebexCredentialManager } from '../credential-manager'
|
|
10
|
+
import { WebexTokenExtractor } from '../token-extractor'
|
|
9
11
|
|
|
10
12
|
interface ResolvedCredentials {
|
|
11
13
|
clientId: string
|
|
@@ -68,11 +70,11 @@ export async function loginAction(options: { token?: string; clientId?: string;
|
|
|
68
70
|
|
|
69
71
|
const device = await credManager.requestDeviceCode(clientId)
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
info(`Open this URL and enter the code: ${device.verificationUri}`)
|
|
74
|
+
info(`Code: ${device.userCode}`)
|
|
75
|
+
info('')
|
|
74
76
|
await openBrowser(device.verificationUriComplete)
|
|
75
|
-
|
|
77
|
+
info('Waiting for authorization...')
|
|
76
78
|
|
|
77
79
|
const config = await credManager.pollDeviceToken(
|
|
78
80
|
device.deviceCode,
|
|
@@ -135,6 +137,64 @@ export async function statusAction(options: { pretty?: boolean }): Promise<void>
|
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
export async function extractAction(options: { pretty?: boolean; debug?: boolean }): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
const extractor = new WebexTokenExtractor(
|
|
143
|
+
undefined,
|
|
144
|
+
options.debug ? (msg) => debug(`[debug] ${msg}`) : undefined,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (options.debug) {
|
|
148
|
+
debug('[debug] Searching browser profiles for Webex tokens...')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const extracted = await extractor.extract()
|
|
152
|
+
|
|
153
|
+
if (!extracted) {
|
|
154
|
+
console.log(
|
|
155
|
+
formatOutput(
|
|
156
|
+
{
|
|
157
|
+
error: 'No Webex token found in any browser. Make sure you are logged in to web.webex.com in Chrome, Edge, Arc, or Brave.',
|
|
158
|
+
hint: 'Run "auth login" for OAuth Device Grant flow, or --debug for more info.',
|
|
159
|
+
},
|
|
160
|
+
options.pretty,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
process.exit(1)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const client = await new WebexClient().login({ token: extracted.accessToken })
|
|
168
|
+
const person = await client.testAuth()
|
|
169
|
+
|
|
170
|
+
const credManager = new WebexCredentialManager()
|
|
171
|
+
await credManager.saveConfig({
|
|
172
|
+
accessToken: extracted.accessToken,
|
|
173
|
+
refreshToken: extracted.refreshToken ?? '',
|
|
174
|
+
expiresAt: extracted.expiresAt ?? 0,
|
|
175
|
+
tokenType: 'extracted',
|
|
176
|
+
deviceUrl: extracted.deviceUrl,
|
|
177
|
+
userId: extracted.userId,
|
|
178
|
+
encryptionKeys: extracted.encryptionKeys
|
|
179
|
+
? Object.fromEntries(extracted.encryptionKeys)
|
|
180
|
+
: undefined,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
console.log(
|
|
184
|
+
formatOutput(
|
|
185
|
+
{
|
|
186
|
+
user: { id: person.id, displayName: person.displayName, emails: person.emails },
|
|
187
|
+
authenticated: true,
|
|
188
|
+
tokenType: 'extracted',
|
|
189
|
+
},
|
|
190
|
+
options.pretty,
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
handleError(error as Error)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
138
198
|
export async function logoutAction(options: { pretty?: boolean }): Promise<void> {
|
|
139
199
|
try {
|
|
140
200
|
const credManager = new WebexCredentialManager()
|
|
@@ -166,6 +226,13 @@ export const authCommand = new Command('auth')
|
|
|
166
226
|
.option('--pretty', 'Pretty print JSON output')
|
|
167
227
|
.action(loginAction),
|
|
168
228
|
)
|
|
229
|
+
.addCommand(
|
|
230
|
+
new Command('extract')
|
|
231
|
+
.description('Extract Webex token from browser (Chrome, Edge, Arc, Brave)')
|
|
232
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
233
|
+
.option('--debug', 'Show debug output')
|
|
234
|
+
.action(extractAction),
|
|
235
|
+
)
|
|
169
236
|
.addCommand(
|
|
170
237
|
new Command('status')
|
|
171
238
|
.description('Show authentication status')
|
|
@@ -8,6 +8,8 @@ describe('member commands', () => {
|
|
|
8
8
|
let consoleSpy: ReturnType<typeof spyOn>
|
|
9
9
|
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
10
10
|
let processExitSpy: ReturnType<typeof spyOn>
|
|
11
|
+
let stderrOutput: string
|
|
12
|
+
let origStderrWrite: typeof process.stderr.write
|
|
11
13
|
const mockMembers = [
|
|
12
14
|
{
|
|
13
15
|
id: 'mem-1',
|
|
@@ -35,11 +37,18 @@ describe('member commands', () => {
|
|
|
35
37
|
processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
|
|
36
38
|
throw new Error(`process.exit(${_code})`)
|
|
37
39
|
})
|
|
40
|
+
stderrOutput = ''
|
|
41
|
+
origStderrWrite = process.stderr.write
|
|
42
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
43
|
+
stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
|
|
44
|
+
return true
|
|
45
|
+
}) as typeof process.stderr.write
|
|
38
46
|
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient() as any)
|
|
39
47
|
spyOn(WebexClient.prototype, 'listMemberships').mockResolvedValue(mockMembers)
|
|
40
48
|
})
|
|
41
49
|
|
|
42
50
|
afterEach(() => {
|
|
51
|
+
process.stderr.write = origStderrWrite
|
|
43
52
|
mock.restore()
|
|
44
53
|
})
|
|
45
54
|
|
|
@@ -90,7 +99,7 @@ describe('member commands', () => {
|
|
|
90
99
|
|
|
91
100
|
await expect(listAction('room-1', {})).rejects.toThrow('process.exit(1)')
|
|
92
101
|
|
|
93
|
-
expect(
|
|
102
|
+
expect(stderrOutput).toContain('No Webex credentials found')
|
|
94
103
|
})
|
|
95
104
|
|
|
96
105
|
test('listAction handles API error', async () => {
|
|
@@ -97,8 +97,13 @@ test('send: with --markdown passes markdown option', async () => {
|
|
|
97
97
|
|
|
98
98
|
test('send: not authenticated shows error', async () => {
|
|
99
99
|
clientLoginSpy.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
|
|
101
|
+
let stderrOutput = ''
|
|
102
|
+
const origWrite = process.stderr.write
|
|
103
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
104
|
+
stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
|
|
105
|
+
return true
|
|
106
|
+
}) as typeof process.stderr.write
|
|
102
107
|
|
|
103
108
|
const originalExit = process.exit
|
|
104
109
|
process.exit = mock((_code?: number) => {
|
|
@@ -110,11 +115,10 @@ test('send: not authenticated shows error', async () => {
|
|
|
110
115
|
} catch {
|
|
111
116
|
} finally {
|
|
112
117
|
process.exit = originalExit
|
|
118
|
+
process.stderr.write = origWrite
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
expect(
|
|
116
|
-
const output = errorSpy.mock.calls[0][0]
|
|
117
|
-
expect(output).toContain('No Webex credentials found')
|
|
121
|
+
expect(stderrOutput).toContain('No Webex credentials found')
|
|
118
122
|
})
|
|
119
123
|
|
|
120
124
|
test('dm: calls sendDirectMessage with email and text', async () => {
|
|
@@ -6,6 +6,8 @@ import { snapshotAction } from './snapshot'
|
|
|
6
6
|
describe('snapshot command', () => {
|
|
7
7
|
let consoleSpy: ReturnType<typeof spyOn>
|
|
8
8
|
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
9
|
+
let stderrOutput: string
|
|
10
|
+
let origStderrWrite: typeof process.stderr.write
|
|
9
11
|
|
|
10
12
|
const mockSpaces = [
|
|
11
13
|
{ id: 'space-1', title: 'General', type: 'group', isLocked: false, lastActivity: '2024-01-15T00:00:00.000Z', created: '2024-01-01T00:00:00.000Z', creatorId: 'person-1' },
|
|
@@ -22,13 +24,22 @@ describe('snapshot command', () => {
|
|
|
22
24
|
beforeEach(() => {
|
|
23
25
|
consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
24
26
|
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
27
|
+
stderrOutput = ''
|
|
28
|
+
origStderrWrite = process.stderr.write
|
|
29
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
30
|
+
stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
|
|
31
|
+
return true
|
|
32
|
+
}) as typeof process.stderr.write
|
|
25
33
|
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient() as any)
|
|
26
34
|
spyOn(WebexClient.prototype, 'listSpaces').mockResolvedValue(mockSpaces as any)
|
|
27
35
|
spyOn(WebexClient.prototype, 'listMessages').mockResolvedValue(mockMessages as any)
|
|
28
36
|
spyOn(WebexClient.prototype, 'listMemberships').mockResolvedValue(mockMembers as any)
|
|
29
37
|
})
|
|
30
38
|
|
|
31
|
-
afterEach(() => {
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
process.stderr.write = origStderrWrite
|
|
41
|
+
mock.restore()
|
|
42
|
+
})
|
|
32
43
|
|
|
33
44
|
test('full snapshot includes spaces, recent_messages, members', async () => {
|
|
34
45
|
await snapshotAction({})
|
|
@@ -81,9 +92,7 @@ describe('snapshot command', () => {
|
|
|
81
92
|
process.exit = originalExit
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
expect(
|
|
85
|
-
const output = consoleErrorSpy.mock.calls[0][0]
|
|
86
|
-
expect(output).toContain('No Webex credentials found')
|
|
95
|
+
expect(stderrOutput).toContain('No Webex credentials found')
|
|
87
96
|
})
|
|
88
97
|
|
|
89
98
|
test('passes limit option to listMessages', async () => {
|
|
@@ -42,6 +42,8 @@ let clientGetSpaceSpy: ReturnType<typeof spyOn>
|
|
|
42
42
|
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
43
43
|
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
44
44
|
let processExitSpy: ReturnType<typeof spyOn>
|
|
45
|
+
let stderrOutput: string
|
|
46
|
+
let origStderrWrite: typeof process.stderr.write
|
|
45
47
|
|
|
46
48
|
beforeEach(() => {
|
|
47
49
|
clientLoginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue(
|
|
@@ -58,9 +60,17 @@ beforeEach(() => {
|
|
|
58
60
|
processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
|
|
59
61
|
throw new Error(`process.exit(${_code})`)
|
|
60
62
|
})
|
|
63
|
+
|
|
64
|
+
stderrOutput = ''
|
|
65
|
+
origStderrWrite = process.stderr.write
|
|
66
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
67
|
+
stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
|
|
68
|
+
return true
|
|
69
|
+
}) as typeof process.stderr.write
|
|
61
70
|
})
|
|
62
71
|
|
|
63
72
|
afterEach(() => {
|
|
73
|
+
process.stderr.write = origStderrWrite
|
|
64
74
|
clientLoginSpy?.mockRestore()
|
|
65
75
|
clientListSpacesSpy?.mockRestore()
|
|
66
76
|
clientGetSpaceSpy?.mockRestore()
|
|
@@ -139,7 +149,7 @@ describe('listAction', () => {
|
|
|
139
149
|
await expect(listAction({})).rejects.toThrow('process.exit(1)')
|
|
140
150
|
|
|
141
151
|
expect(clientListSpacesSpy).not.toHaveBeenCalled()
|
|
142
|
-
expect(
|
|
152
|
+
expect(stderrOutput).toContain('No Webex credentials found')
|
|
143
153
|
})
|
|
144
154
|
})
|
|
145
155
|
|
|
@@ -201,6 +211,6 @@ describe('infoAction', () => {
|
|
|
201
211
|
await expect(infoAction('space-1', {})).rejects.toThrow('process.exit(1)')
|
|
202
212
|
|
|
203
213
|
expect(clientGetSpaceSpy).not.toHaveBeenCalled()
|
|
204
|
-
expect(
|
|
214
|
+
expect(stderrOutput).toContain('No Webex credentials found')
|
|
205
215
|
})
|
|
206
216
|
})
|
|
@@ -45,7 +45,7 @@ export class WebexCredentialManager {
|
|
|
45
45
|
const config = await this.loadConfig()
|
|
46
46
|
if (!config) return null
|
|
47
47
|
|
|
48
|
-
if (config.tokenType === 'manual') {
|
|
48
|
+
if (config.tokenType === 'manual' || config.tokenType === 'extracted') {
|
|
49
49
|
return config.accessToken
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as jose from 'node-jose'
|
|
2
|
+
|
|
3
|
+
export class WebexEncryptionService {
|
|
4
|
+
private rawKeys: Map<string, string>
|
|
5
|
+
private keyCache: Map<string, jose.JWK.Key> = new Map()
|
|
6
|
+
|
|
7
|
+
constructor(serializedKeys: Map<string, string>) {
|
|
8
|
+
this.rawKeys = serializedKeys
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async getKey(keyUri: string): Promise<jose.JWK.Key | null> {
|
|
12
|
+
const cached = this.keyCache.get(keyUri)
|
|
13
|
+
if (cached) return cached
|
|
14
|
+
|
|
15
|
+
const raw = this.rawKeys.get(keyUri)
|
|
16
|
+
if (!raw) return null
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(raw) as { jwk: object }
|
|
20
|
+
const joseKey = await jose.JWK.asKey(parsed.jwk)
|
|
21
|
+
this.keyCache.set(keyUri, joseKey)
|
|
22
|
+
return joseKey
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async encryptText(keyUri: string, plaintext: string): Promise<string | null> {
|
|
29
|
+
const key = await this.getKey(keyUri)
|
|
30
|
+
if (!key) return null
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return await jose.JWE.createEncrypt(
|
|
34
|
+
{ format: 'compact', contentAlg: 'A256GCM' },
|
|
35
|
+
{ key, header: { alg: 'dir' }, reference: null },
|
|
36
|
+
).final(plaintext, 'utf8')
|
|
37
|
+
} catch {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async decryptText(keyUri: string, ciphertext: string): Promise<string | null> {
|
|
43
|
+
const key = await this.getKey(keyUri)
|
|
44
|
+
if (!key) return null
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const result = await jose.JWE.createDecrypt(key).decrypt(ciphertext)
|
|
48
|
+
return result.plaintext.toString('utf8')
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -3,11 +3,13 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
|
|
|
3
3
|
import { WebexClient } from './client'
|
|
4
4
|
import { WebexCredentialManager } from './credential-manager'
|
|
5
5
|
import { ensureWebexAuth } from './ensure-auth'
|
|
6
|
+
import { WebexTokenExtractor } from './token-extractor'
|
|
6
7
|
|
|
7
8
|
let loadConfigSpy: ReturnType<typeof spyOn>
|
|
8
9
|
let getTokenSpy: ReturnType<typeof spyOn>
|
|
9
10
|
let loginSpy: ReturnType<typeof spyOn>
|
|
10
11
|
let testAuthSpy: ReturnType<typeof spyOn>
|
|
12
|
+
let extractSpy: ReturnType<typeof spyOn>
|
|
11
13
|
|
|
12
14
|
beforeEach(() => {
|
|
13
15
|
loadConfigSpy = spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
@@ -19,6 +21,7 @@ beforeEach(() => {
|
|
|
19
21
|
emails: ['test@example.com'],
|
|
20
22
|
type: 'person',
|
|
21
23
|
})
|
|
24
|
+
extractSpy = spyOn(WebexTokenExtractor.prototype, 'extract').mockResolvedValue(null)
|
|
22
25
|
})
|
|
23
26
|
|
|
24
27
|
afterEach(() => {
|
|
@@ -26,6 +29,7 @@ afterEach(() => {
|
|
|
26
29
|
getTokenSpy?.mockRestore()
|
|
27
30
|
loginSpy?.mockRestore()
|
|
28
31
|
testAuthSpy?.mockRestore()
|
|
32
|
+
extractSpy?.mockRestore()
|
|
29
33
|
})
|
|
30
34
|
|
|
31
35
|
describe('ensureWebexAuth', () => {
|