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,314 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { WebexCredentialManager } from './credential-manager'
|
|
7
|
+
|
|
8
|
+
describe('WebexCredentialManager', () => {
|
|
9
|
+
let tempDir: string
|
|
10
|
+
let credManager: WebexCredentialManager
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tempDir = await mkdtemp(join(tmpdir(), 'webex-cred-test-'))
|
|
14
|
+
credManager = new WebexCredentialManager(tempDir)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
19
|
+
mock.restore()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('loadConfig returns null when no file exists', async () => {
|
|
23
|
+
expect(await credManager.loadConfig()).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('saveConfig + loadConfig round trip with OAuth tokens', async () => {
|
|
27
|
+
const config = {
|
|
28
|
+
accessToken: 'test-access-token',
|
|
29
|
+
refreshToken: 'test-refresh-token',
|
|
30
|
+
expiresAt: Date.now() + 3600000,
|
|
31
|
+
}
|
|
32
|
+
await credManager.saveConfig(config)
|
|
33
|
+
const loaded = await credManager.loadConfig()
|
|
34
|
+
expect(loaded).toEqual(config)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('getToken returns accessToken when not expired', async () => {
|
|
38
|
+
await credManager.saveConfig({
|
|
39
|
+
accessToken: 'valid-token',
|
|
40
|
+
refreshToken: 'refresh',
|
|
41
|
+
expiresAt: Date.now() + 3600000, // 1 hour from now
|
|
42
|
+
})
|
|
43
|
+
const token = await credManager.getToken()
|
|
44
|
+
expect(token).toBe('valid-token')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('getToken returns null when expired and no refresh available', async () => {
|
|
48
|
+
await credManager.saveConfig({
|
|
49
|
+
accessToken: 'expired-token',
|
|
50
|
+
refreshToken: 'bad-refresh',
|
|
51
|
+
expiresAt: Date.now() - 1000, // Already expired
|
|
52
|
+
})
|
|
53
|
+
const token = await credManager.getToken()
|
|
54
|
+
expect(token).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('getToken auto-refreshes expired token', async () => {
|
|
58
|
+
const originalFetch = globalThis.fetch
|
|
59
|
+
globalThis.fetch = mock(() =>
|
|
60
|
+
Promise.resolve(
|
|
61
|
+
new Response(
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
access_token: 'new-access-token',
|
|
64
|
+
refresh_token: 'new-refresh-token',
|
|
65
|
+
expires_in: 3600,
|
|
66
|
+
}),
|
|
67
|
+
{ status: 200 },
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
) as typeof fetch
|
|
71
|
+
|
|
72
|
+
await credManager.saveConfig({
|
|
73
|
+
accessToken: 'expired-token',
|
|
74
|
+
refreshToken: 'valid-refresh',
|
|
75
|
+
expiresAt: Date.now() - 1000,
|
|
76
|
+
clientId: 'test-client-id',
|
|
77
|
+
clientSecret: 'test-client-secret',
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const token = await credManager.getToken()
|
|
81
|
+
expect(token).toBe('new-access-token')
|
|
82
|
+
|
|
83
|
+
// Verify updated config was saved
|
|
84
|
+
const config = await credManager.loadConfig()
|
|
85
|
+
expect(config?.accessToken).toBe('new-access-token')
|
|
86
|
+
expect(config?.refreshToken).toBe('new-refresh-token')
|
|
87
|
+
|
|
88
|
+
globalThis.fetch = originalFetch
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('requestDeviceCode calls device authorize endpoint', async () => {
|
|
92
|
+
const originalFetch = globalThis.fetch
|
|
93
|
+
globalThis.fetch = mock(() =>
|
|
94
|
+
Promise.resolve(
|
|
95
|
+
new Response(
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
device_code: 'device-123',
|
|
98
|
+
user_code: '123456',
|
|
99
|
+
verification_uri: 'https://login-k.webex.com/verify',
|
|
100
|
+
verification_uri_complete: 'https://login-k.webex.com/verify?userCode=abc',
|
|
101
|
+
expires_in: 300,
|
|
102
|
+
interval: 2,
|
|
103
|
+
}),
|
|
104
|
+
{ status: 200 },
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
) as typeof fetch
|
|
108
|
+
|
|
109
|
+
const result = await credManager.requestDeviceCode('test-client-id')
|
|
110
|
+
expect(result.deviceCode).toBe('device-123')
|
|
111
|
+
expect(result.userCode).toBe('123456')
|
|
112
|
+
expect(result.verificationUri).toBe('https://login-k.webex.com/verify')
|
|
113
|
+
expect(result.interval).toBe(2)
|
|
114
|
+
|
|
115
|
+
globalThis.fetch = originalFetch
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('requestDeviceCode throws on failure', async () => {
|
|
119
|
+
const originalFetch = globalThis.fetch
|
|
120
|
+
globalThis.fetch = mock(() =>
|
|
121
|
+
Promise.resolve(new Response('{"error":"invalid_client"}', { status: 400 })),
|
|
122
|
+
) as typeof fetch
|
|
123
|
+
|
|
124
|
+
await expect(credManager.requestDeviceCode('test-client-id')).rejects.toThrow('Device authorization failed')
|
|
125
|
+
|
|
126
|
+
globalThis.fetch = originalFetch
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('pollDeviceToken polls until authorized', async () => {
|
|
130
|
+
const originalFetch = globalThis.fetch
|
|
131
|
+
let callCount = 0
|
|
132
|
+
globalThis.fetch = mock(() => {
|
|
133
|
+
callCount++
|
|
134
|
+
if (callCount <= 2) {
|
|
135
|
+
return Promise.resolve(new Response('', { status: 428 }))
|
|
136
|
+
}
|
|
137
|
+
return Promise.resolve(
|
|
138
|
+
new Response(
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
access_token: 'device-access-token',
|
|
141
|
+
refresh_token: 'device-refresh-token',
|
|
142
|
+
expires_in: 3600,
|
|
143
|
+
}),
|
|
144
|
+
{ status: 200 },
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
}) as typeof fetch
|
|
148
|
+
|
|
149
|
+
const config = await credManager.pollDeviceToken('device-123', 0.01, 30, 'test-client-id', 'test-client-secret')
|
|
150
|
+
expect(config.accessToken).toBe('device-access-token')
|
|
151
|
+
expect(config.refreshToken).toBe('device-refresh-token')
|
|
152
|
+
|
|
153
|
+
globalThis.fetch = originalFetch
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('clearCredentials removes the file', async () => {
|
|
157
|
+
await credManager.saveConfig({
|
|
158
|
+
accessToken: 'token',
|
|
159
|
+
refreshToken: 'refresh',
|
|
160
|
+
expiresAt: Date.now() + 3600000,
|
|
161
|
+
})
|
|
162
|
+
await credManager.clearCredentials()
|
|
163
|
+
expect(await credManager.loadConfig()).toBeNull()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('clearCredentials does nothing when no file', async () => {
|
|
167
|
+
await credManager.clearCredentials() // Should not throw
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('credentials file has 0o600 permissions', async () => {
|
|
171
|
+
await credManager.saveConfig({
|
|
172
|
+
accessToken: 'token',
|
|
173
|
+
refreshToken: 'refresh',
|
|
174
|
+
expiresAt: Date.now() + 3600000,
|
|
175
|
+
})
|
|
176
|
+
const { stat } = await import('node:fs/promises')
|
|
177
|
+
const credPath = join(tempDir, 'webex-credentials.json')
|
|
178
|
+
const stats = await stat(credPath)
|
|
179
|
+
const mode = stats.mode & 0o777
|
|
180
|
+
expect(mode).toBe(0o600)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('pollDeviceToken with undefined clientSecret uses empty Basic auth', async () => {
|
|
184
|
+
const originalFetch = globalThis.fetch
|
|
185
|
+
let capturedAuth: string | null = null
|
|
186
|
+
globalThis.fetch = mock((url: string, init?: RequestInit) => {
|
|
187
|
+
capturedAuth = (init?.headers as Record<string, string>)?.Authorization ?? null
|
|
188
|
+
return Promise.resolve(
|
|
189
|
+
new Response(
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
access_token: 'token',
|
|
192
|
+
refresh_token: 'refresh',
|
|
193
|
+
expires_in: 3600,
|
|
194
|
+
}),
|
|
195
|
+
{ status: 200 },
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
}) as typeof fetch
|
|
199
|
+
|
|
200
|
+
await credManager.pollDeviceToken('device-123', 0.01, 30, 'test-client-id')
|
|
201
|
+
expect(capturedAuth).toBe(`Basic ${btoa('test-client-id:')}`)
|
|
202
|
+
|
|
203
|
+
globalThis.fetch = originalFetch
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('pollDeviceToken does not auto-save config', async () => {
|
|
207
|
+
const originalFetch = globalThis.fetch
|
|
208
|
+
globalThis.fetch = mock(() =>
|
|
209
|
+
Promise.resolve(
|
|
210
|
+
new Response(
|
|
211
|
+
JSON.stringify({
|
|
212
|
+
access_token: 'token',
|
|
213
|
+
refresh_token: 'refresh',
|
|
214
|
+
expires_in: 3600,
|
|
215
|
+
}),
|
|
216
|
+
{ status: 200 },
|
|
217
|
+
),
|
|
218
|
+
),
|
|
219
|
+
) as typeof fetch
|
|
220
|
+
|
|
221
|
+
await credManager.pollDeviceToken('device-123', 0.01, 30, 'test-client-id', 'test-client-secret')
|
|
222
|
+
|
|
223
|
+
const loaded = await credManager.loadConfig()
|
|
224
|
+
expect(loaded).toBeNull()
|
|
225
|
+
|
|
226
|
+
globalThis.fetch = originalFetch
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('getToken returns null when expired and no client credentials available', async () => {
|
|
230
|
+
await credManager.saveConfig({
|
|
231
|
+
accessToken: 'expired-token',
|
|
232
|
+
refreshToken: 'valid-refresh',
|
|
233
|
+
expiresAt: Date.now() - 1000,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const token = await credManager.getToken()
|
|
237
|
+
expect(token).toBeNull()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('getToken returns manual token without attempting refresh', async () => {
|
|
241
|
+
await credManager.saveConfig({
|
|
242
|
+
accessToken: 'my-bot-token',
|
|
243
|
+
refreshToken: '',
|
|
244
|
+
expiresAt: 0,
|
|
245
|
+
tokenType: 'manual',
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const token = await credManager.getToken()
|
|
249
|
+
expect(token).toBe('my-bot-token')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('getToken uses stored clientId/clientSecret for refresh', async () => {
|
|
253
|
+
const originalFetch = globalThis.fetch
|
|
254
|
+
globalThis.fetch = mock(() =>
|
|
255
|
+
Promise.resolve(
|
|
256
|
+
new Response(
|
|
257
|
+
JSON.stringify({
|
|
258
|
+
access_token: 'refreshed-token',
|
|
259
|
+
refresh_token: 'new-refresh',
|
|
260
|
+
expires_in: 3600,
|
|
261
|
+
}),
|
|
262
|
+
{ status: 200 },
|
|
263
|
+
),
|
|
264
|
+
),
|
|
265
|
+
) as typeof fetch
|
|
266
|
+
|
|
267
|
+
await credManager.saveConfig({
|
|
268
|
+
accessToken: 'expired-token',
|
|
269
|
+
refreshToken: 'valid-refresh',
|
|
270
|
+
expiresAt: Date.now() - 1000,
|
|
271
|
+
clientId: 'stored-client-id',
|
|
272
|
+
clientSecret: 'stored-client-secret',
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const token = await credManager.getToken()
|
|
276
|
+
expect(token).toBe('refreshed-token')
|
|
277
|
+
|
|
278
|
+
globalThis.fetch = originalFetch
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('saveConfig persists clientId and clientSecret', async () => {
|
|
282
|
+
await credManager.saveConfig({
|
|
283
|
+
accessToken: 'token',
|
|
284
|
+
refreshToken: 'refresh',
|
|
285
|
+
expiresAt: Date.now() + 3600000,
|
|
286
|
+
clientId: 'my-client-id',
|
|
287
|
+
clientSecret: 'my-client-secret',
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const loaded = await credManager.loadConfig()
|
|
291
|
+
expect(loaded?.clientId).toBe('my-client-id')
|
|
292
|
+
expect(loaded?.clientSecret).toBe('my-client-secret')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('loadConfig backward compat — old config without clientId/clientSecret', async () => {
|
|
296
|
+
// Write raw JSON without clientId/clientSecret fields
|
|
297
|
+
const credPath = join(tempDir, 'webex-credentials.json')
|
|
298
|
+
await writeFile(
|
|
299
|
+
credPath,
|
|
300
|
+
JSON.stringify({
|
|
301
|
+
accessToken: 'old-token',
|
|
302
|
+
refreshToken: 'old-refresh',
|
|
303
|
+
expiresAt: Date.now() + 3600000,
|
|
304
|
+
}),
|
|
305
|
+
'utf-8',
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
const loaded = await credManager.loadConfig()
|
|
309
|
+
expect(loaded).not.toBeNull()
|
|
310
|
+
expect(loaded?.accessToken).toBe('old-token')
|
|
311
|
+
expect(loaded?.clientId).toBeUndefined()
|
|
312
|
+
expect(loaded?.clientSecret).toBeUndefined()
|
|
313
|
+
})
|
|
314
|
+
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { getWebexAppCredentials } from './app-config'
|
|
7
|
+
import type { WebexConfig } from './types'
|
|
8
|
+
import { WebexConfigSchema } from './types'
|
|
9
|
+
|
|
10
|
+
const OAUTH_DEVICE_AUTHORIZE_URL = 'https://webexapis.com/v1/device/authorize'
|
|
11
|
+
const OAUTH_DEVICE_TOKEN_URL = 'https://webexapis.com/v1/device/token'
|
|
12
|
+
const OAUTH_TOKEN_URL = 'https://webexapis.com/v1/access_token'
|
|
13
|
+
const OAUTH_SCOPES = 'spark:all'
|
|
14
|
+
|
|
15
|
+
export { OAUTH_SCOPES }
|
|
16
|
+
|
|
17
|
+
export class WebexCredentialManager {
|
|
18
|
+
private configDir: string
|
|
19
|
+
private credentialsPath: string
|
|
20
|
+
|
|
21
|
+
constructor(configDir?: string) {
|
|
22
|
+
this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
|
|
23
|
+
this.credentialsPath = join(this.configDir, 'webex-credentials.json')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async loadConfig(): Promise<WebexConfig | null> {
|
|
27
|
+
if (!existsSync(this.credentialsPath)) return null
|
|
28
|
+
const content = await readFile(this.credentialsPath, 'utf-8')
|
|
29
|
+
const result = WebexConfigSchema.safeParse(JSON.parse(content))
|
|
30
|
+
if (!result.success) return null
|
|
31
|
+
return result.data
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async saveConfig(config: WebexConfig): Promise<void> {
|
|
35
|
+
await mkdir(this.configDir, { recursive: true })
|
|
36
|
+
const tmpPath = `${this.credentialsPath}.tmp`
|
|
37
|
+
await writeFile(tmpPath, JSON.stringify(config, null, 2), {
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
mode: 0o600,
|
|
40
|
+
})
|
|
41
|
+
await rename(tmpPath, this.credentialsPath)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getToken(clientId?: string, clientSecret?: string): Promise<string | null> {
|
|
45
|
+
const config = await this.loadConfig()
|
|
46
|
+
if (!config) return null
|
|
47
|
+
|
|
48
|
+
if (config.tokenType === 'manual' || config.tokenType === 'extracted') {
|
|
49
|
+
return config.accessToken
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (config.expiresAt < Date.now() + 5 * 60 * 1000) {
|
|
53
|
+
const builtinCreds = getWebexAppCredentials()
|
|
54
|
+
const resolvedClientId = clientId ?? config.clientId ?? builtinCreds.clientId
|
|
55
|
+
const resolvedClientSecret = clientSecret ?? config.clientSecret ?? builtinCreds.clientSecret
|
|
56
|
+
const refreshed = await this.refreshToken(config.refreshToken, resolvedClientId, resolvedClientSecret)
|
|
57
|
+
if (refreshed) return refreshed.accessToken
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return config.accessToken
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async refreshToken(refreshToken: string, clientId: string, clientSecret: string): Promise<WebexConfig | null> {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(OAUTH_TOKEN_URL, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
69
|
+
body: new URLSearchParams({
|
|
70
|
+
grant_type: 'refresh_token',
|
|
71
|
+
client_id: clientId,
|
|
72
|
+
client_secret: clientSecret,
|
|
73
|
+
refresh_token: refreshToken,
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (!response.ok) return null
|
|
78
|
+
|
|
79
|
+
const data = (await response.json()) as {
|
|
80
|
+
access_token: string
|
|
81
|
+
refresh_token: string
|
|
82
|
+
expires_in: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const config: WebexConfig = {
|
|
86
|
+
accessToken: data.access_token,
|
|
87
|
+
refreshToken: data.refresh_token,
|
|
88
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await this.saveConfig(config)
|
|
92
|
+
return config
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async requestDeviceCode(clientId: string, scopes?: string): Promise<{
|
|
99
|
+
deviceCode: string
|
|
100
|
+
userCode: string
|
|
101
|
+
verificationUri: string
|
|
102
|
+
verificationUriComplete: string
|
|
103
|
+
expiresIn: number
|
|
104
|
+
interval: number
|
|
105
|
+
}> {
|
|
106
|
+
const response = await fetch(OAUTH_DEVICE_AUTHORIZE_URL, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
109
|
+
body: new URLSearchParams({
|
|
110
|
+
client_id: clientId,
|
|
111
|
+
scope: scopes ?? OAUTH_SCOPES,
|
|
112
|
+
}),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const errorBody = await response.text()
|
|
117
|
+
throw new Error(`Device authorization failed: ${response.status} ${errorBody}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const data = (await response.json()) as {
|
|
121
|
+
device_code: string
|
|
122
|
+
user_code: string
|
|
123
|
+
verification_uri: string
|
|
124
|
+
verification_uri_complete: string
|
|
125
|
+
expires_in: number
|
|
126
|
+
interval: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
deviceCode: data.device_code,
|
|
131
|
+
userCode: data.user_code,
|
|
132
|
+
verificationUri: data.verification_uri,
|
|
133
|
+
verificationUriComplete: data.verification_uri_complete,
|
|
134
|
+
expiresIn: data.expires_in,
|
|
135
|
+
interval: data.interval,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async pollDeviceToken(deviceCode: string, interval: number, expiresIn: number, clientId: string, clientSecret?: string): Promise<WebexConfig> {
|
|
140
|
+
const basicAuth = btoa(`${clientId}:${clientSecret ?? ''}`)
|
|
141
|
+
const deadline = Date.now() + expiresIn * 1000
|
|
142
|
+
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000))
|
|
145
|
+
|
|
146
|
+
const response = await fetch(OAUTH_DEVICE_TOKEN_URL, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Basic ${basicAuth}`,
|
|
150
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
151
|
+
},
|
|
152
|
+
body: new URLSearchParams({
|
|
153
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
154
|
+
device_code: deviceCode,
|
|
155
|
+
client_id: clientId,
|
|
156
|
+
}),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (response.ok) {
|
|
160
|
+
const data = (await response.json()) as {
|
|
161
|
+
access_token: string
|
|
162
|
+
refresh_token: string
|
|
163
|
+
expires_in: number
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const config: WebexConfig = {
|
|
167
|
+
accessToken: data.access_token,
|
|
168
|
+
refreshToken: data.refresh_token,
|
|
169
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return config
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (response.status === 428) continue
|
|
176
|
+
|
|
177
|
+
const errorBody = (await response.json().catch(() => null)) as {
|
|
178
|
+
errors?: Array<{ description: string }>
|
|
179
|
+
} | null
|
|
180
|
+
const errorDesc = errorBody?.errors?.[0]?.description ?? ''
|
|
181
|
+
|
|
182
|
+
if (errorDesc.includes('authorization_pending') || errorDesc.includes('slow_down')) {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw new Error(`Device token exchange failed: ${response.status} ${errorDesc}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw new Error('Device authorization timed out')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async clearCredentials(): Promise<void> {
|
|
193
|
+
if (existsSync(this.credentialsPath)) {
|
|
194
|
+
await rm(this.credentialsPath)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { WebexClient } from './client'
|
|
4
|
+
import { WebexCredentialManager } from './credential-manager'
|
|
5
|
+
import { ensureWebexAuth } from './ensure-auth'
|
|
6
|
+
import { WebexTokenExtractor } from './token-extractor'
|
|
7
|
+
|
|
8
|
+
let loadConfigSpy: ReturnType<typeof spyOn>
|
|
9
|
+
let getTokenSpy: ReturnType<typeof spyOn>
|
|
10
|
+
let loginSpy: ReturnType<typeof spyOn>
|
|
11
|
+
let testAuthSpy: ReturnType<typeof spyOn>
|
|
12
|
+
let extractSpy: ReturnType<typeof spyOn>
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
loadConfigSpy = spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
16
|
+
getTokenSpy = spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue(null)
|
|
17
|
+
loginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue({} as WebexClient)
|
|
18
|
+
testAuthSpy = spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue({
|
|
19
|
+
id: 'user-123',
|
|
20
|
+
displayName: 'Test User',
|
|
21
|
+
emails: ['test@example.com'],
|
|
22
|
+
type: 'person',
|
|
23
|
+
})
|
|
24
|
+
extractSpy = spyOn(WebexTokenExtractor.prototype, 'extract').mockResolvedValue(null)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
loadConfigSpy?.mockRestore()
|
|
29
|
+
getTokenSpy?.mockRestore()
|
|
30
|
+
loginSpy?.mockRestore()
|
|
31
|
+
testAuthSpy?.mockRestore()
|
|
32
|
+
extractSpy?.mockRestore()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('ensureWebexAuth', () => {
|
|
36
|
+
test('does nothing when no config stored', async () => {
|
|
37
|
+
// given
|
|
38
|
+
loadConfigSpy.mockResolvedValue(null)
|
|
39
|
+
|
|
40
|
+
// when
|
|
41
|
+
await ensureWebexAuth()
|
|
42
|
+
|
|
43
|
+
// then
|
|
44
|
+
expect(getTokenSpy).not.toHaveBeenCalled()
|
|
45
|
+
expect(testAuthSpy).not.toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('validates token when stored', async () => {
|
|
49
|
+
// given
|
|
50
|
+
loadConfigSpy.mockResolvedValue({
|
|
51
|
+
accessToken: 'test-webex-token',
|
|
52
|
+
refreshToken: 'refresh',
|
|
53
|
+
expiresAt: Date.now() + 3600000,
|
|
54
|
+
clientId: 'stored-id',
|
|
55
|
+
clientSecret: 'stored-secret',
|
|
56
|
+
})
|
|
57
|
+
getTokenSpy.mockResolvedValue('test-webex-token')
|
|
58
|
+
|
|
59
|
+
// when
|
|
60
|
+
await ensureWebexAuth()
|
|
61
|
+
|
|
62
|
+
// then
|
|
63
|
+
expect(getTokenSpy).toHaveBeenCalledWith('stored-id', 'stored-secret')
|
|
64
|
+
expect(loginSpy).toHaveBeenCalledWith({ token: 'test-webex-token' })
|
|
65
|
+
expect(testAuthSpy).toHaveBeenCalled()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('does not throw when token validation fails', async () => {
|
|
69
|
+
// given
|
|
70
|
+
loadConfigSpy.mockResolvedValue({
|
|
71
|
+
accessToken: 'invalid-token',
|
|
72
|
+
refreshToken: 'refresh',
|
|
73
|
+
expiresAt: Date.now() + 3600000,
|
|
74
|
+
})
|
|
75
|
+
getTokenSpy.mockResolvedValue('invalid-token')
|
|
76
|
+
testAuthSpy.mockRejectedValue(new Error('401 Unauthorized'))
|
|
77
|
+
|
|
78
|
+
// when / then
|
|
79
|
+
await expect(ensureWebexAuth()).resolves.toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('does not throw when credential manager fails', async () => {
|
|
83
|
+
// given
|
|
84
|
+
getTokenSpy.mockRejectedValue(new Error('Disk read error'))
|
|
85
|
+
|
|
86
|
+
// when / then
|
|
87
|
+
await expect(ensureWebexAuth()).resolves.toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { WebexClient } from './client'
|
|
2
|
+
import { WebexCredentialManager } from './credential-manager'
|
|
3
|
+
import { WebexTokenExtractor } from './token-extractor'
|
|
4
|
+
|
|
5
|
+
export async function ensureWebexAuth(): Promise<void> {
|
|
6
|
+
try {
|
|
7
|
+
const credManager = new WebexCredentialManager()
|
|
8
|
+
const config = await credManager.loadConfig()
|
|
9
|
+
|
|
10
|
+
if (config) {
|
|
11
|
+
const token = await credManager.getToken(config.clientId, config.clientSecret)
|
|
12
|
+
if (token) {
|
|
13
|
+
const client = new WebexClient()
|
|
14
|
+
await client.login({ token })
|
|
15
|
+
await client.testAuth()
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const extractor = new WebexTokenExtractor()
|
|
21
|
+
const extracted = await extractor.extract()
|
|
22
|
+
if (!extracted) return
|
|
23
|
+
|
|
24
|
+
const client = new WebexClient()
|
|
25
|
+
await client.login({ token: extracted.accessToken })
|
|
26
|
+
await client.testAuth()
|
|
27
|
+
|
|
28
|
+
await credManager.saveConfig({
|
|
29
|
+
accessToken: extracted.accessToken,
|
|
30
|
+
refreshToken: extracted.refreshToken ?? '',
|
|
31
|
+
expiresAt: extracted.expiresAt ?? 0,
|
|
32
|
+
tokenType: 'extracted',
|
|
33
|
+
deviceUrl: extracted.deviceUrl,
|
|
34
|
+
})
|
|
35
|
+
} catch {
|
|
36
|
+
// Intentionally silent — best-effort preflight that should not block commands
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import * as webex from './index'
|
|
4
|
+
|
|
5
|
+
describe('webex barrel exports', () => {
|
|
6
|
+
test('exports WebexClient', () => {
|
|
7
|
+
expect(webex.WebexClient).toBeDefined()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('exports WebexCredentialManager', () => {
|
|
11
|
+
expect(webex.WebexCredentialManager).toBeDefined()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('exports WebexError', () => {
|
|
15
|
+
expect(webex.WebexError).toBeDefined()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('exports Zod schemas', () => {
|
|
19
|
+
expect(webex.WebexSpaceSchema).toBeDefined()
|
|
20
|
+
expect(webex.WebexMessageSchema).toBeDefined()
|
|
21
|
+
expect(webex.WebexPersonSchema).toBeDefined()
|
|
22
|
+
expect(webex.WebexMembershipSchema).toBeDefined()
|
|
23
|
+
expect(webex.WebexConfigSchema).toBeDefined()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { WebexClient } from './client'
|
|
2
|
+
export { WebexCredentialManager } from './credential-manager'
|
|
3
|
+
export { WebexTokenExtractor } from './token-extractor'
|
|
4
|
+
export type { ExtractedWebexToken } from './token-extractor'
|
|
5
|
+
export { WebexError } from './types'
|
|
6
|
+
export type {
|
|
7
|
+
WebexConfig,
|
|
8
|
+
WebexMembership,
|
|
9
|
+
WebexMessage,
|
|
10
|
+
WebexPerson,
|
|
11
|
+
WebexSpace,
|
|
12
|
+
} from './types'
|
|
13
|
+
export {
|
|
14
|
+
WebexConfigSchema,
|
|
15
|
+
WebexMembershipSchema,
|
|
16
|
+
WebexMessageSchema,
|
|
17
|
+
WebexPersonSchema,
|
|
18
|
+
WebexSpaceSchema,
|
|
19
|
+
} from './types'
|