agent-messenger 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/teams/client.d.ts +9 -1
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +69 -18
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +7 -2
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/channel.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/channel.js +18 -3
- package/dist/src/platforms/teams/commands/channel.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +18 -3
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +24 -4
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.js +12 -2
- package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
- package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/snapshot.js +6 -1
- package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/teams/commands/team.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/team.js +6 -1
- package/dist/src/platforms/teams/commands/team.js.map +1 -1
- package/dist/src/platforms/teams/commands/user.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/user.js +18 -3
- package/dist/src/platforms/teams/commands/user.js.map +1 -1
- package/dist/src/platforms/teams/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/whoami.js +6 -1
- package/dist/src/platforms/teams/commands/whoami.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts +3 -1
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +6 -1
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +7 -2
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/token-extractor.d.ts +3 -1
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +67 -10
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/types.d.ts +17 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +2 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +3 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +58 -13
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +61 -10
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/webex/credential-manager.js +18 -6
- package/dist/src/platforms/webex/credential-manager.js.map +1 -1
- package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
- package/dist/src/platforms/webex/encryption.js +3 -1
- package/dist/src/platforms/webex/encryption.js.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/ensure-auth.js +10 -2
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +1 -0
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/webex/token-extractor.js +21 -4
- package/dist/src/platforms/webex/token-extractor.js.map +1 -1
- package/e2e/webex.e2e.test.ts +57 -0
- package/package.json +1 -1
- 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 +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/teams/client.test.ts +34 -30
- package/src/platforms/teams/client.ts +92 -20
- package/src/platforms/teams/commands/auth.test.ts +6 -2
- package/src/platforms/teams/commands/auth.ts +7 -2
- package/src/platforms/teams/commands/channel.test.ts +6 -6
- package/src/platforms/teams/commands/channel.ts +18 -3
- package/src/platforms/teams/commands/file.ts +18 -3
- package/src/platforms/teams/commands/message.ts +24 -4
- package/src/platforms/teams/commands/reaction.ts +12 -2
- package/src/platforms/teams/commands/snapshot.ts +6 -1
- package/src/platforms/teams/commands/team.test.ts +2 -2
- package/src/platforms/teams/commands/team.ts +6 -1
- package/src/platforms/teams/commands/user.ts +18 -3
- package/src/platforms/teams/commands/whoami.ts +6 -1
- package/src/platforms/teams/credential-manager.test.ts +25 -0
- package/src/platforms/teams/credential-manager.ts +13 -3
- package/src/platforms/teams/ensure-auth.test.ts +6 -1
- package/src/platforms/teams/ensure-auth.ts +7 -2
- package/src/platforms/teams/token-extractor.ts +77 -12
- package/src/platforms/teams/types.test.ts +17 -0
- package/src/platforms/teams/types.ts +6 -0
- package/src/platforms/webex/client.test.ts +157 -13
- package/src/platforms/webex/client.ts +64 -15
- package/src/platforms/webex/commands/auth.test.ts +122 -1
- package/src/platforms/webex/commands/auth.ts +72 -17
- package/src/platforms/webex/credential-manager.test.ts +63 -0
- package/src/platforms/webex/credential-manager.ts +22 -8
- package/src/platforms/webex/encryption.test.ts +54 -0
- package/src/platforms/webex/encryption.ts +3 -1
- package/src/platforms/webex/ensure-auth.ts +10 -2
- package/src/platforms/webex/token-extractor.test.ts +32 -3
- package/src/platforms/webex/token-extractor.ts +26 -5
|
@@ -88,6 +88,31 @@ describe('TeamsCredentialManager', () => {
|
|
|
88
88
|
expect(team).toBeNull()
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
+
test('getTokenWithExpiry includes region', async () => {
|
|
92
|
+
const manager = setup()
|
|
93
|
+
await manager.saveConfig({
|
|
94
|
+
current_account: 'work',
|
|
95
|
+
accounts: {
|
|
96
|
+
work: {
|
|
97
|
+
token: 'test-token',
|
|
98
|
+
token_expires_at: '2025-12-31T23:59:59Z',
|
|
99
|
+
region: 'emea',
|
|
100
|
+
account_type: 'work',
|
|
101
|
+
current_team: null,
|
|
102
|
+
teams: {},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const token = await manager.getTokenWithExpiry()
|
|
108
|
+
expect(token).toEqual({
|
|
109
|
+
token: 'test-token',
|
|
110
|
+
tokenExpiresAt: '2025-12-31T23:59:59Z',
|
|
111
|
+
accountType: 'work',
|
|
112
|
+
region: 'emea',
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
91
116
|
test('getCurrentTeam returns null when current_team is set but team not in teams record', async () => {
|
|
92
117
|
const manager = setup()
|
|
93
118
|
await manager.setToken('test-token', 'work')
|
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import type { TeamsAccount, TeamsAccountType, TeamsConfig, TeamsConfigLegacy } from './types'
|
|
6
|
+
import type { TeamsAccount, TeamsAccountType, TeamsConfig, TeamsConfigLegacy, TeamsRegion } from './types'
|
|
7
7
|
|
|
8
8
|
export class TeamsCredentialManager {
|
|
9
9
|
static accountOverride?: TeamsAccountType
|
|
@@ -78,12 +78,22 @@ export class TeamsCredentialManager {
|
|
|
78
78
|
return this.resolveCurrentAccount(config)?.token ?? null
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async getTokenWithExpiry(): Promise<{
|
|
81
|
+
async getTokenWithExpiry(): Promise<{
|
|
82
|
+
token: string
|
|
83
|
+
tokenExpiresAt?: string
|
|
84
|
+
accountType?: TeamsAccountType
|
|
85
|
+
region?: TeamsRegion
|
|
86
|
+
} | null> {
|
|
82
87
|
const config = await this.loadConfig()
|
|
83
88
|
if (!config) return null
|
|
84
89
|
const account = this.resolveCurrentAccount(config)
|
|
85
90
|
if (!account?.token) return null
|
|
86
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
token: account.token,
|
|
93
|
+
tokenExpiresAt: account.token_expires_at,
|
|
94
|
+
accountType: account.account_type,
|
|
95
|
+
region: account.region,
|
|
96
|
+
}
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
async setToken(token: string, accountType: TeamsAccountType, expiresAt?: string): Promise<void> {
|
|
@@ -10,6 +10,7 @@ let extractSpy: ReturnType<typeof spyOn>
|
|
|
10
10
|
let testAuthSpy: ReturnType<typeof spyOn>
|
|
11
11
|
let listTeamsSpy: ReturnType<typeof spyOn>
|
|
12
12
|
let saveConfigSpy: ReturnType<typeof spyOn>
|
|
13
|
+
let getRegionSpy: ReturnType<typeof spyOn>
|
|
13
14
|
|
|
14
15
|
beforeEach(() => {
|
|
15
16
|
loadConfigSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
|
|
@@ -28,6 +29,8 @@ beforeEach(() => {
|
|
|
28
29
|
{ id: 'team-2', name: 'Team Two' },
|
|
29
30
|
])
|
|
30
31
|
|
|
32
|
+
getRegionSpy = spyOn(TeamsClient.prototype, 'getRegion').mockReturnValue('emea')
|
|
33
|
+
|
|
31
34
|
saveConfigSpy = spyOn(TeamsCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
|
|
32
35
|
})
|
|
33
36
|
|
|
@@ -37,6 +40,7 @@ afterEach(() => {
|
|
|
37
40
|
testAuthSpy?.mockRestore()
|
|
38
41
|
listTeamsSpy?.mockRestore()
|
|
39
42
|
saveConfigSpy?.mockRestore()
|
|
43
|
+
getRegionSpy?.mockRestore()
|
|
40
44
|
})
|
|
41
45
|
|
|
42
46
|
describe('ensureTeamsAuth', () => {
|
|
@@ -78,6 +82,7 @@ describe('ensureTeamsAuth', () => {
|
|
|
78
82
|
accounts: expect.objectContaining({
|
|
79
83
|
work: expect.objectContaining({
|
|
80
84
|
token: 'test-teams-token',
|
|
85
|
+
region: 'emea',
|
|
81
86
|
current_team: 'team-1',
|
|
82
87
|
teams: {
|
|
83
88
|
'team-1': { team_id: 'team-1', team_name: 'Team One' },
|
|
@@ -215,7 +220,7 @@ describe('ensureTeamsAuth', () => {
|
|
|
215
220
|
current_account: 'work',
|
|
216
221
|
accounts: expect.objectContaining({
|
|
217
222
|
work: expect.objectContaining({ token: 'work-token', current_team: 'team-w' }),
|
|
218
|
-
personal: expect.objectContaining({ token: 'personal-token', current_team: 'team-p' }),
|
|
223
|
+
personal: expect.objectContaining({ token: 'personal-token', region: 'emea', current_team: 'team-p' }),
|
|
219
224
|
}),
|
|
220
225
|
}),
|
|
221
226
|
)
|
|
@@ -23,11 +23,15 @@ export async function ensureTeamsAuth(): Promise<void> {
|
|
|
23
23
|
|
|
24
24
|
for (const { token, accountType } of extracted) {
|
|
25
25
|
try {
|
|
26
|
-
const client = await new TeamsClient().login({
|
|
26
|
+
const client = await new TeamsClient().login({
|
|
27
|
+
token,
|
|
28
|
+
accountType,
|
|
29
|
+
region: config?.accounts[accountType]?.region,
|
|
30
|
+
})
|
|
27
31
|
await client.testAuth()
|
|
28
32
|
|
|
29
33
|
const teams = await client.listTeams()
|
|
30
|
-
if (teams.length === 0) continue
|
|
34
|
+
if (accountType !== 'personal' && teams.length === 0) continue
|
|
31
35
|
|
|
32
36
|
const teamMap: Record<string, { team_id: string; team_name: string }> = {}
|
|
33
37
|
for (const team of teams) {
|
|
@@ -38,6 +42,7 @@ export async function ensureTeamsAuth(): Promise<void> {
|
|
|
38
42
|
const account: TeamsAccount = {
|
|
39
43
|
token,
|
|
40
44
|
token_expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
45
|
+
region: client.getRegion(),
|
|
41
46
|
account_type: accountType,
|
|
42
47
|
user_name: existing?.user_name,
|
|
43
48
|
current_team: existing?.current_team ?? teams[0].id,
|
|
@@ -55,9 +55,11 @@ export class TeamsTokenExtractor {
|
|
|
55
55
|
private platform: NodeJS.Platform
|
|
56
56
|
private decryptor: ChromiumCookieDecryptor
|
|
57
57
|
private cookieReader: ChromiumCookieReader
|
|
58
|
+
private debugLog: ((message: string) => void) | null
|
|
58
59
|
|
|
59
|
-
constructor(platform?: NodeJS.Platform, keyCache?: DerivedKeyCache) {
|
|
60
|
+
constructor(platform?: NodeJS.Platform, keyCache?: DerivedKeyCache, debugLog?: (message: string) => void) {
|
|
60
61
|
this.platform = platform ?? process.platform
|
|
62
|
+
this.debugLog = debugLog ?? null
|
|
61
63
|
|
|
62
64
|
const resolvedKeyCache = keyCache ?? new DerivedKeyCache()
|
|
63
65
|
this.decryptor = new ChromiumCookieDecryptor({
|
|
@@ -69,6 +71,10 @@ export class TeamsTokenExtractor {
|
|
|
69
71
|
this.cookieReader = new ChromiumCookieReader()
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
private debug(message: string): void {
|
|
75
|
+
this.debugLog?.(message)
|
|
76
|
+
}
|
|
77
|
+
|
|
72
78
|
getDesktopCookiesPaths(): TeamsCookiePath[] {
|
|
73
79
|
switch (this.platform) {
|
|
74
80
|
case 'darwin': {
|
|
@@ -197,17 +203,38 @@ export class TeamsTokenExtractor {
|
|
|
197
203
|
private async extractFromCookiesDB(): Promise<ExtractedTeamsToken[]> {
|
|
198
204
|
const results: ExtractedTeamsToken[] = []
|
|
199
205
|
const seenAccountTypes = new Set<TeamsAccountType>()
|
|
206
|
+
const allPaths = this.getTeamsCookiesPaths()
|
|
207
|
+
|
|
208
|
+
this.debug(`Scanning ${allPaths.length} candidate cookie path(s)`)
|
|
209
|
+
|
|
210
|
+
for (const { path: dbPath, accountType } of allPaths) {
|
|
211
|
+
if (!dbPath) continue
|
|
212
|
+
|
|
213
|
+
if (!existsSync(dbPath)) {
|
|
214
|
+
this.debug(` [skip] ${dbPath} (not found)`)
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (seenAccountTypes.has(accountType)) {
|
|
219
|
+
this.debug(` [skip] ${dbPath} (already have ${accountType} account)`)
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
200
222
|
|
|
201
|
-
|
|
202
|
-
if (!dbPath || !existsSync(dbPath) || seenAccountTypes.has(accountType)) continue
|
|
223
|
+
this.debug(` [try] ${dbPath} (${accountType})`)
|
|
203
224
|
|
|
204
225
|
const token = await this.copyAndExtract(dbPath)
|
|
205
226
|
if (token && this.isValidSkypeToken(token)) {
|
|
227
|
+
this.debug(` [ok] Extracted valid token (${token.length} chars)`)
|
|
206
228
|
results.push({ token, accountType })
|
|
207
229
|
seenAccountTypes.add(accountType)
|
|
230
|
+
} else if (token) {
|
|
231
|
+
this.debug(` [fail] Token too short (${token.length} chars, need >=50)`)
|
|
232
|
+
} else {
|
|
233
|
+
this.debug(` [fail] No token extracted`)
|
|
208
234
|
}
|
|
209
235
|
}
|
|
210
236
|
|
|
237
|
+
this.debug(`Extraction complete: ${results.length} token(s) found`)
|
|
211
238
|
return results
|
|
212
239
|
}
|
|
213
240
|
|
|
@@ -216,11 +243,26 @@ export class TeamsTokenExtractor {
|
|
|
216
243
|
|
|
217
244
|
try {
|
|
218
245
|
tempPath = this.copyDatabaseToTemp(dbPath, dbPath)
|
|
219
|
-
|
|
220
|
-
|
|
246
|
+
|
|
247
|
+
let localStatePath: string | undefined
|
|
248
|
+
if (this.platform === 'win32') {
|
|
249
|
+
localStatePath = findLocalStatePath(dbPath) ?? undefined
|
|
250
|
+
if (localStatePath) {
|
|
251
|
+
this.debug(` Local State (from cookie path): ${localStatePath}`)
|
|
252
|
+
} else {
|
|
253
|
+
localStatePath = this.getLocalStatePath()
|
|
254
|
+
if (existsSync(localStatePath)) {
|
|
255
|
+
this.debug(` Local State (fallback): ${localStatePath}`)
|
|
256
|
+
} else {
|
|
257
|
+
this.debug(` Local State not found (tried fallback: ${localStatePath})`)
|
|
258
|
+
localStatePath = undefined
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
221
262
|
|
|
222
263
|
return await this.extractFromSQLite(tempPath, localStatePath)
|
|
223
|
-
} catch {
|
|
264
|
+
} catch (error) {
|
|
265
|
+
this.debug(` Copy/extract error: ${(error as Error).message}`)
|
|
224
266
|
return null
|
|
225
267
|
} finally {
|
|
226
268
|
this.cleanupTempFile(tempPath)
|
|
@@ -231,27 +273,50 @@ export class TeamsTokenExtractor {
|
|
|
231
273
|
try {
|
|
232
274
|
for (const hostPattern of TEAMS_HOST_PATTERNS) {
|
|
233
275
|
const sql = `
|
|
234
|
-
SELECT encrypted_value
|
|
276
|
+
SELECT value, encrypted_value
|
|
235
277
|
FROM cookies
|
|
236
278
|
WHERE name = '${SKYPETOKEN_COOKIE_NAME}'
|
|
237
279
|
AND host_key LIKE '%${hostPattern}%'
|
|
238
280
|
LIMIT 1
|
|
239
281
|
`
|
|
240
282
|
|
|
241
|
-
type CookieRow = { encrypted_value?: Uint8Array | Buffer } | null
|
|
283
|
+
type CookieRow = { value?: string; encrypted_value?: Uint8Array | Buffer } | null
|
|
242
284
|
|
|
243
285
|
const row = await this.cookieReader.queryFirst<CookieRow>(dbPath, sql)
|
|
244
|
-
if (!row
|
|
286
|
+
if (!row) continue
|
|
287
|
+
|
|
288
|
+
if (row.value && row.value.length >= 50) {
|
|
289
|
+
this.debug(` Found plaintext cookie for ${hostPattern} (${row.value.length} chars)`)
|
|
290
|
+
return row.value
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!row.encrypted_value || row.encrypted_value.length === 0) {
|
|
294
|
+
this.debug(` No cookie data for ${hostPattern}`)
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const encBuf = Buffer.from(row.encrypted_value)
|
|
299
|
+
const isEncrypted = this.isEncryptedValue(encBuf)
|
|
300
|
+
this.debug(
|
|
301
|
+
` Found cookie for ${hostPattern}: ${encBuf.length} bytes, encrypted=${isEncrypted}, prefix=${encBuf.subarray(0, 3).toString('utf8')}`,
|
|
302
|
+
)
|
|
245
303
|
|
|
246
|
-
const decryptedBuf = this.decryptor.decryptCookieRaw(
|
|
247
|
-
if (!decryptedBuf)
|
|
304
|
+
const decryptedBuf = this.decryptor.decryptCookieRaw(encBuf, localStatePath)
|
|
305
|
+
if (!decryptedBuf) {
|
|
306
|
+
this.debug(` Decryption failed`)
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
248
309
|
|
|
310
|
+
this.debug(` Decrypted: ${decryptedBuf.length} bytes`)
|
|
249
311
|
const token = this.postProcessDecrypted(decryptedBuf)
|
|
250
312
|
if (this.isValidSkypeToken(token)) return token
|
|
313
|
+
|
|
314
|
+
this.debug(` Post-process result not a valid token (${token.length} chars)`)
|
|
251
315
|
}
|
|
252
316
|
|
|
253
317
|
return null
|
|
254
|
-
} catch {
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.debug(` SQLite query error: ${(error as Error).message}`)
|
|
255
320
|
return null
|
|
256
321
|
}
|
|
257
322
|
}
|
|
@@ -203,6 +203,7 @@ test('TeamsConfigSchema validates config with multiple accounts', () => {
|
|
|
203
203
|
work: {
|
|
204
204
|
token: 'work_token',
|
|
205
205
|
token_expires_at: '2024-01-01T00:00:00.000Z',
|
|
206
|
+
region: 'emea',
|
|
206
207
|
account_type: 'work',
|
|
207
208
|
user_name: 'Work User',
|
|
208
209
|
current_team: '19:abc123@thread.tacv2',
|
|
@@ -224,6 +225,22 @@ test('TeamsConfigSchema validates config with multiple accounts', () => {
|
|
|
224
225
|
expect(result.success).toBe(true)
|
|
225
226
|
})
|
|
226
227
|
|
|
228
|
+
test('TeamsConfigSchema rejects invalid region', () => {
|
|
229
|
+
const result = TeamsConfigSchema.safeParse({
|
|
230
|
+
current_account: 'work',
|
|
231
|
+
accounts: {
|
|
232
|
+
work: {
|
|
233
|
+
token: 'token_value',
|
|
234
|
+
region: 'invalid',
|
|
235
|
+
account_type: 'work',
|
|
236
|
+
current_team: null,
|
|
237
|
+
teams: {},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
expect(result.success).toBe(false)
|
|
242
|
+
})
|
|
243
|
+
|
|
227
244
|
test('TeamsConfigSchema rejects missing required fields', () => {
|
|
228
245
|
const result = TeamsConfigSchema.safeParse({
|
|
229
246
|
current_account: null,
|
|
@@ -55,9 +55,12 @@ export interface TeamsCredentials {
|
|
|
55
55
|
|
|
56
56
|
export type TeamsAccountType = 'work' | 'personal'
|
|
57
57
|
|
|
58
|
+
export type TeamsRegion = 'amer' | 'emea' | 'apac'
|
|
59
|
+
|
|
58
60
|
export interface TeamsAccount {
|
|
59
61
|
token: string
|
|
60
62
|
token_expires_at?: string
|
|
63
|
+
region?: TeamsRegion
|
|
61
64
|
account_type: TeamsAccountType
|
|
62
65
|
user_name?: string
|
|
63
66
|
current_team: string | null
|
|
@@ -141,9 +144,12 @@ export const TeamsCredentialsSchema = z.object({
|
|
|
141
144
|
|
|
142
145
|
export const TeamsAccountTypeSchema = z.enum(['work', 'personal'])
|
|
143
146
|
|
|
147
|
+
export const TeamsRegionSchema = z.enum(['amer', 'emea', 'apac'])
|
|
148
|
+
|
|
144
149
|
export const TeamsAccountSchema = z.object({
|
|
145
150
|
token: z.string(),
|
|
146
151
|
token_expires_at: z.string().optional(),
|
|
152
|
+
region: TeamsRegionSchema.optional(),
|
|
147
153
|
account_type: TeamsAccountTypeSchema,
|
|
148
154
|
user_name: z.string().optional(),
|
|
149
155
|
current_team: z.string().nullable(),
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
+
import * as jose from 'node-jose'
|
|
4
|
+
|
|
3
5
|
import { WebexClient } from './client'
|
|
6
|
+
import { WebexEncryptionService } from './encryption'
|
|
4
7
|
import { WebexError } from './types'
|
|
5
8
|
|
|
6
9
|
describe('WebexClient', () => {
|
|
@@ -56,6 +59,17 @@ describe('WebexClient', () => {
|
|
|
56
59
|
await expect(new WebexClient().login({ token: '' })).rejects.toThrow(WebexError)
|
|
57
60
|
await expect(new WebexClient().login({ token: '' })).rejects.toThrow('Token is required')
|
|
58
61
|
})
|
|
62
|
+
|
|
63
|
+
test('accepts deviceUrl and tokenType', async () => {
|
|
64
|
+
const client = await new WebexClient().login({
|
|
65
|
+
token: 'test-token',
|
|
66
|
+
deviceUrl: 'https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1',
|
|
67
|
+
tokenType: 'extracted',
|
|
68
|
+
})
|
|
69
|
+
expect(client).toBeInstanceOf(WebexClient)
|
|
70
|
+
expect((client as any).deviceUrl).toBe('https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1')
|
|
71
|
+
expect((client as any).tokenType).toBe('extracted')
|
|
72
|
+
})
|
|
59
73
|
})
|
|
60
74
|
|
|
61
75
|
describe('testAuth', () => {
|
|
@@ -84,6 +98,59 @@ describe('WebexClient', () => {
|
|
|
84
98
|
const client = await new WebexClient().login({ token: 'bad-token' })
|
|
85
99
|
await expect(client.testAuth()).rejects.toThrow(WebexError)
|
|
86
100
|
})
|
|
101
|
+
|
|
102
|
+
test('falls back to internal API when public API fails for extracted tokens', async () => {
|
|
103
|
+
// given - public API rejects, internal API succeeds
|
|
104
|
+
mockResponse({ message: 'Unauthorized' }, 401)
|
|
105
|
+
fetchResponses.push(
|
|
106
|
+
new Response(JSON.stringify({ id: 'conv-1', activities: { items: [] } }), {
|
|
107
|
+
status: 200,
|
|
108
|
+
headers: { 'Content-Type': 'application/json' },
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const client = await new WebexClient().login({
|
|
113
|
+
token: 'extracted-token',
|
|
114
|
+
deviceUrl: 'https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1',
|
|
115
|
+
tokenType: 'extracted',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// when
|
|
119
|
+
const person = await client.testAuth()
|
|
120
|
+
|
|
121
|
+
// then - succeeds via internal API
|
|
122
|
+
expect(fetchCalls.length).toBe(2)
|
|
123
|
+
expect(fetchCalls[0].url).toBe('https://webexapis.com/v1/people/me')
|
|
124
|
+
expect(fetchCalls[1].url).toContain('conv-r.wbx2.com/conversation/api/v1/conversations')
|
|
125
|
+
expect(person).toBeTruthy()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('throws when both public and internal APIs fail for extracted tokens', async () => {
|
|
129
|
+
// given - both APIs reject
|
|
130
|
+
mockResponse({ message: 'Unauthorized' }, 401)
|
|
131
|
+
fetchResponses.push(
|
|
132
|
+
new Response(JSON.stringify({ message: 'Unauthorized' }), {
|
|
133
|
+
status: 401,
|
|
134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const client = await new WebexClient().login({
|
|
139
|
+
token: 'bad-extracted-token',
|
|
140
|
+
deviceUrl: 'https://wdm-r.wbx2.com/wdm/api/v1/devices/dev-1',
|
|
141
|
+
tokenType: 'extracted',
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await expect(client.testAuth()).rejects.toThrow(WebexError)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('does not use internal API fallback for non-extracted tokens', async () => {
|
|
148
|
+
mockResponse({ message: 'Unauthorized' }, 401)
|
|
149
|
+
|
|
150
|
+
const client = await new WebexClient().login({ token: 'bad-token' })
|
|
151
|
+
await expect(client.testAuth()).rejects.toThrow(WebexError)
|
|
152
|
+
expect(fetchCalls.length).toBe(1)
|
|
153
|
+
})
|
|
87
154
|
})
|
|
88
155
|
|
|
89
156
|
describe('listSpaces', () => {
|
|
@@ -402,10 +469,11 @@ describe('WebexClient', () => {
|
|
|
402
469
|
})
|
|
403
470
|
|
|
404
471
|
const createExtractedClient = async () => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
472
|
+
return new WebexClient().login({
|
|
473
|
+
token: 'extracted-token',
|
|
474
|
+
deviceUrl: TEST_DEVICE_URL,
|
|
475
|
+
tokenType: 'extracted',
|
|
476
|
+
})
|
|
409
477
|
}
|
|
410
478
|
|
|
411
479
|
describe('sendMessage', () => {
|
|
@@ -419,7 +487,7 @@ describe('WebexClient', () => {
|
|
|
419
487
|
expect(fetchCalls[0].options?.method).toBe('POST')
|
|
420
488
|
})
|
|
421
489
|
|
|
422
|
-
test('body has verb, object type, displayName
|
|
490
|
+
test('body has verb, object type, and displayName (no content for plain text)', async () => {
|
|
423
491
|
mockResponse(mockActivity('Hello world'))
|
|
424
492
|
|
|
425
493
|
const client = await createExtractedClient()
|
|
@@ -429,7 +497,7 @@ describe('WebexClient', () => {
|
|
|
429
497
|
expect(body.verb).toBe('post')
|
|
430
498
|
expect(body.object.objectType).toBe('comment')
|
|
431
499
|
expect(body.object.displayName).toBe('Hello world')
|
|
432
|
-
expect(body.object.content).
|
|
500
|
+
expect(body.object.content).toBeUndefined()
|
|
433
501
|
})
|
|
434
502
|
|
|
435
503
|
test('body has target with decoded conv UUID and conversation type', async () => {
|
|
@@ -488,7 +556,7 @@ describe('WebexClient', () => {
|
|
|
488
556
|
expect(body.object.markdown).toBeUndefined()
|
|
489
557
|
})
|
|
490
558
|
|
|
491
|
-
test('
|
|
559
|
+
test('plain text messages omit content field', async () => {
|
|
492
560
|
mockResponse(mockActivity('Hello world'))
|
|
493
561
|
|
|
494
562
|
const client = await createExtractedClient()
|
|
@@ -496,7 +564,7 @@ describe('WebexClient', () => {
|
|
|
496
564
|
|
|
497
565
|
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
498
566
|
expect(body.object.displayName).toBe('Hello world')
|
|
499
|
-
expect(body.object.content).
|
|
567
|
+
expect(body.object.content).toBeUndefined()
|
|
500
568
|
})
|
|
501
569
|
})
|
|
502
570
|
|
|
@@ -620,8 +688,11 @@ describe('WebexClient', () => {
|
|
|
620
688
|
})
|
|
621
689
|
|
|
622
690
|
describe('editMessage', () => {
|
|
691
|
+
const mockEditActivity = (text: string, parentId = 'activity-123') =>
|
|
692
|
+
mockActivity(text, { parent: { id: parentId, type: 'edit' } })
|
|
693
|
+
|
|
623
694
|
test('posts activity with verb post and parent edit reference', async () => {
|
|
624
|
-
mockResponse(
|
|
695
|
+
mockResponse(mockEditActivity('Edited text'))
|
|
625
696
|
|
|
626
697
|
const client = await createExtractedClient()
|
|
627
698
|
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
@@ -631,8 +702,8 @@ describe('WebexClient', () => {
|
|
|
631
702
|
expect(body.parent).toEqual({ id: 'activity-123', type: 'edit' })
|
|
632
703
|
})
|
|
633
704
|
|
|
634
|
-
test('
|
|
635
|
-
mockResponse(
|
|
705
|
+
test('plain text edit populates both displayName and content to avoid auto-tombstone', async () => {
|
|
706
|
+
mockResponse(mockEditActivity('Edited text'))
|
|
636
707
|
|
|
637
708
|
const client = await createExtractedClient()
|
|
638
709
|
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
@@ -643,8 +714,18 @@ describe('WebexClient', () => {
|
|
|
643
714
|
expect(body.object.content).toBe('Edited text')
|
|
644
715
|
})
|
|
645
716
|
|
|
717
|
+
test('clientTempId uses -edit suffix to match Webex web client format', async () => {
|
|
718
|
+
mockResponse(mockEditActivity('Edited text'))
|
|
719
|
+
|
|
720
|
+
const client = await createExtractedClient()
|
|
721
|
+
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
722
|
+
|
|
723
|
+
const body = JSON.parse(fetchCalls[0].options?.body as string)
|
|
724
|
+
expect(body.clientTempId).toMatch(/^tmp-\d+-edit$/)
|
|
725
|
+
})
|
|
726
|
+
|
|
646
727
|
test('target has decoded conv UUID', async () => {
|
|
647
|
-
mockResponse(
|
|
728
|
+
mockResponse(mockEditActivity('Edited text'))
|
|
648
729
|
|
|
649
730
|
const client = await createExtractedClient()
|
|
650
731
|
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
@@ -654,7 +735,7 @@ describe('WebexClient', () => {
|
|
|
654
735
|
})
|
|
655
736
|
|
|
656
737
|
test('markdown option converts content to HTML and strips displayName', async () => {
|
|
657
|
-
mockResponse(
|
|
738
|
+
mockResponse(mockEditActivity('italic text'))
|
|
658
739
|
|
|
659
740
|
const client = await createExtractedClient()
|
|
660
741
|
await client.editMessage('activity-123', TEST_ROOM_ID, '_italic text_', { markdown: true })
|
|
@@ -664,6 +745,69 @@ describe('WebexClient', () => {
|
|
|
664
745
|
expect(body.object.content).toBe('<em>italic text</em>')
|
|
665
746
|
expect(body.object.markdown).toBeUndefined()
|
|
666
747
|
})
|
|
748
|
+
|
|
749
|
+
test('tolerates responses that omit parent (minimal success shape)', async () => {
|
|
750
|
+
mockResponse(mockActivity('Edited text'))
|
|
751
|
+
|
|
752
|
+
const client = await createExtractedClient()
|
|
753
|
+
const message = await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
754
|
+
expect(message.id).toBe('activity-123')
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
test('throws when server returns activity linked to a different parent', async () => {
|
|
758
|
+
mockResponse(mockEditActivity('Edited text', 'activity-999'))
|
|
759
|
+
|
|
760
|
+
const client = await createExtractedClient()
|
|
761
|
+
await expect(client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')).rejects.toThrow(/Edit rejected/)
|
|
762
|
+
})
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
describe('encrypted send and edit', () => {
|
|
766
|
+
const TEST_KEY_URI = 'kms://kms-aore.wbx2.com/keys/test-key-id'
|
|
767
|
+
|
|
768
|
+
const decodeJweHeader = (jwe: string): Record<string, unknown> => {
|
|
769
|
+
const [header = ''] = jwe.split('.')
|
|
770
|
+
const padded = header + '='.repeat((4 - (header.length % 4)) % 4)
|
|
771
|
+
return JSON.parse(Buffer.from(padded, 'base64url').toString('utf8')) as Record<string, unknown>
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const createEncryptedClient = async () => {
|
|
775
|
+
const keystore = jose.JWK.createKeyStore()
|
|
776
|
+
const key = await keystore.generate('oct', 256, { alg: 'A256GCM' })
|
|
777
|
+
const rawKeys = new Map<string, string>([[TEST_KEY_URI, JSON.stringify({ jwk: key.toJSON(true) })]])
|
|
778
|
+
const service = new WebexEncryptionService(rawKeys)
|
|
779
|
+
const client = await createExtractedClient()
|
|
780
|
+
;(client as unknown as { encryption: WebexEncryptionService }).encryption = service
|
|
781
|
+
return client
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
test('plain text send omits content field on encrypted path (preserves prior fix)', async () => {
|
|
785
|
+
mockResponse({ id: TEST_CONV_UUID, defaultActivityEncryptionKeyUrl: TEST_KEY_URI })
|
|
786
|
+
mockResponse(mockActivity('Hello world'))
|
|
787
|
+
|
|
788
|
+
const client = await createEncryptedClient()
|
|
789
|
+
await client.sendMessage(TEST_ROOM_ID, 'Hello world')
|
|
790
|
+
|
|
791
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
792
|
+
expect(body.object.content).toBeUndefined()
|
|
793
|
+
expect(body.object.displayName.startsWith('eyJ')).toBe(true)
|
|
794
|
+
expect(body.encryptionKeyUrl).toBe(TEST_KEY_URI)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
test('plain text edit encrypts both displayName and content with kid in JWE header', async () => {
|
|
798
|
+
mockResponse({ id: TEST_CONV_UUID, defaultActivityEncryptionKeyUrl: TEST_KEY_URI })
|
|
799
|
+
mockResponse(mockActivity('Edited text', { parent: { id: 'activity-123', type: 'edit' } }))
|
|
800
|
+
|
|
801
|
+
const client = await createEncryptedClient()
|
|
802
|
+
await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
|
|
803
|
+
|
|
804
|
+
const body = JSON.parse(fetchCalls[1].options?.body as string)
|
|
805
|
+
expect(body.object.displayName.startsWith('eyJ')).toBe(true)
|
|
806
|
+
expect(body.object.content.startsWith('eyJ')).toBe(true)
|
|
807
|
+
expect(body.encryptionKeyUrl).toBe(TEST_KEY_URI)
|
|
808
|
+
expect(decodeJweHeader(body.object.displayName).kid).toBe(TEST_KEY_URI)
|
|
809
|
+
expect(decodeJweHeader(body.object.content).kid).toBe(TEST_KEY_URI)
|
|
810
|
+
})
|
|
667
811
|
})
|
|
668
812
|
|
|
669
813
|
describe('sendDirectMessage', () => {
|