agent-messenger 2.28.0 → 2.29.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/README.md +18 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/client.d.ts +6 -2
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +10 -3
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/instagram/client.d.ts +24 -2
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +343 -53
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +123 -11
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.js +17 -1
- package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
- package/dist/src/platforms/instagram/types.d.ts +10 -8
- package/dist/src/platforms/instagram/types.d.ts.map +1 -1
- package/dist/src/platforms/instagram/types.js.map +1 -1
- package/dist/src/platforms/teams/app-config.d.ts +36 -0
- package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
- package/dist/src/platforms/teams/app-config.js +69 -0
- package/dist/src/platforms/teams/app-config.js.map +1 -0
- package/dist/src/platforms/teams/client.d.ts +19 -2
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +295 -7
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +137 -3
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.d.ts +3 -0
- package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +59 -2
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/message.d.ts +11 -0
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +121 -2
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +46 -2
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/device-code.d.ts +48 -0
- package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-code.js +199 -0
- package/dist/src/platforms/teams/device-code.js.map +1 -0
- package/dist/src/platforms/teams/device-login.d.ts +37 -0
- package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-login.js +129 -0
- package/dist/src/platforms/teams/device-login.js.map +1 -0
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +12 -0
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/index.d.ts +7 -3
- package/dist/src/platforms/teams/index.d.ts.map +1 -1
- package/dist/src/platforms/teams/index.js +5 -2
- package/dist/src/platforms/teams/index.js.map +1 -1
- package/dist/src/platforms/teams/listener.d.ts +35 -0
- package/dist/src/platforms/teams/listener.d.ts.map +1 -0
- package/dist/src/platforms/teams/listener.js +236 -0
- package/dist/src/platforms/teams/listener.js.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.js +33 -0
- package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
- package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +60 -0
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/token-provider.d.ts +25 -0
- package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
- package/dist/src/platforms/teams/token-provider.js +190 -0
- package/dist/src/platforms/teams/token-provider.js.map +1 -0
- package/dist/src/platforms/teams/trouter.d.ts +31 -0
- package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
- package/dist/src/platforms/teams/trouter.js +181 -0
- package/dist/src/platforms/teams/trouter.js.map +1 -0
- package/dist/src/platforms/teams/types.d.ts +94 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +30 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +1 -1
- package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/instagram-adapter.js +16 -0
- package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
- package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.js +21 -0
- package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
- package/dist/src/tui/app.js +3 -3
- package/docs/content/docs/cli/instagram.mdx +1 -0
- package/docs/content/docs/cli/teams.mdx +22 -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-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-instagram/references/authentication.md +11 -0
- 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 +59 -6
- package/skills/agent-teams/references/authentication.md +31 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/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/discord/client.test.ts +86 -0
- package/src/platforms/discord/client.ts +10 -3
- package/src/platforms/instagram/client.test.ts +478 -33
- package/src/platforms/instagram/client.ts +449 -68
- package/src/platforms/instagram/commands/auth.test.ts +177 -1
- package/src/platforms/instagram/commands/auth.ts +182 -11
- package/src/platforms/instagram/credential-manager.test.ts +30 -0
- package/src/platforms/instagram/credential-manager.ts +23 -1
- package/src/platforms/instagram/types.ts +11 -8
- package/src/platforms/teams/app-config.ts +92 -0
- package/src/platforms/teams/client.test.ts +335 -1
- package/src/platforms/teams/client.ts +342 -7
- package/src/platforms/teams/commands/auth.test.ts +93 -1
- package/src/platforms/teams/commands/auth.ts +182 -3
- package/src/platforms/teams/commands/file.test.ts +92 -1
- package/src/platforms/teams/commands/file.ts +76 -1
- package/src/platforms/teams/commands/message.test.ts +95 -1
- package/src/platforms/teams/commands/message.ts +145 -3
- package/src/platforms/teams/credential-manager.test.ts +20 -0
- package/src/platforms/teams/credential-manager.ts +68 -3
- package/src/platforms/teams/device-code.test.ts +311 -0
- package/src/platforms/teams/device-code.ts +277 -0
- package/src/platforms/teams/device-login.test.ts +288 -0
- package/src/platforms/teams/device-login.ts +206 -0
- package/src/platforms/teams/ensure-auth.ts +11 -0
- package/src/platforms/teams/index.test.ts +10 -0
- package/src/platforms/teams/index.ts +16 -1
- package/src/platforms/teams/listener.ts +295 -0
- package/src/platforms/teams/realm-discovery.test.ts +73 -0
- package/src/platforms/teams/realm-discovery.ts +42 -0
- package/src/platforms/teams/token-extractor.ts +69 -0
- package/src/platforms/teams/token-provider.test.ts +227 -0
- package/src/platforms/teams/token-provider.ts +284 -0
- package/src/platforms/teams/trouter.test.ts +114 -0
- package/src/platforms/teams/trouter.ts +228 -0
- package/src/platforms/teams/types.test.ts +17 -0
- package/src/platforms/teams/types.ts +86 -0
- package/src/tui/adapters/instagram-adapter.ts +13 -0
- package/src/tui/adapters/teams-adapter.ts +23 -0
- package/src/tui/app.ts +3 -3
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
2
|
-
import { unlinkSync } from 'node:fs'
|
|
2
|
+
import { rmSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
3
4
|
|
|
4
5
|
import { TeamsClient } from './client'
|
|
6
|
+
import { TeamsCredentialManager } from './credential-manager'
|
|
5
7
|
import { TeamsError } from './types'
|
|
6
8
|
|
|
7
9
|
const TEMP_FILES_TO_CLEANUP = ['/tmp/test-teams-upload.txt']
|
|
10
|
+
const TEMP_DIRS_TO_CLEANUP: string[] = []
|
|
11
|
+
const SEARCH_TENANT_ID = '11111111-1111-1111-1111-111111111111'
|
|
12
|
+
const SEARCH_USER_ID = '22222222-2222-2222-2222-222222222222'
|
|
13
|
+
const GRAPH_AUDIENCE = 'https://graph.microsoft.com'
|
|
8
14
|
|
|
9
15
|
describe('TeamsClient', () => {
|
|
10
16
|
const originalFetch = globalThis.fetch
|
|
@@ -36,8 +42,63 @@ describe('TeamsClient', () => {
|
|
|
36
42
|
// File may not exist, ignore
|
|
37
43
|
}
|
|
38
44
|
}
|
|
45
|
+
for (const dir of TEMP_DIRS_TO_CLEANUP.splice(0)) {
|
|
46
|
+
rmSync(dir, { recursive: true, force: true })
|
|
47
|
+
}
|
|
39
48
|
})
|
|
40
49
|
|
|
50
|
+
const setupCredentialManager = async (): Promise<TeamsCredentialManager> => {
|
|
51
|
+
const dir = join(import.meta.dir, `.test-teams-client-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
52
|
+
TEMP_DIRS_TO_CLEANUP.push(dir)
|
|
53
|
+
const manager = new TeamsCredentialManager(dir)
|
|
54
|
+
await manager.setDeviceCodeAccount({
|
|
55
|
+
accountType: 'work',
|
|
56
|
+
token: 'skype-token',
|
|
57
|
+
tokenExpiresAt: '2100-01-01T00:00:00Z',
|
|
58
|
+
aadRefreshToken: 'refresh-token',
|
|
59
|
+
aadClientId: 'client-id',
|
|
60
|
+
teams: {},
|
|
61
|
+
currentTeam: null,
|
|
62
|
+
})
|
|
63
|
+
return manager
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const createSearchJwt = (): string => {
|
|
67
|
+
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url')
|
|
68
|
+
const payload = Buffer.from(
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
aud: 'https://substrate.office.com',
|
|
71
|
+
tid: SEARCH_TENANT_ID,
|
|
72
|
+
oid: SEARCH_USER_ID,
|
|
73
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
74
|
+
}),
|
|
75
|
+
).toString('base64url')
|
|
76
|
+
return `${header}.${payload}.signature`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const createGraphJwt = (): string => {
|
|
80
|
+
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url')
|
|
81
|
+
const payload = Buffer.from(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
aud: GRAPH_AUDIENCE,
|
|
84
|
+
tid: SEARCH_TENANT_ID,
|
|
85
|
+
oid: SEARCH_USER_ID,
|
|
86
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
87
|
+
}),
|
|
88
|
+
).toString('base64url')
|
|
89
|
+
return `${header}.${payload}.signature`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const headerValue = (init: RequestInit | undefined, name: string): string | undefined => {
|
|
93
|
+
const headers = init?.headers
|
|
94
|
+
if (headers instanceof Headers) return headers.get(name) ?? undefined
|
|
95
|
+
if (Array.isArray(headers)) {
|
|
96
|
+
const pair = headers.find(([key]) => key.toLowerCase() === name.toLowerCase())
|
|
97
|
+
return pair?.[1]
|
|
98
|
+
}
|
|
99
|
+
return headers?.[name]
|
|
100
|
+
}
|
|
101
|
+
|
|
41
102
|
const mockResponse = (body: unknown, status = 200, headers: Record<string, string> = {}) => {
|
|
42
103
|
const defaultHeaders: Record<string, string> = {
|
|
43
104
|
'Content-Type': 'application/json',
|
|
@@ -53,6 +114,10 @@ describe('TeamsClient', () => {
|
|
|
53
114
|
)
|
|
54
115
|
}
|
|
55
116
|
|
|
117
|
+
const mockBinaryResponse = (body: string, status = 200) => {
|
|
118
|
+
fetchResponses.push(new Response(body, { status }))
|
|
119
|
+
}
|
|
120
|
+
|
|
56
121
|
describe('login', () => {
|
|
57
122
|
it('requires token', async () => {
|
|
58
123
|
await expect(new TeamsClient().login({ token: '', region: 'emea' })).rejects.toThrow(TeamsError)
|
|
@@ -325,6 +390,29 @@ describe('TeamsClient', () => {
|
|
|
325
390
|
expect(fetchCalls[0].options?.method).toBe('POST')
|
|
326
391
|
expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Hello world' }))
|
|
327
392
|
})
|
|
393
|
+
|
|
394
|
+
it('sends a reply to a channel thread', async () => {
|
|
395
|
+
mockResponse({
|
|
396
|
+
id: 'reply1',
|
|
397
|
+
channel_id: 'ch1',
|
|
398
|
+
author: { id: '123', displayName: 'Test User' },
|
|
399
|
+
content: 'Thread reply',
|
|
400
|
+
timestamp: '2024-01-01T00:01:00.000Z',
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
|
|
404
|
+
const message = await client.sendMessage('111', 'ch1', 'Thread reply', 'root1')
|
|
405
|
+
|
|
406
|
+
expect(fetchCalls[0].url).toBe(
|
|
407
|
+
'https://teams.microsoft.com/api/csa/emea/api/v2/teams/111/channels/ch1/messages/root1/replies',
|
|
408
|
+
)
|
|
409
|
+
expect(fetchCalls[0].options?.method).toBe('POST')
|
|
410
|
+
expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Thread reply', parentMessageId: 'root1' }))
|
|
411
|
+
// the reply the API echoes back omits thread ids, so the client normalizes them from the root
|
|
412
|
+
expect(message.root_message_id).toBe('root1')
|
|
413
|
+
expect(message.parent_message_id).toBe('root1')
|
|
414
|
+
expect(message.is_thread_reply).toBe(true)
|
|
415
|
+
})
|
|
328
416
|
})
|
|
329
417
|
|
|
330
418
|
describe('getMessages', () => {
|
|
@@ -359,6 +447,168 @@ describe('TeamsClient', () => {
|
|
|
359
447
|
'https://teams.microsoft.com/api/csa/emea/api/v2/teams/111/channels/ch1/messages?limit=50',
|
|
360
448
|
)
|
|
361
449
|
})
|
|
450
|
+
|
|
451
|
+
it('preserves thread fields for replies without inventing them for top-level messages', async () => {
|
|
452
|
+
mockResponse([
|
|
453
|
+
{
|
|
454
|
+
id: 'reply1',
|
|
455
|
+
channel_id: 'ch1',
|
|
456
|
+
author: { id: '123', displayName: 'User 1' },
|
|
457
|
+
content: 'Reply',
|
|
458
|
+
timestamp: '2024-01-01T00:01:00.000Z',
|
|
459
|
+
rootMessageId: 'root1',
|
|
460
|
+
parentMessageId: 'root1',
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'root1',
|
|
464
|
+
channel_id: 'ch1',
|
|
465
|
+
author: { id: '123', displayName: 'User 1' },
|
|
466
|
+
content: 'Top level',
|
|
467
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
468
|
+
},
|
|
469
|
+
])
|
|
470
|
+
|
|
471
|
+
const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
|
|
472
|
+
const messages = await client.getMessages('111', 'ch1')
|
|
473
|
+
|
|
474
|
+
expect(messages[0].root_message_id).toBe('root1')
|
|
475
|
+
expect(messages[0].parent_message_id).toBe('root1')
|
|
476
|
+
expect(messages[0].is_thread_reply).toBe(true)
|
|
477
|
+
expect(messages[1].root_message_id).toBeUndefined()
|
|
478
|
+
expect(messages[1].is_thread_reply).toBeFalsy()
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
describe('getThreadReplies', () => {
|
|
483
|
+
it('returns replies for a channel thread with root metadata', async () => {
|
|
484
|
+
mockResponse([
|
|
485
|
+
{
|
|
486
|
+
id: 'reply1',
|
|
487
|
+
channel_id: 'ch1',
|
|
488
|
+
author: { id: '123', displayName: 'User 1' },
|
|
489
|
+
content: 'Reply 1',
|
|
490
|
+
timestamp: '2024-01-01T00:01:00.000Z',
|
|
491
|
+
},
|
|
492
|
+
])
|
|
493
|
+
|
|
494
|
+
const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
|
|
495
|
+
const replies = await client.getThreadReplies('111', 'ch1', 'root1', 10)
|
|
496
|
+
|
|
497
|
+
expect(replies).toHaveLength(1)
|
|
498
|
+
expect(replies[0].root_message_id).toBe('root1')
|
|
499
|
+
expect(replies[0].parent_message_id).toBe('root1')
|
|
500
|
+
expect(replies[0].is_thread_reply).toBe(true)
|
|
501
|
+
expect(fetchCalls[0].url).toBe(
|
|
502
|
+
'https://teams.microsoft.com/api/csa/emea/api/v2/teams/111/channels/ch1/messages/root1/replies?limit=10',
|
|
503
|
+
)
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
describe('searchMessages', () => {
|
|
508
|
+
it('posts to Substrate search with bearer token, anchor mailbox, and parses nested results', async () => {
|
|
509
|
+
const manager = await setupCredentialManager()
|
|
510
|
+
const searchJwt = createSearchJwt()
|
|
511
|
+
mockResponse({ access_token: searchJwt, refresh_token: 'rotated-refresh', expires_in: 3600 })
|
|
512
|
+
mockResponse({
|
|
513
|
+
EntitySets: [
|
|
514
|
+
{
|
|
515
|
+
ResultSets: [
|
|
516
|
+
{
|
|
517
|
+
Results: [
|
|
518
|
+
{
|
|
519
|
+
Id: 'msg-1',
|
|
520
|
+
Content: '<p>Deploy complete</p>',
|
|
521
|
+
Author: { Id: 'author-1', DisplayName: 'Alice' },
|
|
522
|
+
ChannelId: 'channel-1',
|
|
523
|
+
ThreadId: 'thread-1',
|
|
524
|
+
TeamName: 'Team One',
|
|
525
|
+
ChannelName: 'General',
|
|
526
|
+
DateTimeSent: '2024-01-01T00:00:00.000Z',
|
|
527
|
+
WebUrl: 'https://teams.microsoft.com/l/message/msg-1',
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
|
|
537
|
+
const results = await client.searchMessages('deploy', { limit: 10, from: 5 })
|
|
538
|
+
|
|
539
|
+
expect(fetchCalls[1].url).toBe('https://substrate.office.com/searchservice/api/v2/query')
|
|
540
|
+
expect(fetchCalls[1].options?.method).toBe('POST')
|
|
541
|
+
expect(headerValue(fetchCalls[1].options, 'Authorization')).toBe(`Bearer ${searchJwt}`)
|
|
542
|
+
expect(headerValue(fetchCalls[1].options, 'x-anchormailbox')).toBe(`Oid:${SEARCH_USER_ID}@${SEARCH_TENANT_ID}`)
|
|
543
|
+
const payload = JSON.parse(String(fetchCalls[1].options?.body)) as {
|
|
544
|
+
entityRequests: Array<{ entityType: string; contentSources: string[]; from: number; size: number }>
|
|
545
|
+
}
|
|
546
|
+
expect(payload.entityRequests[0]).toMatchObject({
|
|
547
|
+
entityType: 'Message',
|
|
548
|
+
contentSources: ['Teams'],
|
|
549
|
+
from: 5,
|
|
550
|
+
size: 10,
|
|
551
|
+
})
|
|
552
|
+
expect(results).toEqual([
|
|
553
|
+
{
|
|
554
|
+
id: 'msg-1',
|
|
555
|
+
content: 'Deploy complete',
|
|
556
|
+
author: { id: 'author-1', displayName: 'Alice' },
|
|
557
|
+
channel_id: 'channel-1',
|
|
558
|
+
thread_id: 'thread-1',
|
|
559
|
+
team_name: 'Team One',
|
|
560
|
+
channel_name: 'General',
|
|
561
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
562
|
+
permalink: 'https://teams.microsoft.com/l/message/msg-1',
|
|
563
|
+
},
|
|
564
|
+
])
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('returns an empty array when Substrate has no nested results', async () => {
|
|
568
|
+
const manager = await setupCredentialManager()
|
|
569
|
+
mockResponse({ access_token: createSearchJwt(), refresh_token: 'rotated-refresh', expires_in: 3600 })
|
|
570
|
+
mockResponse({ EntitySets: [] })
|
|
571
|
+
|
|
572
|
+
const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
|
|
573
|
+
const results = await client.searchMessages('zzimprobablequery_xyz')
|
|
574
|
+
|
|
575
|
+
expect(results).toEqual([])
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('uses the logged-in account refresh token when current account differs', async () => {
|
|
579
|
+
const manager = await setupCredentialManager()
|
|
580
|
+
await manager.setDeviceCodeAccount({
|
|
581
|
+
accountType: 'personal',
|
|
582
|
+
token: 'personal-skype-token',
|
|
583
|
+
tokenExpiresAt: '2100-01-01T00:00:00Z',
|
|
584
|
+
aadRefreshToken: 'personal-refresh-token',
|
|
585
|
+
aadClientId: 'personal-client-id',
|
|
586
|
+
teams: {},
|
|
587
|
+
currentTeam: null,
|
|
588
|
+
})
|
|
589
|
+
const searchJwt = createSearchJwt()
|
|
590
|
+
mockResponse({ access_token: searchJwt, refresh_token: 'work-rotated-refresh', expires_in: 3600 })
|
|
591
|
+
mockResponse({ EntitySets: [] })
|
|
592
|
+
|
|
593
|
+
const client = await new TeamsClient(manager).login({ token: 'skype-token', accountType: 'work', region: 'emea' })
|
|
594
|
+
await client.searchMessages('deploy')
|
|
595
|
+
|
|
596
|
+
const tokenRequestBody = new URLSearchParams(String(fetchCalls[0].options?.body))
|
|
597
|
+
expect(tokenRequestBody.get('refresh_token')).toBe('refresh-token')
|
|
598
|
+
expect(tokenRequestBody.get('client_id')).toBe('client-id')
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('rejects invalid pagination options', async () => {
|
|
602
|
+
const manager = await setupCredentialManager()
|
|
603
|
+
const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
|
|
604
|
+
|
|
605
|
+
await expect(client.searchMessages('deploy', { limit: Number.NaN })).rejects.toThrow('positive integer')
|
|
606
|
+
await expect(client.searchMessages('deploy', { limit: 0 })).rejects.toThrow('positive integer')
|
|
607
|
+
await expect(client.searchMessages('deploy', { limit: -1 })).rejects.toThrow('positive integer')
|
|
608
|
+
await expect(client.searchMessages('deploy', { from: -1 })).rejects.toThrow('non-negative integer')
|
|
609
|
+
await expect(client.searchMessages('deploy', { from: 1.5 })).rejects.toThrow('non-negative integer')
|
|
610
|
+
expect(fetchCalls).toHaveLength(0)
|
|
611
|
+
})
|
|
362
612
|
})
|
|
363
613
|
|
|
364
614
|
describe('getMessage', () => {
|
|
@@ -490,6 +740,90 @@ describe('TeamsClient', () => {
|
|
|
490
740
|
})
|
|
491
741
|
})
|
|
492
742
|
|
|
743
|
+
describe('downloadFile', () => {
|
|
744
|
+
it('downloads SharePoint files through Graph shares with base64url share id', async () => {
|
|
745
|
+
const manager = await setupCredentialManager()
|
|
746
|
+
const shareUrl = 'https://contoso.sharepoint.com/sites/team/Shared%20Documents/report.docx'
|
|
747
|
+
mockResponse([
|
|
748
|
+
{ id: 'file1', name: 'report.docx', size: 11, url: shareUrl, contentType: 'application/vnd.ms-word' },
|
|
749
|
+
])
|
|
750
|
+
mockResponse({ access_token: createGraphJwt(), refresh_token: 'rotated-refresh', expires_in: 3600 })
|
|
751
|
+
mockBinaryResponse('graph-bytes')
|
|
752
|
+
|
|
753
|
+
const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
|
|
754
|
+
const result = await client.downloadFile('111', 'ch1', 'file1')
|
|
755
|
+
|
|
756
|
+
const shareId = `u!${Buffer.from(shareUrl).toString('base64url').replace(/=+$/, '')}`
|
|
757
|
+
expect(fetchCalls[2].url).toBe(`https://graph.microsoft.com/v1.0/shares/${shareId}/driveItem/content`)
|
|
758
|
+
expect(headerValue(fetchCalls[2].options, 'Authorization')).toBe(`Bearer ${createGraphJwt()}`)
|
|
759
|
+
expect(Buffer.from(result.buffer).toString()).toBe('graph-bytes')
|
|
760
|
+
expect(result.file.id).toBe('file1')
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
it('downloads inline object URLs with the Skype token', async () => {
|
|
764
|
+
mockResponse([
|
|
765
|
+
{
|
|
766
|
+
id: 'file2',
|
|
767
|
+
name: 'image.png',
|
|
768
|
+
size: 10,
|
|
769
|
+
url: 'https://teams.microsoft.com/files/image.png',
|
|
770
|
+
object_url: 'https://us-api.asm.skype.com/v1/objects/0-weu-d1/image.png',
|
|
771
|
+
contentType: 'image/png',
|
|
772
|
+
},
|
|
773
|
+
])
|
|
774
|
+
mockBinaryResponse('image-bytes')
|
|
775
|
+
|
|
776
|
+
const client = await new TeamsClient().login({ token: 'skype-token', region: 'emea' })
|
|
777
|
+
const result = await client.downloadFile('111', 'ch1', 'file2')
|
|
778
|
+
|
|
779
|
+
expect(fetchCalls[1].url).toBe('https://us-api.asm.skype.com/v1/objects/0-weu-d1/image.png')
|
|
780
|
+
expect(headerValue(fetchCalls[1].options, 'Authorization')).toBe('Bearer skype-token')
|
|
781
|
+
expect(headerValue(fetchCalls[1].options, 'X-Skypetoken')).toBe('skype-token')
|
|
782
|
+
expect(Buffer.from(result.buffer).toString()).toBe('image-bytes')
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
it('throws TeamsAuthCapabilityError for SharePoint files with cookie-only credentials', async () => {
|
|
786
|
+
const dir = join(
|
|
787
|
+
import.meta.dir,
|
|
788
|
+
`.test-teams-client-cookie-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
789
|
+
)
|
|
790
|
+
TEMP_DIRS_TO_CLEANUP.push(dir)
|
|
791
|
+
const manager = new TeamsCredentialManager(dir)
|
|
792
|
+
await manager.setToken('skype-token', 'work', '2100-01-01T00:00:00Z')
|
|
793
|
+
mockResponse([
|
|
794
|
+
{
|
|
795
|
+
id: 'file3',
|
|
796
|
+
name: 'deck.pptx',
|
|
797
|
+
size: 10,
|
|
798
|
+
url: 'https://contoso.sharepoint.com/sites/team/Shared%20Documents/deck.pptx',
|
|
799
|
+
},
|
|
800
|
+
])
|
|
801
|
+
|
|
802
|
+
const client = await new TeamsClient(manager).login({ token: 'skype-token', region: 'emea' })
|
|
803
|
+
|
|
804
|
+
await expect(client.downloadFile('111', 'ch1', 'file3')).rejects.toThrow('Requires `agent-teams auth login`')
|
|
805
|
+
expect(fetchCalls).toHaveLength(1)
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
it('refuses to send the Skype token to an untrusted host', async () => {
|
|
809
|
+
mockResponse([
|
|
810
|
+
{
|
|
811
|
+
id: 'file4',
|
|
812
|
+
name: 'evil.bin',
|
|
813
|
+
size: 10,
|
|
814
|
+
url: 'https://evil.example.com/steal',
|
|
815
|
+
object_url: 'https://evil.example.com/steal',
|
|
816
|
+
},
|
|
817
|
+
])
|
|
818
|
+
|
|
819
|
+
const client = await new TeamsClient().login({ token: 'skype-token', region: 'emea' })
|
|
820
|
+
|
|
821
|
+
await expect(client.downloadFile('111', 'ch1', 'file4')).rejects.toThrow('untrusted host')
|
|
822
|
+
// only the listFiles call happened — no credentialed download fetch to the untrusted host
|
|
823
|
+
expect(fetchCalls).toHaveLength(1)
|
|
824
|
+
})
|
|
825
|
+
})
|
|
826
|
+
|
|
493
827
|
describe('rate limiting', () => {
|
|
494
828
|
it('waits when bucket is exhausted before making request', async () => {
|
|
495
829
|
mockResponse({ userDetails: JSON.stringify({ name: 'User 1' }), locale: 'en-us' }, 200, {
|