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.
Files changed (119) hide show
  1. package/.claude-plugin/marketplace.json +14 -1
  2. package/.claude-plugin/plugin.json +4 -2
  3. package/README.md +33 -29
  4. package/dist/package.json +10 -2
  5. package/dist/src/cli.d.ts.map +1 -1
  6. package/dist/src/cli.js +3 -0
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/platforms/webex/app-config.d.ts +7 -0
  9. package/dist/src/platforms/webex/app-config.d.ts.map +1 -0
  10. package/dist/src/platforms/webex/app-config.js +20 -0
  11. package/dist/src/platforms/webex/app-config.js.map +1 -0
  12. package/dist/src/platforms/webex/cli.d.ts +5 -0
  13. package/dist/src/platforms/webex/cli.d.ts.map +1 -0
  14. package/dist/src/platforms/webex/cli.js +32 -0
  15. package/dist/src/platforms/webex/cli.js.map +1 -0
  16. package/dist/src/platforms/webex/client.d.ts +45 -0
  17. package/dist/src/platforms/webex/client.d.ts.map +1 -0
  18. package/dist/src/platforms/webex/client.js +175 -0
  19. package/dist/src/platforms/webex/client.js.map +1 -0
  20. package/dist/src/platforms/webex/commands/auth.d.ts +15 -0
  21. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -0
  22. package/dist/src/platforms/webex/commands/auth.js +124 -0
  23. package/dist/src/platforms/webex/commands/auth.js.map +1 -0
  24. package/dist/src/platforms/webex/commands/index.d.ts +6 -0
  25. package/dist/src/platforms/webex/commands/index.d.ts.map +1 -0
  26. package/dist/src/platforms/webex/commands/index.js +6 -0
  27. package/dist/src/platforms/webex/commands/index.js.map +1 -0
  28. package/dist/src/platforms/webex/commands/member.d.ts +7 -0
  29. package/dist/src/platforms/webex/commands/member.d.ts.map +1 -0
  30. package/dist/src/platforms/webex/commands/member.js +34 -0
  31. package/dist/src/platforms/webex/commands/member.js.map +1 -0
  32. package/dist/src/platforms/webex/commands/message.d.ts +26 -0
  33. package/dist/src/platforms/webex/commands/message.d.ts.map +1 -0
  34. package/dist/src/platforms/webex/commands/message.js +153 -0
  35. package/dist/src/platforms/webex/commands/message.js.map +1 -0
  36. package/dist/src/platforms/webex/commands/snapshot.d.ts +9 -0
  37. package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -0
  38. package/dist/src/platforms/webex/commands/snapshot.js +72 -0
  39. package/dist/src/platforms/webex/commands/snapshot.js.map +1 -0
  40. package/dist/src/platforms/webex/commands/space.d.ts +11 -0
  41. package/dist/src/platforms/webex/commands/space.d.ts.map +1 -0
  42. package/dist/src/platforms/webex/commands/space.js +59 -0
  43. package/dist/src/platforms/webex/commands/space.js.map +1 -0
  44. package/dist/src/platforms/webex/credential-manager.d.ts +23 -0
  45. package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -0
  46. package/dist/src/platforms/webex/credential-manager.js +148 -0
  47. package/dist/src/platforms/webex/credential-manager.js.map +1 -0
  48. package/dist/src/platforms/webex/ensure-auth.d.ts +2 -0
  49. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -0
  50. package/dist/src/platforms/webex/ensure-auth.js +20 -0
  51. package/dist/src/platforms/webex/ensure-auth.js.map +1 -0
  52. package/dist/src/platforms/webex/index.d.ts +6 -0
  53. package/dist/src/platforms/webex/index.d.ts.map +1 -0
  54. package/dist/src/platforms/webex/index.js +5 -0
  55. package/dist/src/platforms/webex/index.js.map +1 -0
  56. package/dist/src/platforms/webex/types.d.ts +124 -0
  57. package/dist/src/platforms/webex/types.d.ts.map +1 -0
  58. package/dist/src/platforms/webex/types.js +63 -0
  59. package/dist/src/platforms/webex/types.js.map +1 -0
  60. package/dist/src/tui/adapters/webex-adapter.d.ts +14 -0
  61. package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -0
  62. package/dist/src/tui/adapters/webex-adapter.js +79 -0
  63. package/dist/src/tui/adapters/webex-adapter.js.map +1 -0
  64. package/dist/src/tui/app.d.ts.map +1 -1
  65. package/dist/src/tui/app.js +2 -0
  66. package/dist/src/tui/app.js.map +1 -1
  67. package/docs/content/docs/cli/meta.json +1 -0
  68. package/docs/content/docs/cli/webex.mdx +291 -0
  69. package/docs/content/docs/sdk/meta.json +1 -1
  70. package/docs/content/docs/sdk/webex.mdx +260 -0
  71. package/docs/content/docs/tui.mdx +4 -3
  72. package/docs/src/app/page.tsx +2 -2
  73. package/package.json +10 -2
  74. package/skills/agent-channeltalk/SKILL.md +1 -1
  75. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  76. package/skills/agent-discord/SKILL.md +1 -1
  77. package/skills/agent-discordbot/SKILL.md +1 -1
  78. package/skills/agent-instagram/SKILL.md +1 -1
  79. package/skills/agent-kakaotalk/SKILL.md +1 -1
  80. package/skills/agent-line/SKILL.md +1 -1
  81. package/skills/agent-slack/SKILL.md +1 -1
  82. package/skills/agent-slackbot/SKILL.md +1 -1
  83. package/skills/agent-teams/SKILL.md +1 -1
  84. package/skills/agent-telegram/SKILL.md +1 -1
  85. package/skills/agent-webex/SKILL.md +386 -0
  86. package/skills/agent-webex/references/authentication.md +318 -0
  87. package/skills/agent-webex/references/common-patterns.md +723 -0
  88. package/skills/agent-webex/templates/monitor-space.sh +165 -0
  89. package/skills/agent-webex/templates/post-message.sh +170 -0
  90. package/skills/agent-whatsapp/SKILL.md +1 -1
  91. package/skills/agent-whatsappbot/SKILL.md +1 -1
  92. package/src/cli.ts +4 -0
  93. package/src/platforms/webex/app-config.test.ts +98 -0
  94. package/src/platforms/webex/app-config.ts +31 -0
  95. package/src/platforms/webex/cli.test.ts +58 -0
  96. package/src/platforms/webex/cli.ts +39 -0
  97. package/src/platforms/webex/client.test.ts +429 -0
  98. package/src/platforms/webex/client.ts +247 -0
  99. package/src/platforms/webex/commands/auth.test.ts +222 -0
  100. package/src/platforms/webex/commands/auth.ts +180 -0
  101. package/src/platforms/webex/commands/index.ts +5 -0
  102. package/src/platforms/webex/commands/member.test.ts +103 -0
  103. package/src/platforms/webex/commands/member.ts +45 -0
  104. package/src/platforms/webex/commands/message.test.ts +231 -0
  105. package/src/platforms/webex/commands/message.ts +204 -0
  106. package/src/platforms/webex/commands/snapshot.test.ts +96 -0
  107. package/src/platforms/webex/commands/snapshot.ts +91 -0
  108. package/src/platforms/webex/commands/space.test.ts +206 -0
  109. package/src/platforms/webex/commands/space.ts +74 -0
  110. package/src/platforms/webex/credential-manager.test.ts +314 -0
  111. package/src/platforms/webex/credential-manager.ts +197 -0
  112. package/src/platforms/webex/ensure-auth.test.ts +85 -0
  113. package/src/platforms/webex/ensure-auth.ts +19 -0
  114. package/src/platforms/webex/index.test.ts +25 -0
  115. package/src/platforms/webex/index.ts +17 -0
  116. package/src/platforms/webex/types.test.ts +307 -0
  117. package/src/platforms/webex/types.ts +127 -0
  118. package/src/tui/adapters/webex-adapter.ts +103 -0
  119. 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
+ })