agent-messenger 2.0.0 → 2.1.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/README.md +33 -29
- package/dist/package.json +10 -2
- 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/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 +45 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -0
- package/dist/src/platforms/webex/client.js +175 -0
- package/dist/src/platforms/webex/client.js.map +1 -0
- package/dist/src/platforms/webex/commands/auth.d.ts +15 -0
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/auth.js +124 -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 +20 -0
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -0
- package/dist/src/platforms/webex/index.d.ts +6 -0
- package/dist/src/platforms/webex/index.d.ts.map +1 -0
- package/dist/src/platforms/webex/index.js +5 -0
- package/dist/src/platforms/webex/index.js.map +1 -0
- package/dist/src/platforms/webex/types.d.ts +124 -0
- package/dist/src/platforms/webex/types.d.ts.map +1 -0
- package/dist/src/platforms/webex/types.js +63 -0
- package/dist/src/platforms/webex/types.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/meta.json +1 -0
- package/docs/content/docs/cli/webex.mdx +291 -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/package.json +10 -2
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +386 -0
- package/skills/agent-webex/references/authentication.md +318 -0
- package/skills/agent-webex/references/common-patterns.md +723 -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/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 +429 -0
- package/src/platforms/webex/client.ts +247 -0
- package/src/platforms/webex/commands/auth.test.ts +222 -0
- package/src/platforms/webex/commands/auth.ts +180 -0
- package/src/platforms/webex/commands/index.ts +5 -0
- package/src/platforms/webex/commands/member.test.ts +103 -0
- package/src/platforms/webex/commands/member.ts +45 -0
- package/src/platforms/webex/commands/message.test.ts +231 -0
- package/src/platforms/webex/commands/message.ts +204 -0
- package/src/platforms/webex/commands/snapshot.test.ts +96 -0
- package/src/platforms/webex/commands/snapshot.ts +91 -0
- package/src/platforms/webex/commands/space.test.ts +206 -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 +85 -0
- package/src/platforms/webex/ensure-auth.ts +19 -0
- package/src/platforms/webex/index.test.ts +25 -0
- package/src/platforms/webex/index.ts +17 -0
- package/src/platforms/webex/types.test.ts +307 -0
- package/src/platforms/webex/types.ts +127 -0
- package/src/tui/adapters/webex-adapter.ts +103 -0
- package/src/tui/app.ts +2 -0
|
@@ -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') {
|
|
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,85 @@
|
|
|
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
|
+
|
|
7
|
+
let loadConfigSpy: ReturnType<typeof spyOn>
|
|
8
|
+
let getTokenSpy: ReturnType<typeof spyOn>
|
|
9
|
+
let loginSpy: ReturnType<typeof spyOn>
|
|
10
|
+
let testAuthSpy: ReturnType<typeof spyOn>
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
loadConfigSpy = spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
14
|
+
getTokenSpy = spyOn(WebexCredentialManager.prototype, 'getToken').mockResolvedValue(null)
|
|
15
|
+
loginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue({} as WebexClient)
|
|
16
|
+
testAuthSpy = spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue({
|
|
17
|
+
id: 'user-123',
|
|
18
|
+
displayName: 'Test User',
|
|
19
|
+
emails: ['test@example.com'],
|
|
20
|
+
type: 'person',
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
loadConfigSpy?.mockRestore()
|
|
26
|
+
getTokenSpy?.mockRestore()
|
|
27
|
+
loginSpy?.mockRestore()
|
|
28
|
+
testAuthSpy?.mockRestore()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('ensureWebexAuth', () => {
|
|
32
|
+
test('does nothing when no config stored', async () => {
|
|
33
|
+
// given
|
|
34
|
+
loadConfigSpy.mockResolvedValue(null)
|
|
35
|
+
|
|
36
|
+
// when
|
|
37
|
+
await ensureWebexAuth()
|
|
38
|
+
|
|
39
|
+
// then
|
|
40
|
+
expect(getTokenSpy).not.toHaveBeenCalled()
|
|
41
|
+
expect(testAuthSpy).not.toHaveBeenCalled()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('validates token when stored', async () => {
|
|
45
|
+
// given
|
|
46
|
+
loadConfigSpy.mockResolvedValue({
|
|
47
|
+
accessToken: 'test-webex-token',
|
|
48
|
+
refreshToken: 'refresh',
|
|
49
|
+
expiresAt: Date.now() + 3600000,
|
|
50
|
+
clientId: 'stored-id',
|
|
51
|
+
clientSecret: 'stored-secret',
|
|
52
|
+
})
|
|
53
|
+
getTokenSpy.mockResolvedValue('test-webex-token')
|
|
54
|
+
|
|
55
|
+
// when
|
|
56
|
+
await ensureWebexAuth()
|
|
57
|
+
|
|
58
|
+
// then
|
|
59
|
+
expect(getTokenSpy).toHaveBeenCalledWith('stored-id', 'stored-secret')
|
|
60
|
+
expect(loginSpy).toHaveBeenCalledWith({ token: 'test-webex-token' })
|
|
61
|
+
expect(testAuthSpy).toHaveBeenCalled()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('does not throw when token validation fails', async () => {
|
|
65
|
+
// given
|
|
66
|
+
loadConfigSpy.mockResolvedValue({
|
|
67
|
+
accessToken: 'invalid-token',
|
|
68
|
+
refreshToken: 'refresh',
|
|
69
|
+
expiresAt: Date.now() + 3600000,
|
|
70
|
+
})
|
|
71
|
+
getTokenSpy.mockResolvedValue('invalid-token')
|
|
72
|
+
testAuthSpy.mockRejectedValue(new Error('401 Unauthorized'))
|
|
73
|
+
|
|
74
|
+
// when / then
|
|
75
|
+
await expect(ensureWebexAuth()).resolves.toBeUndefined()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('does not throw when credential manager fails', async () => {
|
|
79
|
+
// given
|
|
80
|
+
getTokenSpy.mockRejectedValue(new Error('Disk read error'))
|
|
81
|
+
|
|
82
|
+
// when / then
|
|
83
|
+
await expect(ensureWebexAuth()).resolves.toBeUndefined()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { WebexClient } from './client'
|
|
2
|
+
import { WebexCredentialManager } from './credential-manager'
|
|
3
|
+
|
|
4
|
+
export async function ensureWebexAuth(): Promise<void> {
|
|
5
|
+
try {
|
|
6
|
+
const credManager = new WebexCredentialManager()
|
|
7
|
+
const config = await credManager.loadConfig()
|
|
8
|
+
if (!config) return
|
|
9
|
+
|
|
10
|
+
const token = await credManager.getToken(config.clientId, config.clientSecret)
|
|
11
|
+
if (!token) return
|
|
12
|
+
|
|
13
|
+
const client = new WebexClient()
|
|
14
|
+
await client.login({ token })
|
|
15
|
+
await client.testAuth()
|
|
16
|
+
} catch {
|
|
17
|
+
// Intentionally silent — best-effort preflight that should not block commands
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -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,17 @@
|
|
|
1
|
+
export { WebexClient } from './client'
|
|
2
|
+
export { WebexCredentialManager } from './credential-manager'
|
|
3
|
+
export { WebexError } from './types'
|
|
4
|
+
export type {
|
|
5
|
+
WebexConfig,
|
|
6
|
+
WebexMembership,
|
|
7
|
+
WebexMessage,
|
|
8
|
+
WebexPerson,
|
|
9
|
+
WebexSpace,
|
|
10
|
+
} from './types'
|
|
11
|
+
export {
|
|
12
|
+
WebexConfigSchema,
|
|
13
|
+
WebexMembershipSchema,
|
|
14
|
+
WebexMessageSchema,
|
|
15
|
+
WebexPersonSchema,
|
|
16
|
+
WebexSpaceSchema,
|
|
17
|
+
} from './types'
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
WebexConfigSchema,
|
|
5
|
+
WebexError,
|
|
6
|
+
WebexMembershipSchema,
|
|
7
|
+
WebexMessageSchema,
|
|
8
|
+
WebexPersonSchema,
|
|
9
|
+
WebexSpaceSchema,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
test('WebexSpaceSchema validates valid space', () => {
|
|
13
|
+
const result = WebexSpaceSchema.safeParse({
|
|
14
|
+
id: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
15
|
+
title: 'Project Alpha',
|
|
16
|
+
type: 'group',
|
|
17
|
+
isLocked: false,
|
|
18
|
+
lastActivity: '2024-01-15T10:30:00.000Z',
|
|
19
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
20
|
+
creatorId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
21
|
+
})
|
|
22
|
+
expect(result.success).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('WebexSpaceSchema validates space with optional teamId', () => {
|
|
26
|
+
const result = WebexSpaceSchema.safeParse({
|
|
27
|
+
id: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
28
|
+
title: 'Project Alpha',
|
|
29
|
+
type: 'group',
|
|
30
|
+
isLocked: true,
|
|
31
|
+
teamId: 'Y2lzY29zcGFyazovL3VzL1RFQU0vdGVhbQ',
|
|
32
|
+
lastActivity: '2024-01-15T10:30:00.000Z',
|
|
33
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
34
|
+
creatorId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
35
|
+
})
|
|
36
|
+
expect(result.success).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('WebexSpaceSchema validates direct space type', () => {
|
|
40
|
+
const result = WebexSpaceSchema.safeParse({
|
|
41
|
+
id: 'Y2lzY29zcGFyazovL3VzL1JPT00vZGlyZWN0',
|
|
42
|
+
title: 'Direct Message',
|
|
43
|
+
type: 'direct',
|
|
44
|
+
isLocked: false,
|
|
45
|
+
lastActivity: '2024-01-15T10:30:00.000Z',
|
|
46
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
47
|
+
creatorId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
48
|
+
})
|
|
49
|
+
expect(result.success).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('WebexSpaceSchema rejects missing required fields', () => {
|
|
53
|
+
const result = WebexSpaceSchema.safeParse({
|
|
54
|
+
id: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
55
|
+
title: 'Project Alpha',
|
|
56
|
+
})
|
|
57
|
+
expect(result.success).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('WebexSpaceSchema rejects invalid type', () => {
|
|
61
|
+
const result = WebexSpaceSchema.safeParse({
|
|
62
|
+
id: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
63
|
+
title: 'Project Alpha',
|
|
64
|
+
type: 'channel',
|
|
65
|
+
isLocked: false,
|
|
66
|
+
lastActivity: '2024-01-15T10:30:00.000Z',
|
|
67
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
68
|
+
creatorId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
69
|
+
})
|
|
70
|
+
expect(result.success).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('WebexMessageSchema validates valid message', () => {
|
|
74
|
+
const result = WebexMessageSchema.safeParse({
|
|
75
|
+
id: 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvbXNn',
|
|
76
|
+
roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
77
|
+
roomType: 'group',
|
|
78
|
+
text: 'Hello world',
|
|
79
|
+
personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
80
|
+
personEmail: 'user@example.com',
|
|
81
|
+
created: '2024-01-15T10:30:00.000Z',
|
|
82
|
+
})
|
|
83
|
+
expect(result.success).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('WebexMessageSchema validates message with optional fields', () => {
|
|
87
|
+
const result = WebexMessageSchema.safeParse({
|
|
88
|
+
id: 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvbXNn',
|
|
89
|
+
roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
90
|
+
roomType: 'group',
|
|
91
|
+
text: 'Hello world',
|
|
92
|
+
markdown: '**Hello world**',
|
|
93
|
+
html: '<strong>Hello world</strong>',
|
|
94
|
+
files: ['https://webexapis.com/v1/contents/file1'],
|
|
95
|
+
personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
96
|
+
personEmail: 'user@example.com',
|
|
97
|
+
created: '2024-01-15T10:30:00.000Z',
|
|
98
|
+
parentId: 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvcGFyZW50',
|
|
99
|
+
mentionedPeople: ['Y2lzY29zcGFyazovL3VzL1BFT1BMRS9tZW50aW9u'],
|
|
100
|
+
})
|
|
101
|
+
expect(result.success).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('WebexMessageSchema rejects missing required fields', () => {
|
|
105
|
+
const result = WebexMessageSchema.safeParse({
|
|
106
|
+
id: 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvbXNn',
|
|
107
|
+
roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
108
|
+
text: 'Hello world',
|
|
109
|
+
})
|
|
110
|
+
expect(result.success).toBe(false)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('WebexMessageSchema rejects invalid roomType', () => {
|
|
114
|
+
const result = WebexMessageSchema.safeParse({
|
|
115
|
+
id: 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvbXNn',
|
|
116
|
+
roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
117
|
+
roomType: 'team',
|
|
118
|
+
personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
119
|
+
personEmail: 'user@example.com',
|
|
120
|
+
created: '2024-01-15T10:30:00.000Z',
|
|
121
|
+
})
|
|
122
|
+
expect(result.success).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('WebexPersonSchema validates valid person', () => {
|
|
126
|
+
const result = WebexPersonSchema.safeParse({
|
|
127
|
+
id: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
128
|
+
emails: ['user@example.com'],
|
|
129
|
+
displayName: 'Test User',
|
|
130
|
+
orgId: 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9vcmc',
|
|
131
|
+
type: 'person',
|
|
132
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
133
|
+
})
|
|
134
|
+
expect(result.success).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('WebexPersonSchema validates person with optional fields', () => {
|
|
138
|
+
const result = WebexPersonSchema.safeParse({
|
|
139
|
+
id: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
140
|
+
emails: ['user@example.com', 'user@work.com'],
|
|
141
|
+
displayName: 'Test User',
|
|
142
|
+
nickName: 'Tester',
|
|
143
|
+
firstName: 'Test',
|
|
144
|
+
lastName: 'User',
|
|
145
|
+
avatar: 'https://example.com/avatar.jpg',
|
|
146
|
+
orgId: 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9vcmc',
|
|
147
|
+
type: 'person',
|
|
148
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
149
|
+
})
|
|
150
|
+
expect(result.success).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('WebexPersonSchema validates bot type', () => {
|
|
154
|
+
const result = WebexPersonSchema.safeParse({
|
|
155
|
+
id: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9ib3Q',
|
|
156
|
+
emails: ['bot@webex.bot'],
|
|
157
|
+
displayName: 'My Bot',
|
|
158
|
+
orgId: 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9vcmc',
|
|
159
|
+
type: 'bot',
|
|
160
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
161
|
+
})
|
|
162
|
+
expect(result.success).toBe(true)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('WebexPersonSchema rejects missing required fields', () => {
|
|
166
|
+
const result = WebexPersonSchema.safeParse({
|
|
167
|
+
id: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
168
|
+
displayName: 'Test User',
|
|
169
|
+
})
|
|
170
|
+
expect(result.success).toBe(false)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('WebexPersonSchema rejects invalid type', () => {
|
|
174
|
+
const result = WebexPersonSchema.safeParse({
|
|
175
|
+
id: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
176
|
+
emails: ['user@example.com'],
|
|
177
|
+
displayName: 'Test User',
|
|
178
|
+
orgId: 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9vcmc',
|
|
179
|
+
type: 'admin',
|
|
180
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
181
|
+
})
|
|
182
|
+
expect(result.success).toBe(false)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('WebexMembershipSchema validates valid membership', () => {
|
|
186
|
+
const result = WebexMembershipSchema.safeParse({
|
|
187
|
+
id: 'Y2lzY29zcGFyazovL3VzL01FTUJFUlNISVAvbWVt',
|
|
188
|
+
roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
189
|
+
personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
190
|
+
personEmail: 'user@example.com',
|
|
191
|
+
personDisplayName: 'Test User',
|
|
192
|
+
isModerator: false,
|
|
193
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
194
|
+
})
|
|
195
|
+
expect(result.success).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('WebexMembershipSchema validates moderator membership', () => {
|
|
199
|
+
const result = WebexMembershipSchema.safeParse({
|
|
200
|
+
id: 'Y2lzY29zcGFyazovL3VzL01FTUJFUlNISVAvbWVt',
|
|
201
|
+
roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
202
|
+
personId: 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hYmM',
|
|
203
|
+
personEmail: 'moderator@example.com',
|
|
204
|
+
personDisplayName: 'Moderator User',
|
|
205
|
+
isModerator: true,
|
|
206
|
+
created: '2024-01-01T00:00:00.000Z',
|
|
207
|
+
})
|
|
208
|
+
expect(result.success).toBe(true)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('WebexMembershipSchema rejects missing required fields', () => {
|
|
212
|
+
const result = WebexMembershipSchema.safeParse({
|
|
213
|
+
id: 'Y2lzY29zcGFyazovL3VzL01FTUJFUlNISVAvbWVt',
|
|
214
|
+
roomId: 'Y2lzY29zcGFyazovL3VzL1JPT00vYWJj',
|
|
215
|
+
})
|
|
216
|
+
expect(result.success).toBe(false)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('WebexConfigSchema validates valid OAuth config', () => {
|
|
220
|
+
const result = WebexConfigSchema.safeParse({
|
|
221
|
+
accessToken: 'test',
|
|
222
|
+
refreshToken: 'test',
|
|
223
|
+
expiresAt: 1234567890,
|
|
224
|
+
})
|
|
225
|
+
expect(result.success).toBe(true)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('WebexConfigSchema validates config with clientId and clientSecret', () => {
|
|
229
|
+
const result = WebexConfigSchema.safeParse({
|
|
230
|
+
accessToken: 'test',
|
|
231
|
+
refreshToken: 'test',
|
|
232
|
+
expiresAt: 1234567890,
|
|
233
|
+
clientId: 'C123abc',
|
|
234
|
+
clientSecret: 'secret456',
|
|
235
|
+
})
|
|
236
|
+
expect(result.success).toBe(true)
|
|
237
|
+
if (result.success) {
|
|
238
|
+
expect(result.data.clientId).toBe('C123abc')
|
|
239
|
+
expect(result.data.clientSecret).toBe('secret456')
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('WebexConfigSchema validates config with tokenType oauth', () => {
|
|
244
|
+
const result = WebexConfigSchema.safeParse({
|
|
245
|
+
accessToken: 'test',
|
|
246
|
+
refreshToken: 'test',
|
|
247
|
+
expiresAt: 1234567890,
|
|
248
|
+
tokenType: 'oauth',
|
|
249
|
+
})
|
|
250
|
+
expect(result.success).toBe(true)
|
|
251
|
+
if (result.success) {
|
|
252
|
+
expect(result.data.tokenType).toBe('oauth')
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('WebexConfigSchema validates config with tokenType manual', () => {
|
|
257
|
+
const result = WebexConfigSchema.safeParse({
|
|
258
|
+
accessToken: 'test',
|
|
259
|
+
refreshToken: '',
|
|
260
|
+
expiresAt: 0,
|
|
261
|
+
tokenType: 'manual',
|
|
262
|
+
})
|
|
263
|
+
expect(result.success).toBe(true)
|
|
264
|
+
if (result.success) {
|
|
265
|
+
expect(result.data.tokenType).toBe('manual')
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('WebexConfigSchema rejects invalid tokenType', () => {
|
|
270
|
+
const result = WebexConfigSchema.safeParse({
|
|
271
|
+
accessToken: 'test',
|
|
272
|
+
refreshToken: 'test',
|
|
273
|
+
expiresAt: 1234567890,
|
|
274
|
+
tokenType: 'invalid',
|
|
275
|
+
})
|
|
276
|
+
expect(result.success).toBe(false)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('WebexConfigSchema accepts config without clientId/clientSecret (backward compat)', () => {
|
|
280
|
+
const result = WebexConfigSchema.safeParse({
|
|
281
|
+
accessToken: 'test',
|
|
282
|
+
refreshToken: 'test',
|
|
283
|
+
expiresAt: 1234567890,
|
|
284
|
+
})
|
|
285
|
+
expect(result.success).toBe(true)
|
|
286
|
+
if (result.success) {
|
|
287
|
+
expect(result.data.clientId).toBeUndefined()
|
|
288
|
+
expect(result.data.clientSecret).toBeUndefined()
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('WebexConfigSchema rejects missing fields', () => {
|
|
293
|
+
const result = WebexConfigSchema.safeParse({})
|
|
294
|
+
expect(result.success).toBe(false)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('WebexError has correct name and code', () => {
|
|
298
|
+
const error = new WebexError('Not found', 'NOT_FOUND')
|
|
299
|
+
expect(error.name).toBe('WebexError')
|
|
300
|
+
expect(error.message).toBe('Not found')
|
|
301
|
+
expect(error.code).toBe('NOT_FOUND')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('WebexError is instance of Error', () => {
|
|
305
|
+
const error = new WebexError('Unauthorized', 'UNAUTHORIZED')
|
|
306
|
+
expect(error instanceof Error).toBe(true)
|
|
307
|
+
})
|