agent-messenger 2.0.0 → 2.2.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/marketplace.json +14 -1
- package/.claude-plugin/plugin.json +4 -2
- package/.env.template +35 -17
- package/README.md +37 -33
- package/bun.lock +6 -6
- package/dist/package.json +11 -3
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- 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/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/app-config.d.ts +7 -0
- package/dist/src/platforms/webex/app-config.d.ts.map +1 -0
- package/dist/src/platforms/webex/app-config.js +20 -0
- package/dist/src/platforms/webex/app-config.js.map +1 -0
- package/dist/src/platforms/webex/cli.d.ts +5 -0
- package/dist/src/platforms/webex/cli.d.ts.map +1 -0
- package/dist/src/platforms/webex/cli.js +32 -0
- package/dist/src/platforms/webex/cli.js.map +1 -0
- package/dist/src/platforms/webex/client.d.ts +55 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -0
- package/dist/src/platforms/webex/client.js +299 -0
- package/dist/src/platforms/webex/client.js.map +1 -0
- package/dist/src/platforms/webex/commands/auth.d.ts +19 -0
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/auth.js +166 -0
- package/dist/src/platforms/webex/commands/auth.js.map +1 -0
- package/dist/src/platforms/webex/commands/index.d.ts +6 -0
- package/dist/src/platforms/webex/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/index.js +6 -0
- package/dist/src/platforms/webex/commands/index.js.map +1 -0
- package/dist/src/platforms/webex/commands/member.d.ts +7 -0
- package/dist/src/platforms/webex/commands/member.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/member.js +34 -0
- package/dist/src/platforms/webex/commands/member.js.map +1 -0
- package/dist/src/platforms/webex/commands/message.d.ts +26 -0
- package/dist/src/platforms/webex/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/message.js +153 -0
- package/dist/src/platforms/webex/commands/message.js.map +1 -0
- package/dist/src/platforms/webex/commands/snapshot.d.ts +9 -0
- package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/snapshot.js +72 -0
- package/dist/src/platforms/webex/commands/snapshot.js.map +1 -0
- package/dist/src/platforms/webex/commands/space.d.ts +11 -0
- package/dist/src/platforms/webex/commands/space.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/space.js +59 -0
- package/dist/src/platforms/webex/commands/space.js.map +1 -0
- package/dist/src/platforms/webex/credential-manager.d.ts +23 -0
- package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/webex/credential-manager.js +148 -0
- package/dist/src/platforms/webex/credential-manager.js.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts +2 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.js +36 -0
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -0
- package/dist/src/platforms/webex/index.d.ts +8 -0
- package/dist/src/platforms/webex/index.d.ts.map +1 -0
- package/dist/src/platforms/webex/index.js +6 -0
- package/dist/src/platforms/webex/index.js.map +1 -0
- package/dist/src/platforms/webex/token-extractor.d.ts +28 -0
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
- package/dist/src/platforms/webex/token-extractor.js +344 -0
- package/dist/src/platforms/webex/token-extractor.js.map +1 -0
- package/dist/src/platforms/webex/types.d.ts +127 -0
- package/dist/src/platforms/webex/types.d.ts.map +1 -0
- package/dist/src/platforms/webex/types.js +64 -0
- package/dist/src/platforms/webex/types.js.map +1 -0
- 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/dist/src/tui/adapters/webex-adapter.d.ts +14 -0
- package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -0
- package/dist/src/tui/adapters/webex-adapter.js +79 -0
- package/dist/src/tui/adapters/webex-adapter.js.map +1 -0
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +2 -0
- package/dist/src/tui/app.js.map +1 -1
- 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/meta.json +1 -0
- 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 +310 -0
- package/docs/content/docs/sdk/meta.json +1 -1
- package/docs/content/docs/sdk/webex.mdx +260 -0
- package/docs/content/docs/tui.mdx +4 -3
- package/docs/src/app/page.tsx +2 -2
- 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 +11 -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 +406 -0
- package/skills/agent-webex/references/authentication.md +371 -0
- package/skills/agent-webex/references/common-patterns.md +726 -0
- package/skills/agent-webex/templates/monitor-space.sh +165 -0
- package/skills/agent-webex/templates/post-message.sh +170 -0
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/cli.ts +4 -0
- 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/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/app-config.test.ts +98 -0
- package/src/platforms/webex/app-config.ts +31 -0
- package/src/platforms/webex/cli.test.ts +58 -0
- package/src/platforms/webex/cli.ts +39 -0
- package/src/platforms/webex/client.test.ts +743 -0
- package/src/platforms/webex/client.ts +405 -0
- package/src/platforms/webex/commands/auth.test.ts +222 -0
- package/src/platforms/webex/commands/auth.ts +243 -0
- package/src/platforms/webex/commands/index.ts +5 -0
- package/src/platforms/webex/commands/member.test.ts +112 -0
- package/src/platforms/webex/commands/member.ts +45 -0
- package/src/platforms/webex/commands/message.test.ts +235 -0
- package/src/platforms/webex/commands/message.ts +204 -0
- package/src/platforms/webex/commands/snapshot.test.ts +105 -0
- package/src/platforms/webex/commands/snapshot.ts +91 -0
- package/src/platforms/webex/commands/space.test.ts +216 -0
- package/src/platforms/webex/commands/space.ts +74 -0
- package/src/platforms/webex/credential-manager.test.ts +314 -0
- package/src/platforms/webex/credential-manager.ts +197 -0
- package/src/platforms/webex/ensure-auth.test.ts +89 -0
- package/src/platforms/webex/ensure-auth.ts +38 -0
- package/src/platforms/webex/index.test.ts +25 -0
- package/src/platforms/webex/index.ts +19 -0
- package/src/platforms/webex/token-extractor.test.ts +327 -0
- package/src/platforms/webex/token-extractor.ts +393 -0
- package/src/platforms/webex/types.test.ts +307 -0
- package/src/platforms/webex/types.ts +129 -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
- package/src/tui/adapters/webex-adapter.ts +103 -0
- package/src/tui/app.ts +2 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
|
|
2
|
+
import { WebexError } from './types'
|
|
3
|
+
import { WebexCredentialManager } from './credential-manager'
|
|
4
|
+
|
|
5
|
+
const BASE_URL = 'https://webexapis.com/v1'
|
|
6
|
+
const MAX_RETRIES = 3
|
|
7
|
+
const BASE_BACKOFF_MS = 100
|
|
8
|
+
|
|
9
|
+
interface RateLimitBucket {
|
|
10
|
+
remaining: number
|
|
11
|
+
resetAt: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class WebexClient {
|
|
15
|
+
private token: string | null = null
|
|
16
|
+
private deviceUrl: string | null = null
|
|
17
|
+
private tokenType: string | null = null
|
|
18
|
+
private buckets: Map<string, RateLimitBucket> = new Map()
|
|
19
|
+
private globalRateLimitUntil: number = 0
|
|
20
|
+
|
|
21
|
+
async login(credentials?: { token: string }): Promise<this> {
|
|
22
|
+
if (credentials) {
|
|
23
|
+
if (!credentials.token) {
|
|
24
|
+
throw new WebexError('Token is required', 'missing_token')
|
|
25
|
+
}
|
|
26
|
+
this.token = credentials.token
|
|
27
|
+
return this
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { ensureWebexAuth } = await import('./ensure-auth')
|
|
31
|
+
await ensureWebexAuth()
|
|
32
|
+
const credManager = new WebexCredentialManager()
|
|
33
|
+
const config = await credManager.loadConfig()
|
|
34
|
+
const token = await credManager.getToken(config?.clientId, config?.clientSecret)
|
|
35
|
+
if (!token) {
|
|
36
|
+
throw new WebexError(
|
|
37
|
+
'No Webex credentials found. Run "auth login" to authenticate.',
|
|
38
|
+
'no_credentials',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
this.deviceUrl = config?.deviceUrl ?? null
|
|
42
|
+
this.tokenType = config?.tokenType ?? null
|
|
43
|
+
return this.login({ token })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private ensureAuth(): string {
|
|
47
|
+
if (this.token === null) {
|
|
48
|
+
throw new WebexError('Not authenticated. Call .login() first.', 'not_authenticated')
|
|
49
|
+
}
|
|
50
|
+
return this.token
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private getBucketKey(method: string, path: string): string {
|
|
54
|
+
const normalized = path.replace(
|
|
55
|
+
/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?=\/|$)/gi,
|
|
56
|
+
'/{id}',
|
|
57
|
+
)
|
|
58
|
+
return `${method}:${normalized}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async waitForRateLimit(bucketKey: string): Promise<void> {
|
|
62
|
+
const now = Date.now()
|
|
63
|
+
|
|
64
|
+
if (this.globalRateLimitUntil > now) {
|
|
65
|
+
await this.sleep(this.globalRateLimitUntil - now)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const bucket = this.buckets.get(bucketKey)
|
|
69
|
+
if (bucket && bucket.remaining === 0 && bucket.resetAt * 1000 > now) {
|
|
70
|
+
await this.sleep(bucket.resetAt * 1000 - now)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private updateBucket(bucketKey: string, response: Response): void {
|
|
75
|
+
const remaining = response.headers.get('X-RateLimit-Remaining')
|
|
76
|
+
const reset = response.headers.get('X-RateLimit-Reset')
|
|
77
|
+
|
|
78
|
+
if (remaining !== null && reset !== null) {
|
|
79
|
+
this.buckets.set(bucketKey, {
|
|
80
|
+
remaining: parseInt(remaining, 10),
|
|
81
|
+
resetAt: parseFloat(reset),
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async handleRateLimitResponse(response: Response): Promise<number> {
|
|
87
|
+
const retryAfter = response.headers.get('Retry-After')
|
|
88
|
+
const waitMs = parseFloat(retryAfter || '1') * 1000
|
|
89
|
+
|
|
90
|
+
this.globalRateLimitUntil = Date.now() + waitMs
|
|
91
|
+
await this.sleep(waitMs)
|
|
92
|
+
return waitMs
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private sleep(ms: number): Promise<void> {
|
|
96
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
100
|
+
const url = `${BASE_URL}${path}`
|
|
101
|
+
const bucketKey = this.getBucketKey(method, path)
|
|
102
|
+
|
|
103
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
104
|
+
await this.waitForRateLimit(bucketKey)
|
|
105
|
+
|
|
106
|
+
const options: RequestInit = {
|
|
107
|
+
method,
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: `Bearer ${this.ensureAuth()}`,
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (body !== undefined) {
|
|
115
|
+
options.body = JSON.stringify(body)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const response = await fetch(url, options)
|
|
119
|
+
this.updateBucket(bucketKey, response)
|
|
120
|
+
|
|
121
|
+
if (response.status === 429) {
|
|
122
|
+
if (attempt < MAX_RETRIES) {
|
|
123
|
+
await this.handleRateLimitResponse(response)
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
const errorBody = (await response.json().catch(() => null)) as {
|
|
127
|
+
message?: string
|
|
128
|
+
} | null
|
|
129
|
+
throw new WebexError(errorBody?.message ?? 'Rate limited', 'rate_limited')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (response.status >= 500 && attempt < MAX_RETRIES) {
|
|
133
|
+
await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const errorBody = (await response.json().catch(() => null)) as {
|
|
139
|
+
message?: string
|
|
140
|
+
errors?: Array<{ description: string }>
|
|
141
|
+
trackingId?: string
|
|
142
|
+
} | null
|
|
143
|
+
const message =
|
|
144
|
+
errorBody?.message ??
|
|
145
|
+
errorBody?.errors?.[0]?.description ??
|
|
146
|
+
`HTTP ${response.status}`
|
|
147
|
+
throw new WebexError(message, `http_${response.status}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (response.status === 204) {
|
|
151
|
+
return undefined as T
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return response.json() as Promise<T>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new WebexError('Request failed after retries', 'max_retries')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async testAuth(): Promise<WebexPerson> {
|
|
161
|
+
return this.request<WebexPerson>('GET', '/people/me')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async listSpaces(options?: { type?: string; max?: number }): Promise<WebexSpace[]> {
|
|
165
|
+
const params = new URLSearchParams()
|
|
166
|
+
if (options?.type) params.set('type', options.type)
|
|
167
|
+
params.set('max', String(options?.max ?? 50))
|
|
168
|
+
const query = params.toString()
|
|
169
|
+
const data = await this.request<{ items: WebexSpace[] }>('GET', `/rooms?${query}`)
|
|
170
|
+
return data.items
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getSpace(spaceId: string): Promise<WebexSpace> {
|
|
174
|
+
return this.request<WebexSpace>('GET', `/rooms/${spaceId}`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async sendMessage(
|
|
178
|
+
roomId: string,
|
|
179
|
+
text: string,
|
|
180
|
+
options?: { markdown?: boolean },
|
|
181
|
+
): Promise<WebexMessage> {
|
|
182
|
+
if (this.useInternalAPI) {
|
|
183
|
+
return this.sendMessageInternal(roomId, text, options)
|
|
184
|
+
}
|
|
185
|
+
const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
|
|
186
|
+
return this.request<WebexMessage>('POST', '/messages', body)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private get useInternalAPI(): boolean {
|
|
190
|
+
return this.tokenType === 'extracted' && this.deviceUrl !== null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private get convBaseUrl(): string {
|
|
194
|
+
const match = this.deviceUrl?.match(/wdm(-[a-z0-9]+)\.wbx2\.com/)
|
|
195
|
+
return `https://conv${match?.[1] ?? ''}.wbx2.com/conversation/api/v1`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private get internalHeaders(): Record<string, string> {
|
|
199
|
+
return {
|
|
200
|
+
Authorization: `Bearer ${this.ensureAuth()}`,
|
|
201
|
+
'Content-Type': 'application/json',
|
|
202
|
+
'cisco-device-url': this.deviceUrl!,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private decodeConvUuid(roomId: string): string {
|
|
207
|
+
return Buffer.from(roomId, 'base64').toString('utf8').split('/').pop() ?? roomId
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async internalRequest<T>(path: string, init?: RequestInit): Promise<T> {
|
|
211
|
+
const response = await fetch(`${this.convBaseUrl}${path}`, {
|
|
212
|
+
...init,
|
|
213
|
+
headers: { ...this.internalHeaders, ...init?.headers as Record<string, string> },
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
|
|
218
|
+
throw new WebexError(
|
|
219
|
+
errorBody?.message ?? `HTTP ${response.status}`,
|
|
220
|
+
`http_${response.status}`,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (response.status === 204) return undefined as T
|
|
225
|
+
return response.json() as Promise<T>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private activityToMessage(a: InternalActivity, roomId: string): WebexMessage {
|
|
229
|
+
return {
|
|
230
|
+
id: a.id,
|
|
231
|
+
roomId,
|
|
232
|
+
roomType: 'group' as const,
|
|
233
|
+
text: a.object?.content ?? a.object?.displayName,
|
|
234
|
+
personId: a.actor?.entryUUID ?? a.actor?.id ?? '',
|
|
235
|
+
personEmail: a.actor?.emailAddress ?? '',
|
|
236
|
+
created: a.published,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private async sendMessageInternal(
|
|
241
|
+
roomId: string,
|
|
242
|
+
text: string,
|
|
243
|
+
options?: { markdown?: boolean },
|
|
244
|
+
): Promise<WebexMessage> {
|
|
245
|
+
const convUuid = this.decodeConvUuid(roomId)
|
|
246
|
+
const object = options?.markdown
|
|
247
|
+
? { objectType: 'comment', displayName: text, content: text, markdown: text }
|
|
248
|
+
: { objectType: 'comment', displayName: text, content: text }
|
|
249
|
+
const result = await this.internalRequest<InternalActivity>('/activities', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
verb: 'post',
|
|
253
|
+
object,
|
|
254
|
+
target: { id: convUuid, objectType: 'conversation' },
|
|
255
|
+
clientTempId: `tmp-${Date.now()}`,
|
|
256
|
+
}),
|
|
257
|
+
})
|
|
258
|
+
return this.activityToMessage(result, roomId)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async sendDirectMessage(
|
|
262
|
+
personEmail: string,
|
|
263
|
+
text: string,
|
|
264
|
+
options?: { markdown?: boolean },
|
|
265
|
+
): Promise<WebexMessage> {
|
|
266
|
+
if (this.useInternalAPI) {
|
|
267
|
+
const roomId = await this.findDirectRoomByEmail(personEmail)
|
|
268
|
+
if (!roomId) {
|
|
269
|
+
throw new WebexError(`No existing direct conversation with ${personEmail}`, 'not_found')
|
|
270
|
+
}
|
|
271
|
+
return this.sendMessageInternal(roomId, text, options)
|
|
272
|
+
}
|
|
273
|
+
const body = options?.markdown
|
|
274
|
+
? { toPersonEmail: personEmail, markdown: text }
|
|
275
|
+
: { toPersonEmail: personEmail, text }
|
|
276
|
+
return this.request<WebexMessage>('POST', '/messages', body)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async findDirectRoomByEmail(email: string): Promise<string | null> {
|
|
280
|
+
const rooms = await this.request<{ items: WebexSpace[] }>('GET', `/rooms?type=direct&max=100`)
|
|
281
|
+
for (const room of rooms.items) {
|
|
282
|
+
const members = await this.request<{ items: WebexMembership[] }>(
|
|
283
|
+
'GET',
|
|
284
|
+
`/memberships?roomId=${room.id}&max=10`,
|
|
285
|
+
)
|
|
286
|
+
if (members.items.some((m) => m.personEmail === email)) {
|
|
287
|
+
return room.id
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async listMessages(roomId: string, options?: { max?: number }): Promise<WebexMessage[]> {
|
|
294
|
+
if (this.useInternalAPI) {
|
|
295
|
+
const convUuid = this.decodeConvUuid(roomId)
|
|
296
|
+
const max = options?.max ?? 50
|
|
297
|
+
const conv = await this.internalRequest<InternalConversation>(
|
|
298
|
+
`/conversations/${convUuid}?activitiesLimit=${max}&participantsLimit=0`,
|
|
299
|
+
)
|
|
300
|
+
return (conv.activities?.items ?? [])
|
|
301
|
+
.filter((a) => a.verb === 'post')
|
|
302
|
+
.map((a) => this.activityToMessage(a, roomId))
|
|
303
|
+
}
|
|
304
|
+
const params = new URLSearchParams()
|
|
305
|
+
params.set('roomId', roomId)
|
|
306
|
+
params.set('max', String(options?.max ?? 50))
|
|
307
|
+
const data = await this.request<{ items: WebexMessage[] }>('GET', `/messages?${params}`)
|
|
308
|
+
return data.items
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async getMessage(messageId: string): Promise<WebexMessage> {
|
|
312
|
+
if (this.useInternalAPI) {
|
|
313
|
+
const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
|
|
314
|
+
const convId = activity.target?.id ?? ''
|
|
315
|
+
const roomId = convId ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64') : ''
|
|
316
|
+
return this.activityToMessage(activity, roomId)
|
|
317
|
+
}
|
|
318
|
+
return this.request<WebexMessage>('GET', `/messages/${messageId}`)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async deleteMessage(messageId: string): Promise<void> {
|
|
322
|
+
if (this.useInternalAPI) {
|
|
323
|
+
const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
|
|
324
|
+
const convId = activity.target?.id
|
|
325
|
+
if (!convId) throw new WebexError('Cannot determine conversation for activity', 'internal_error')
|
|
326
|
+
await this.internalRequest<unknown>('/activities', {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
body: JSON.stringify({
|
|
329
|
+
verb: 'delete',
|
|
330
|
+
object: { id: messageId, objectType: 'activity' },
|
|
331
|
+
target: { id: convId, objectType: 'conversation' },
|
|
332
|
+
}),
|
|
333
|
+
})
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
return this.request<void>('DELETE', `/messages/${messageId}`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async editMessage(
|
|
340
|
+
messageId: string,
|
|
341
|
+
roomId: string,
|
|
342
|
+
text: string,
|
|
343
|
+
options?: { markdown?: boolean },
|
|
344
|
+
): Promise<WebexMessage> {
|
|
345
|
+
if (this.useInternalAPI) {
|
|
346
|
+
const convUuid = this.decodeConvUuid(roomId)
|
|
347
|
+
const result = await this.internalRequest<InternalActivity>('/activities', {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
body: JSON.stringify({
|
|
350
|
+
verb: 'post',
|
|
351
|
+
object: { objectType: 'comment', displayName: text, content: text },
|
|
352
|
+
target: { id: convUuid, objectType: 'conversation' },
|
|
353
|
+
parent: { id: messageId, type: 'edit' },
|
|
354
|
+
clientTempId: `tmp-${Date.now()}`,
|
|
355
|
+
}),
|
|
356
|
+
})
|
|
357
|
+
return this.activityToMessage(result, roomId)
|
|
358
|
+
}
|
|
359
|
+
const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
|
|
360
|
+
return this.request<WebexMessage>('PUT', `/messages/${messageId}`, body)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async listPeople(options?: {
|
|
364
|
+
email?: string
|
|
365
|
+
displayName?: string
|
|
366
|
+
max?: number
|
|
367
|
+
}): Promise<WebexPerson[]> {
|
|
368
|
+
const params = new URLSearchParams()
|
|
369
|
+
if (options?.email) params.set('email', options.email)
|
|
370
|
+
if (options?.displayName) params.set('displayName', options.displayName)
|
|
371
|
+
if (options?.max) params.set('max', String(options.max))
|
|
372
|
+
const query = params.toString()
|
|
373
|
+
const path = query ? `/people?${query}` : '/people'
|
|
374
|
+
const data = await this.request<{ items: WebexPerson[] }>('GET', path)
|
|
375
|
+
return data.items
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async listMemberships(
|
|
379
|
+
roomId: string,
|
|
380
|
+
options?: { max?: number },
|
|
381
|
+
): Promise<WebexMembership[]> {
|
|
382
|
+
const params = new URLSearchParams()
|
|
383
|
+
params.set('roomId', roomId)
|
|
384
|
+
if (options?.max) params.set('max', String(options.max))
|
|
385
|
+
const data = await this.request<{ items: WebexMembership[] }>(
|
|
386
|
+
'GET',
|
|
387
|
+
`/memberships?${params}`,
|
|
388
|
+
)
|
|
389
|
+
return data.items
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
interface InternalActivity {
|
|
394
|
+
id: string
|
|
395
|
+
verb: string
|
|
396
|
+
actor?: { displayName?: string; emailAddress?: string; entryUUID?: string; id?: string }
|
|
397
|
+
object?: { content?: string; displayName?: string; objectType?: string }
|
|
398
|
+
target?: { id: string }
|
|
399
|
+
published: string
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface InternalConversation {
|
|
403
|
+
id: string
|
|
404
|
+
activities?: { items: InternalActivity[] }
|
|
405
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
|
|
2
|
+
import * as childProcess from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
import { WebexClient } from '../client'
|
|
5
|
+
import { WebexCredentialManager } from '../credential-manager'
|
|
6
|
+
import { loginAction, logoutAction, statusAction } from './auth'
|
|
7
|
+
|
|
8
|
+
describe('auth commands', () => {
|
|
9
|
+
let consoleSpy: ReturnType<typeof spyOn>
|
|
10
|
+
let _consoleErrorSpy: ReturnType<typeof spyOn>
|
|
11
|
+
const mockPerson = {
|
|
12
|
+
id: 'person-1',
|
|
13
|
+
displayName: 'Test User',
|
|
14
|
+
emails: ['test@example.com'],
|
|
15
|
+
orgId: 'org-1',
|
|
16
|
+
type: 'person' as const,
|
|
17
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
22
|
+
_consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
23
|
+
spyOn(childProcess, 'exec').mockImplementation((() => {}) as any)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
mock.restore()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('loginAction with --token', () => {
|
|
31
|
+
test('authenticates with provided token (bot token flow)', async () => {
|
|
32
|
+
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
33
|
+
spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
34
|
+
spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
|
|
35
|
+
|
|
36
|
+
await loginAction({ token: 'bot-token-123', pretty: false })
|
|
37
|
+
|
|
38
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
39
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
40
|
+
const output = JSON.parse(lastCall)
|
|
41
|
+
expect(output.authenticated).toBe(true)
|
|
42
|
+
expect(output.user.displayName).toBe('Test User')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('saves tokenType as manual with expiresAt 0', async () => {
|
|
46
|
+
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
47
|
+
spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
48
|
+
const saveSpy = spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
|
|
49
|
+
|
|
50
|
+
await loginAction({ token: 'bot-token-123', pretty: false })
|
|
51
|
+
|
|
52
|
+
const savedConfig = saveSpy.mock.calls[0][0] as { tokenType: string; expiresAt: number; refreshToken: string }
|
|
53
|
+
expect(savedConfig.tokenType).toBe('manual')
|
|
54
|
+
expect(savedConfig.expiresAt).toBe(0)
|
|
55
|
+
expect(savedConfig.refreshToken).toBe('')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('loginAction with --client-id and --client-secret', () => {
|
|
60
|
+
test('uses provided credentials for Device Grant flow', async () => {
|
|
61
|
+
spyOn(WebexCredentialManager.prototype, 'requestDeviceCode').mockResolvedValue({
|
|
62
|
+
deviceCode: 'd',
|
|
63
|
+
userCode: 'u',
|
|
64
|
+
verificationUri: 'https://v',
|
|
65
|
+
verificationUriComplete: 'https://vc',
|
|
66
|
+
expiresIn: 300,
|
|
67
|
+
interval: 0.01,
|
|
68
|
+
})
|
|
69
|
+
spyOn(WebexCredentialManager.prototype, 'pollDeviceToken').mockResolvedValue({
|
|
70
|
+
accessToken: 'at',
|
|
71
|
+
refreshToken: 'rt',
|
|
72
|
+
expiresAt: Date.now() + 3600000,
|
|
73
|
+
})
|
|
74
|
+
spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
|
|
75
|
+
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
76
|
+
spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
77
|
+
|
|
78
|
+
await loginAction({ clientId: 'my-id', clientSecret: 'my-secret', pretty: false })
|
|
79
|
+
|
|
80
|
+
expect(WebexCredentialManager.prototype.requestDeviceCode).toHaveBeenCalledWith('my-id')
|
|
81
|
+
expect(WebexCredentialManager.prototype.pollDeviceToken).toHaveBeenCalledWith('d', 0.01, 300, 'my-id', 'my-secret')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('saves tokenType as oauth in config', async () => {
|
|
85
|
+
spyOn(WebexCredentialManager.prototype, 'requestDeviceCode').mockResolvedValue({
|
|
86
|
+
deviceCode: 'd',
|
|
87
|
+
userCode: 'u',
|
|
88
|
+
verificationUri: 'https://v',
|
|
89
|
+
verificationUriComplete: 'https://vc',
|
|
90
|
+
expiresIn: 300,
|
|
91
|
+
interval: 0.01,
|
|
92
|
+
})
|
|
93
|
+
spyOn(WebexCredentialManager.prototype, 'pollDeviceToken').mockResolvedValue({
|
|
94
|
+
accessToken: 'at',
|
|
95
|
+
refreshToken: 'rt',
|
|
96
|
+
expiresAt: Date.now() + 3600000,
|
|
97
|
+
})
|
|
98
|
+
const saveSpy = spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
|
|
99
|
+
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
100
|
+
spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
101
|
+
|
|
102
|
+
await loginAction({ clientId: 'my-id', clientSecret: 'my-secret', pretty: false })
|
|
103
|
+
|
|
104
|
+
const savedConfig = saveSpy.mock.calls[0][0] as { tokenType: string }
|
|
105
|
+
expect(savedConfig.tokenType).toBe('oauth')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('saves clientId and clientSecret in config', async () => {
|
|
109
|
+
spyOn(WebexCredentialManager.prototype, 'requestDeviceCode').mockResolvedValue({
|
|
110
|
+
deviceCode: 'd',
|
|
111
|
+
userCode: 'u',
|
|
112
|
+
verificationUri: 'https://v',
|
|
113
|
+
verificationUriComplete: 'https://vc',
|
|
114
|
+
expiresIn: 300,
|
|
115
|
+
interval: 0.01,
|
|
116
|
+
})
|
|
117
|
+
spyOn(WebexCredentialManager.prototype, 'pollDeviceToken').mockResolvedValue({
|
|
118
|
+
accessToken: 'at',
|
|
119
|
+
refreshToken: 'rt',
|
|
120
|
+
expiresAt: Date.now() + 3600000,
|
|
121
|
+
})
|
|
122
|
+
spyOn(WebexCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
|
|
123
|
+
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
124
|
+
spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
125
|
+
|
|
126
|
+
await loginAction({ clientId: 'my-id', clientSecret: 'my-secret', pretty: false })
|
|
127
|
+
|
|
128
|
+
const savedConfig = (WebexCredentialManager.prototype.saveConfig as ReturnType<typeof spyOn>).mock.calls[0][0] as { clientId: string; clientSecret: string }
|
|
129
|
+
expect(savedConfig.clientId).toBe('my-id')
|
|
130
|
+
expect(savedConfig.clientSecret).toBe('my-secret')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('statusAction', () => {
|
|
135
|
+
test('shows authenticated status when token is valid', async () => {
|
|
136
|
+
spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
137
|
+
spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue('valid-token')
|
|
138
|
+
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
139
|
+
spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
140
|
+
|
|
141
|
+
await statusAction({ pretty: false })
|
|
142
|
+
|
|
143
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
144
|
+
const output = JSON.parse(lastCall)
|
|
145
|
+
expect(output.authenticated).toBe(true)
|
|
146
|
+
expect(output.user.displayName).toBe('Test User')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('shows not authenticated when no token', async () => {
|
|
150
|
+
spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
151
|
+
spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue(null)
|
|
152
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation(() => undefined as never)
|
|
153
|
+
|
|
154
|
+
await statusAction({ pretty: false })
|
|
155
|
+
|
|
156
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
157
|
+
const output = JSON.parse(lastCall)
|
|
158
|
+
expect(output.error).toContain('Not authenticated')
|
|
159
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('shows not authenticated when token validation fails', async () => {
|
|
163
|
+
spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
164
|
+
spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue('invalid-token')
|
|
165
|
+
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
166
|
+
spyOn(WebexClient.prototype, 'testAuth').mockRejectedValue(new Error('401 Unauthorized'))
|
|
167
|
+
|
|
168
|
+
await statusAction({ pretty: false })
|
|
169
|
+
|
|
170
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
171
|
+
const output = JSON.parse(lastCall)
|
|
172
|
+
expect(output.authenticated).toBe(false)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('loads config for stored client credentials', async () => {
|
|
176
|
+
spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue({
|
|
177
|
+
accessToken: 'at',
|
|
178
|
+
refreshToken: 'rt',
|
|
179
|
+
expiresAt: Date.now() + 3600000,
|
|
180
|
+
clientId: 'stored-id',
|
|
181
|
+
clientSecret: 'stored-secret',
|
|
182
|
+
})
|
|
183
|
+
spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue('valid-token')
|
|
184
|
+
spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
185
|
+
spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockPerson)
|
|
186
|
+
|
|
187
|
+
await statusAction({ pretty: false })
|
|
188
|
+
|
|
189
|
+
expect(WebexCredentialManager.prototype.getToken).toHaveBeenCalledWith('stored-id', 'stored-secret')
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('logoutAction', () => {
|
|
194
|
+
test('clears credentials when authenticated', async () => {
|
|
195
|
+
spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue({
|
|
196
|
+
accessToken: 'token',
|
|
197
|
+
refreshToken: 'refresh',
|
|
198
|
+
expiresAt: Date.now() + 3600000,
|
|
199
|
+
})
|
|
200
|
+
const clearSpy = spyOn(WebexCredentialManager.prototype, 'clearCredentials').mockResolvedValue(undefined)
|
|
201
|
+
|
|
202
|
+
await logoutAction({ pretty: false })
|
|
203
|
+
|
|
204
|
+
expect(clearSpy).toHaveBeenCalled()
|
|
205
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
206
|
+
const output = JSON.parse(lastCall)
|
|
207
|
+
expect(output.success).toBe(true)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('shows error when not authenticated', async () => {
|
|
211
|
+
spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
212
|
+
const exitSpy = spyOn(process, 'exit').mockImplementation(() => undefined as never)
|
|
213
|
+
|
|
214
|
+
await logoutAction({ pretty: false })
|
|
215
|
+
|
|
216
|
+
const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0] as string
|
|
217
|
+
const output = JSON.parse(lastCall)
|
|
218
|
+
expect(output.error).toContain('Not authenticated')
|
|
219
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
})
|