agent-messenger 2.9.0 → 2.10.1

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 (118) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/teams/client.d.ts +9 -1
  4. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  5. package/dist/src/platforms/teams/client.js +69 -18
  6. package/dist/src/platforms/teams/client.js.map +1 -1
  7. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  8. package/dist/src/platforms/teams/commands/auth.js +7 -2
  9. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  10. package/dist/src/platforms/teams/commands/channel.d.ts.map +1 -1
  11. package/dist/src/platforms/teams/commands/channel.js +18 -3
  12. package/dist/src/platforms/teams/commands/channel.js.map +1 -1
  13. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
  14. package/dist/src/platforms/teams/commands/file.js +18 -3
  15. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  16. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  17. package/dist/src/platforms/teams/commands/message.js +24 -4
  18. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  19. package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
  20. package/dist/src/platforms/teams/commands/reaction.js +12 -2
  21. package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
  22. package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
  23. package/dist/src/platforms/teams/commands/snapshot.js +6 -1
  24. package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
  25. package/dist/src/platforms/teams/commands/team.d.ts.map +1 -1
  26. package/dist/src/platforms/teams/commands/team.js +6 -1
  27. package/dist/src/platforms/teams/commands/team.js.map +1 -1
  28. package/dist/src/platforms/teams/commands/user.d.ts.map +1 -1
  29. package/dist/src/platforms/teams/commands/user.js +18 -3
  30. package/dist/src/platforms/teams/commands/user.js.map +1 -1
  31. package/dist/src/platforms/teams/commands/whoami.d.ts.map +1 -1
  32. package/dist/src/platforms/teams/commands/whoami.js +6 -1
  33. package/dist/src/platforms/teams/commands/whoami.js.map +1 -1
  34. package/dist/src/platforms/teams/credential-manager.d.ts +3 -1
  35. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  36. package/dist/src/platforms/teams/credential-manager.js +6 -1
  37. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  38. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  39. package/dist/src/platforms/teams/ensure-auth.js +7 -2
  40. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  41. package/dist/src/platforms/teams/token-extractor.d.ts +3 -1
  42. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  43. package/dist/src/platforms/teams/token-extractor.js +73 -10
  44. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  45. package/dist/src/platforms/teams/types.d.ts +17 -0
  46. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  47. package/dist/src/platforms/teams/types.js +2 -0
  48. package/dist/src/platforms/teams/types.js.map +1 -1
  49. package/dist/src/platforms/webex/client.d.ts +3 -0
  50. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  51. package/dist/src/platforms/webex/client.js +58 -13
  52. package/dist/src/platforms/webex/client.js.map +1 -1
  53. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  54. package/dist/src/platforms/webex/commands/auth.js +61 -10
  55. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  56. package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
  57. package/dist/src/platforms/webex/credential-manager.js +18 -6
  58. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  59. package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
  60. package/dist/src/platforms/webex/encryption.js +3 -1
  61. package/dist/src/platforms/webex/encryption.js.map +1 -1
  62. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  63. package/dist/src/platforms/webex/ensure-auth.js +10 -2
  64. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  65. package/dist/src/platforms/webex/token-extractor.d.ts +1 -0
  66. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  67. package/dist/src/platforms/webex/token-extractor.js +21 -4
  68. package/dist/src/platforms/webex/token-extractor.js.map +1 -1
  69. package/e2e/webex.e2e.test.ts +57 -0
  70. package/package.json +1 -1
  71. package/skills/agent-channeltalk/SKILL.md +1 -1
  72. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  73. package/skills/agent-discord/SKILL.md +1 -1
  74. package/skills/agent-discordbot/SKILL.md +1 -1
  75. package/skills/agent-instagram/SKILL.md +1 -1
  76. package/skills/agent-kakaotalk/SKILL.md +1 -1
  77. package/skills/agent-line/SKILL.md +1 -1
  78. package/skills/agent-slack/SKILL.md +1 -1
  79. package/skills/agent-slackbot/SKILL.md +1 -1
  80. package/skills/agent-teams/SKILL.md +1 -1
  81. package/skills/agent-telegram/SKILL.md +1 -1
  82. package/skills/agent-webex/SKILL.md +1 -1
  83. package/skills/agent-wechatbot/SKILL.md +1 -1
  84. package/skills/agent-whatsapp/SKILL.md +1 -1
  85. package/skills/agent-whatsappbot/SKILL.md +1 -1
  86. package/src/platforms/teams/client.test.ts +34 -30
  87. package/src/platforms/teams/client.ts +92 -20
  88. package/src/platforms/teams/commands/auth.test.ts +6 -2
  89. package/src/platforms/teams/commands/auth.ts +7 -2
  90. package/src/platforms/teams/commands/channel.test.ts +6 -6
  91. package/src/platforms/teams/commands/channel.ts +18 -3
  92. package/src/platforms/teams/commands/file.ts +18 -3
  93. package/src/platforms/teams/commands/message.ts +24 -4
  94. package/src/platforms/teams/commands/reaction.ts +12 -2
  95. package/src/platforms/teams/commands/snapshot.ts +6 -1
  96. package/src/platforms/teams/commands/team.test.ts +2 -2
  97. package/src/platforms/teams/commands/team.ts +6 -1
  98. package/src/platforms/teams/commands/user.ts +18 -3
  99. package/src/platforms/teams/commands/whoami.ts +6 -1
  100. package/src/platforms/teams/credential-manager.test.ts +25 -0
  101. package/src/platforms/teams/credential-manager.ts +13 -3
  102. package/src/platforms/teams/ensure-auth.test.ts +6 -1
  103. package/src/platforms/teams/ensure-auth.ts +7 -2
  104. package/src/platforms/teams/token-extractor.test.ts +112 -98
  105. package/src/platforms/teams/token-extractor.ts +83 -12
  106. package/src/platforms/teams/types.test.ts +17 -0
  107. package/src/platforms/teams/types.ts +6 -0
  108. package/src/platforms/webex/client.test.ts +157 -13
  109. package/src/platforms/webex/client.ts +64 -15
  110. package/src/platforms/webex/commands/auth.test.ts +122 -1
  111. package/src/platforms/webex/commands/auth.ts +72 -17
  112. package/src/platforms/webex/credential-manager.test.ts +63 -0
  113. package/src/platforms/webex/credential-manager.ts +22 -8
  114. package/src/platforms/webex/encryption.test.ts +54 -0
  115. package/src/platforms/webex/encryption.ts +3 -1
  116. package/src/platforms/webex/ensure-auth.ts +10 -2
  117. package/src/platforms/webex/token-extractor.test.ts +32 -3
  118. package/src/platforms/webex/token-extractor.ts +26 -5
@@ -55,18 +55,18 @@ describe('TeamsClient', () => {
55
55
 
56
56
  describe('login', () => {
57
57
  test('requires token', async () => {
58
- await expect(new TeamsClient().login({ token: '' })).rejects.toThrow(TeamsError)
59
- await expect(new TeamsClient().login({ token: '' })).rejects.toThrow('Token is required')
58
+ await expect(new TeamsClient().login({ token: '', region: 'emea' })).rejects.toThrow(TeamsError)
59
+ await expect(new TeamsClient().login({ token: '', region: 'emea' })).rejects.toThrow('Token is required')
60
60
  })
61
61
 
62
62
  test('accepts valid token', async () => {
63
- const client = await new TeamsClient().login({ token: 'test-token' })
63
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
64
64
  expect(client).toBeInstanceOf(TeamsClient)
65
65
  })
66
66
 
67
67
  test('accepts token with expiry time', async () => {
68
68
  const expiresAt = new Date(Date.now() + 3600000).toISOString()
69
- const client = await new TeamsClient().login({ token: 'test-token', tokenExpiresAt: expiresAt })
69
+ const client = await new TeamsClient().login({ token: 'test-token', tokenExpiresAt: expiresAt, region: 'emea' })
70
70
  expect(client).toBeInstanceOf(TeamsClient)
71
71
  })
72
72
  })
@@ -74,7 +74,11 @@ describe('TeamsClient', () => {
74
74
  describe('token expiry', () => {
75
75
  test('throws when token is expired', async () => {
76
76
  const expiredAt = new Date(Date.now() - 1000).toISOString()
77
- const client = await new TeamsClient().login({ token: 'expired-token', tokenExpiresAt: expiredAt })
77
+ const client = await new TeamsClient().login({
78
+ token: 'expired-token',
79
+ tokenExpiresAt: expiredAt,
80
+ region: 'emea',
81
+ })
78
82
 
79
83
  await expect(client.testAuth()).rejects.toThrow(TeamsError)
80
84
  await expect(client.testAuth()).rejects.toThrow('Token has expired')
@@ -87,7 +91,7 @@ describe('TeamsClient', () => {
87
91
  locale: 'en-us',
88
92
  })
89
93
 
90
- const client = await new TeamsClient().login({ token: 'valid-token', tokenExpiresAt: expiresAt })
94
+ const client = await new TeamsClient().login({ token: 'valid-token', tokenExpiresAt: expiresAt, region: 'emea' })
91
95
  const user = await client.testAuth()
92
96
 
93
97
  expect(user.id).toBe('ME')
@@ -102,7 +106,7 @@ describe('TeamsClient', () => {
102
106
  locale: 'en-us',
103
107
  })
104
108
 
105
- const client = await new TeamsClient().login({ token: 'test-token' })
109
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
106
110
  const user = await client.testAuth()
107
111
 
108
112
  expect(user.id).toBe('ME')
@@ -117,7 +121,7 @@ describe('TeamsClient', () => {
117
121
  test('throws TeamsError on API error', async () => {
118
122
  mockResponse({ message: 'Unauthorized', code: 'unauthorized' }, 401)
119
123
 
120
- const client = await new TeamsClient().login({ token: 'bad-token' })
124
+ const client = await new TeamsClient().login({ token: 'bad-token', region: 'emea' })
121
125
  await expect(client.testAuth()).rejects.toThrow(TeamsError)
122
126
  })
123
127
  })
@@ -153,7 +157,7 @@ describe('TeamsClient', () => {
153
157
  ],
154
158
  })
155
159
 
156
- const client = await new TeamsClient().login({ token: 'test-token' })
160
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
157
161
  const teams = await client.listTeams()
158
162
 
159
163
  expect(teams).toHaveLength(2)
@@ -169,7 +173,7 @@ describe('TeamsClient', () => {
169
173
  test('returns team info', async () => {
170
174
  mockResponse({ id: '111', name: 'Test Team', description: 'A test team' })
171
175
 
172
- const client = await new TeamsClient().login({ token: 'test-token' })
176
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
173
177
  const team = await client.getTeam('111')
174
178
 
175
179
  expect(team.id).toBe('111')
@@ -185,7 +189,7 @@ describe('TeamsClient', () => {
185
189
  { id: 'ch2', team_id: '111', name: 'Random', type: 'standard' },
186
190
  ])
187
191
 
188
- const client = await new TeamsClient().login({ token: 'test-token' })
192
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
189
193
  const channels = await client.listChannels('111')
190
194
 
191
195
  expect(channels).toHaveLength(2)
@@ -198,7 +202,7 @@ describe('TeamsClient', () => {
198
202
  test('returns channel info', async () => {
199
203
  mockResponse({ id: 'ch1', team_id: '111', name: 'General', type: 'standard' })
200
204
 
201
- const client = await new TeamsClient().login({ token: 'test-token' })
205
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
202
206
  const channel = await client.getChannel('111', 'ch1')
203
207
 
204
208
  expect(channel.id).toBe('ch1')
@@ -217,7 +221,7 @@ describe('TeamsClient', () => {
217
221
  timestamp: '2024-01-01T00:00:00.000Z',
218
222
  })
219
223
 
220
- const client = await new TeamsClient().login({ token: 'test-token' })
224
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
221
225
  const message = await client.sendMessage('111', 'ch1', 'Hello world')
222
226
 
223
227
  expect(message.content).toBe('Hello world')
@@ -239,7 +243,7 @@ describe('TeamsClient', () => {
239
243
  },
240
244
  ])
241
245
 
242
- const client = await new TeamsClient().login({ token: 'test-token' })
246
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
243
247
  const messages = await client.getMessages('111', 'ch1', 50)
244
248
 
245
249
  expect(messages).toHaveLength(1)
@@ -252,7 +256,7 @@ describe('TeamsClient', () => {
252
256
  test('uses default limit of 50', async () => {
253
257
  mockResponse([])
254
258
 
255
- const client = await new TeamsClient().login({ token: 'test-token' })
259
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
256
260
  await client.getMessages('111', 'ch1')
257
261
 
258
262
  expect(fetchCalls[0].url).toBe(
@@ -271,7 +275,7 @@ describe('TeamsClient', () => {
271
275
  timestamp: '2024-01-01T00:00:00.000Z',
272
276
  })
273
277
 
274
- const client = await new TeamsClient().login({ token: 'test-token' })
278
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
275
279
  const message = await client.getMessage('111', 'ch1', 'msg1')
276
280
 
277
281
  expect(message.id).toBe('msg1')
@@ -285,7 +289,7 @@ describe('TeamsClient', () => {
285
289
  test('deletes message', async () => {
286
290
  mockResponse(null, 204)
287
291
 
288
- const client = await new TeamsClient().login({ token: 'test-token' })
292
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
289
293
  await client.deleteMessage('111', 'ch1', 'msg1')
290
294
 
291
295
  expect(fetchCalls[0].url).toBe(
@@ -299,7 +303,7 @@ describe('TeamsClient', () => {
299
303
  test('adds reaction to message', async () => {
300
304
  mockResponse(null, 204)
301
305
 
302
- const client = await new TeamsClient().login({ token: 'test-token' })
306
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
303
307
  await client.addReaction('111', 'ch1', 'msg1', 'like')
304
308
 
305
309
  expect(fetchCalls[0].url).toBe(
@@ -314,7 +318,7 @@ describe('TeamsClient', () => {
314
318
  test('removes reaction from message', async () => {
315
319
  mockResponse(null, 204)
316
320
 
317
- const client = await new TeamsClient().login({ token: 'test-token' })
321
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
318
322
  await client.removeReaction('111', 'ch1', 'msg1', 'like')
319
323
 
320
324
  expect(fetchCalls[0].url).toBe(
@@ -331,7 +335,7 @@ describe('TeamsClient', () => {
331
335
  { id: 'u2', displayName: 'User 2', email: 'user2@example.com' },
332
336
  ])
333
337
 
334
- const client = await new TeamsClient().login({ token: 'test-token' })
338
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
335
339
  const users = await client.listUsers('111')
336
340
 
337
341
  expect(users).toHaveLength(2)
@@ -344,7 +348,7 @@ describe('TeamsClient', () => {
344
348
  test('returns user info', async () => {
345
349
  mockResponse({ id: 'u1', displayName: 'Test User', email: 'test@example.com' })
346
350
 
347
- const client = await new TeamsClient().login({ token: 'test-token' })
351
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
348
352
  const user = await client.getUser('u1')
349
353
 
350
354
  expect(user.id).toBe('u1')
@@ -365,7 +369,7 @@ describe('TeamsClient', () => {
365
369
  url: 'https://teams.microsoft.com/files/file1',
366
370
  })
367
371
 
368
- const client = await new TeamsClient().login({ token: 'test-token' })
372
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
369
373
  const file = await client.uploadFile('111', 'ch1', tempFile)
370
374
 
371
375
  expect(file.name).toBe('test-teams-upload.txt')
@@ -381,7 +385,7 @@ describe('TeamsClient', () => {
381
385
  { id: 'file2', name: 'image.png', size: 2048, url: 'https://example.com/image.png' },
382
386
  ])
383
387
 
384
- const client = await new TeamsClient().login({ token: 'test-token' })
388
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
385
389
  const files = await client.listFiles('111', 'ch1')
386
390
 
387
391
  expect(files).toHaveLength(2)
@@ -401,7 +405,7 @@ describe('TeamsClient', () => {
401
405
  'X-RateLimit-Reset': String(Date.now() / 1000 + 60),
402
406
  })
403
407
 
404
- const client = await new TeamsClient().login({ token: 'test-token' })
408
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
405
409
  await client.testAuth()
406
410
 
407
411
  const startTime = Date.now()
@@ -416,7 +420,7 @@ describe('TeamsClient', () => {
416
420
  mockResponse({ message: 'Rate limited' }, 429, { 'Retry-After': '0.1' })
417
421
  mockResponse({ userDetails: JSON.stringify({ name: 'User' }), locale: 'en-us' })
418
422
 
419
- const client = await new TeamsClient().login({ token: 'test-token' })
423
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
420
424
  const user = await client.testAuth()
421
425
 
422
426
  expect(user.id).toBe('ME')
@@ -428,7 +432,7 @@ describe('TeamsClient', () => {
428
432
  mockResponse({ message: 'Rate limited' }, 429, { 'Retry-After': '0.01' })
429
433
  }
430
434
 
431
- const client = await new TeamsClient().login({ token: 'test-token' })
435
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
432
436
  await expect(client.testAuth()).rejects.toThrow(TeamsError)
433
437
  expect(fetchCalls.length).toBeLessThanOrEqual(4)
434
438
  })
@@ -439,7 +443,7 @@ describe('TeamsClient', () => {
439
443
  mockResponse({ message: 'Internal Server Error' }, 500)
440
444
  mockResponse({ userDetails: JSON.stringify({ name: 'User' }), locale: 'en-us' })
441
445
 
442
- const client = await new TeamsClient().login({ token: 'test-token' })
446
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
443
447
  const user = await client.testAuth()
444
448
 
445
449
  expect(user.id).toBe('ME')
@@ -449,7 +453,7 @@ describe('TeamsClient', () => {
449
453
  test('does not retry on 4xx client errors (except 429)', async () => {
450
454
  mockResponse({ message: 'Not Found' }, 404)
451
455
 
452
- const client = await new TeamsClient().login({ token: 'test-token' })
456
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
453
457
  await expect(client.testAuth()).rejects.toThrow(TeamsError)
454
458
  expect(fetchCalls.length).toBe(1)
455
459
  })
@@ -459,7 +463,7 @@ describe('TeamsClient', () => {
459
463
  mockResponse({ message: 'Error' }, 500)
460
464
  mockResponse({ userDetails: JSON.stringify({ name: 'User' }), locale: 'en-us' })
461
465
 
462
- const client = await new TeamsClient().login({ token: 'test-token' })
466
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
463
467
  const startTime = Date.now()
464
468
  await client.testAuth()
465
469
  const elapsed = Date.now() - startTime
@@ -474,7 +478,7 @@ describe('TeamsClient', () => {
474
478
  mockResponse([])
475
479
  mockResponse([])
476
480
 
477
- const client = await new TeamsClient().login({ token: 'test-token' })
481
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
478
482
  await client.getMessages('team1', 'ch1')
479
483
  await client.getMessages('team2', 'ch2')
480
484
 
@@ -2,7 +2,15 @@ import { readFile } from 'node:fs/promises'
2
2
  import { basename } from 'node:path'
3
3
 
4
4
  import { TeamsCredentialManager } from './credential-manager'
5
- import type { TeamsChannel, TeamsFile, TeamsMessage, TeamsTeam, TeamsUser } from './types'
5
+ import type {
6
+ TeamsAccountType,
7
+ TeamsChannel,
8
+ TeamsFile,
9
+ TeamsMessage,
10
+ TeamsRegion,
11
+ TeamsTeam,
12
+ TeamsUser,
13
+ } from './types'
6
14
  import { TeamsError } from './types'
7
15
 
8
16
  interface RateLimitBucket {
@@ -10,18 +18,28 @@ interface RateLimitBucket {
10
18
  resetAt: number
11
19
  }
12
20
 
13
- const MSG_API_BASE = 'https://emea.ng.msg.teams.microsoft.com/v1'
21
+ const PERSONAL_MSG_API_BASE = 'https://msgapi.teams.live.com/v1'
14
22
  const CSA_API_BASE = 'https://teams.microsoft.com/api'
15
23
  const MAX_RETRIES = 3
16
24
  const BASE_BACKOFF_MS = 100
25
+ const DEFAULT_REGION: TeamsRegion = 'amer'
26
+ const REGIONS: TeamsRegion[] = ['amer', 'emea', 'apac']
17
27
 
18
28
  export class TeamsClient {
19
29
  private token: string | null = null
20
30
  private tokenExpiresAt?: Date
31
+ private isPersonalAccount: boolean = false
32
+ private region: TeamsRegion = DEFAULT_REGION
33
+ private regionDiscovered: boolean = false
21
34
  private buckets: Map<string, RateLimitBucket> = new Map()
22
35
  private globalRateLimitUntil: number = 0
23
36
 
24
- async login(credentials?: { token: string; tokenExpiresAt?: string }): Promise<this> {
37
+ async login(credentials?: {
38
+ token: string
39
+ tokenExpiresAt?: string
40
+ accountType?: TeamsAccountType
41
+ region?: TeamsRegion
42
+ }): Promise<this> {
25
43
  if (credentials) {
26
44
  if (!credentials.token) {
27
45
  throw new TeamsError('Token is required', 'missing_token')
@@ -30,6 +48,11 @@ export class TeamsClient {
30
48
  if (credentials.tokenExpiresAt) {
31
49
  this.tokenExpiresAt = new Date(credentials.tokenExpiresAt)
32
50
  }
51
+ this.isPersonalAccount = credentials.accountType === 'personal'
52
+ if (credentials.region) {
53
+ this.region = credentials.region
54
+ this.regionDiscovered = true
55
+ }
33
56
  return this
34
57
  }
35
58
 
@@ -43,7 +66,16 @@ export class TeamsClient {
43
66
  'no_credentials',
44
67
  )
45
68
  }
46
- return this.login({ token: creds.token, tokenExpiresAt: creds.tokenExpiresAt })
69
+ return this.login({
70
+ token: creds.token,
71
+ tokenExpiresAt: creds.tokenExpiresAt,
72
+ accountType: creds.accountType,
73
+ region: creds.region,
74
+ })
75
+ }
76
+
77
+ getRegion(): TeamsRegion {
78
+ return this.region
47
79
  }
48
80
 
49
81
  private ensureAuth(): string {
@@ -108,12 +140,47 @@ export class TeamsClient {
108
140
  return new Promise((resolve) => setTimeout(resolve, ms))
109
141
  }
110
142
 
111
- private async request<T>(method: string, path: string, body?: unknown, baseUrl: string = MSG_API_BASE): Promise<T> {
143
+ private getMsgApiBase(): string {
144
+ if (this.isPersonalAccount) return PERSONAL_MSG_API_BASE
145
+ return `https://${this.region}.ng.msg.teams.microsoft.com/v1`
146
+ }
147
+
148
+ private async discoverRegion(): Promise<void> {
149
+ if (this.isPersonalAccount) {
150
+ this.regionDiscovered = true
151
+ return
152
+ }
153
+
154
+ const token = this.ensureAuth()
155
+
156
+ for (const region of REGIONS) {
157
+ try {
158
+ const response = await fetch(`https://${region}.ng.msg.teams.microsoft.com/v1/users/ME/properties`, {
159
+ headers: {
160
+ 'X-Skypetoken': token,
161
+ },
162
+ })
163
+
164
+ if (response.ok || response.status !== 403) {
165
+ this.region = region
166
+ break
167
+ }
168
+ } catch {}
169
+ }
170
+
171
+ this.regionDiscovered = true
172
+ }
173
+
174
+ private async request<T>(method: string, path: string, body?: unknown, baseUrl?: string): Promise<T> {
112
175
  if (this.isTokenExpired()) {
113
176
  throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired')
114
177
  }
115
178
 
116
- const url = `${baseUrl}${path}`
179
+ if (baseUrl === undefined && !this.regionDiscovered) {
180
+ await this.discoverRegion()
181
+ }
182
+
183
+ const url = `${baseUrl ?? this.getMsgApiBase()}${path}`
117
184
  const bucketKey = this.getBucketKey(method, path)
118
185
 
119
186
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
@@ -144,7 +211,7 @@ export class TeamsClient {
144
211
  const errorBody = (await response.json().catch(() => null)) as {
145
212
  message?: string
146
213
  } | null
147
- throw new TeamsError(errorBody?.message ?? 'Rate limited', 'rate_limited')
214
+ throw new TeamsError(errorBody?.message || 'Rate limited', 'rate_limited')
148
215
  }
149
216
 
150
217
  if (response.status >= 500 && attempt < MAX_RETRIES) {
@@ -158,7 +225,7 @@ export class TeamsClient {
158
225
  code?: string | number
159
226
  } | null
160
227
  throw new TeamsError(
161
- errorBody?.message ?? `HTTP ${response.status}`,
228
+ errorBody?.message || `HTTP ${response.status}`,
162
229
  errorBody?.code?.toString() ?? `http_${response.status}`,
163
230
  )
164
231
  }
@@ -173,12 +240,16 @@ export class TeamsClient {
173
240
  throw new TeamsError('Request failed after retries', 'max_retries')
174
241
  }
175
242
 
176
- private async requestFormData<T>(path: string, formData: FormData, baseUrl: string = MSG_API_BASE): Promise<T> {
243
+ private async requestFormData<T>(path: string, formData: FormData, baseUrl?: string): Promise<T> {
177
244
  if (this.isTokenExpired()) {
178
245
  throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired')
179
246
  }
180
247
 
181
- const url = `${baseUrl}${path}`
248
+ if (baseUrl === undefined && !this.regionDiscovered) {
249
+ await this.discoverRegion()
250
+ }
251
+
252
+ const url = `${baseUrl ?? this.getMsgApiBase()}${path}`
182
253
  const bucketKey = this.getBucketKey('POST', path)
183
254
 
184
255
  await this.waitForRateLimit(bucketKey)
@@ -199,7 +270,7 @@ export class TeamsClient {
199
270
  code?: string | number
200
271
  } | null
201
272
  throw new TeamsError(
202
- errorBody?.message ?? `HTTP ${response.status}`,
273
+ errorBody?.message || `HTTP ${response.status}`,
203
274
  errorBody?.code?.toString() ?? `http_${response.status}`,
204
275
  )
205
276
  }
@@ -210,13 +281,14 @@ export class TeamsClient {
210
281
  async testAuth(): Promise<TeamsUser> {
211
282
  interface UserProperties {
212
283
  userDetails?: string
284
+ primaryMemberName?: string
213
285
  locale?: string
214
286
  }
215
287
  const props = await this.request<UserProperties>('GET', '/users/ME/properties')
216
288
  const userDetails = props.userDetails ? JSON.parse(props.userDetails) : {}
217
289
  return {
218
290
  id: 'ME',
219
- displayName: userDetails.name || 'Teams User',
291
+ displayName: userDetails.name || props.primaryMemberName || 'Teams User',
220
292
  }
221
293
  }
222
294
 
@@ -272,7 +344,7 @@ export class TeamsClient {
272
344
  async sendMessage(teamId: string, channelId: string, content: string): Promise<TeamsMessage> {
273
345
  return this.request<TeamsMessage>(
274
346
  'POST',
275
- `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages`,
347
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages`,
276
348
  { content },
277
349
  CSA_API_BASE,
278
350
  )
@@ -281,7 +353,7 @@ export class TeamsClient {
281
353
  async getMessages(teamId: string, channelId: string, limit: number = 50): Promise<TeamsMessage[]> {
282
354
  return this.request<TeamsMessage[]>(
283
355
  'GET',
284
- `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages?limit=${limit}`,
356
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages?limit=${limit}`,
285
357
  undefined,
286
358
  CSA_API_BASE,
287
359
  )
@@ -290,7 +362,7 @@ export class TeamsClient {
290
362
  async getMessage(teamId: string, channelId: string, messageId: string): Promise<TeamsMessage> {
291
363
  return this.request<TeamsMessage>(
292
364
  'GET',
293
- `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`,
365
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`,
294
366
  undefined,
295
367
  CSA_API_BASE,
296
368
  )
@@ -299,7 +371,7 @@ export class TeamsClient {
299
371
  async deleteMessage(teamId: string, channelId: string, messageId: string): Promise<void> {
300
372
  return this.request<void>(
301
373
  'DELETE',
302
- `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`,
374
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`,
303
375
  undefined,
304
376
  CSA_API_BASE,
305
377
  )
@@ -308,7 +380,7 @@ export class TeamsClient {
308
380
  async addReaction(teamId: string, channelId: string, messageId: string, emoji: string): Promise<void> {
309
381
  return this.request<void>(
310
382
  'POST',
311
- `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions`,
383
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions`,
312
384
  { emoji },
313
385
  CSA_API_BASE,
314
386
  )
@@ -317,7 +389,7 @@ export class TeamsClient {
317
389
  async removeReaction(teamId: string, channelId: string, messageId: string, emoji: string): Promise<void> {
318
390
  return this.request<void>(
319
391
  'DELETE',
320
- `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions/${emoji}`,
392
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}/reactions/${emoji}`,
321
393
  undefined,
322
394
  CSA_API_BASE,
323
395
  )
@@ -339,7 +411,7 @@ export class TeamsClient {
339
411
  formData.append('file', new Blob([fileBuffer]), filename)
340
412
 
341
413
  return this.requestFormData<TeamsFile>(
342
- `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/files`,
414
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/files`,
343
415
  formData,
344
416
  CSA_API_BASE,
345
417
  )
@@ -348,7 +420,7 @@ export class TeamsClient {
348
420
  async listFiles(teamId: string, channelId: string): Promise<TeamsFile[]> {
349
421
  return this.request<TeamsFile[]>(
350
422
  'GET',
351
- `/csa/emea/api/v2/teams/${teamId}/channels/${channelId}/files`,
423
+ `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/files`,
352
424
  undefined,
353
425
  CSA_API_BASE,
354
426
  )
@@ -12,6 +12,7 @@ let credManagerLoadConfigSpy: ReturnType<typeof spyOn>
12
12
  let credManagerSaveConfigSpy: ReturnType<typeof spyOn>
13
13
  let credManagerClearCredentialsSpy: ReturnType<typeof spyOn>
14
14
  let credManagerIsTokenExpiredSpy: ReturnType<typeof spyOn>
15
+ let clientGetRegionSpy: ReturnType<typeof spyOn>
15
16
 
16
17
  beforeEach(() => {
17
18
  extractorExtractSpy = spyOn(TeamsTokenExtractor.prototype, 'extract').mockResolvedValue([
@@ -29,6 +30,8 @@ beforeEach(() => {
29
30
  { id: 'team-2', name: 'Team Two' },
30
31
  ])
31
32
 
33
+ clientGetRegionSpy = spyOn(TeamsClient.prototype, 'getRegion').mockReturnValue('emea')
34
+
32
35
  credManagerLoadConfigSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
33
36
 
34
37
  credManagerSaveConfigSpy = spyOn(TeamsCredentialManager.prototype, 'saveConfig').mockResolvedValue(undefined)
@@ -48,6 +51,7 @@ afterEach(() => {
48
51
  credManagerSaveConfigSpy?.mockRestore()
49
52
  credManagerClearCredentialsSpy?.mockRestore()
50
53
  credManagerIsTokenExpiredSpy?.mockRestore()
54
+ clientGetRegionSpy?.mockRestore()
51
55
  })
52
56
 
53
57
  test('extract: calls TeamsTokenExtractor', async () => {
@@ -59,7 +63,7 @@ test('extract: calls TeamsTokenExtractor', async () => {
59
63
  })
60
64
 
61
65
  test('extract: validates token with TeamsClient', async () => {
62
- const client = await new TeamsClient().login({ token: 'test-skype-token-123' })
66
+ const client = await new TeamsClient().login({ token: 'test-skype-token-123', region: 'emea' })
63
67
  const authInfo = await client.testAuth()
64
68
  expect(authInfo).toBeDefined()
65
69
  expect(authInfo.id).toBe('user-123')
@@ -67,7 +71,7 @@ test('extract: validates token with TeamsClient', async () => {
67
71
  })
68
72
 
69
73
  test('extract: discovers teams', async () => {
70
- const client = await new TeamsClient().login({ token: 'test-skype-token-123' })
74
+ const client = await new TeamsClient().login({ token: 'test-skype-token-123', region: 'emea' })
71
75
  const teams = await client.listTeams()
72
76
  expect(teams).toHaveLength(2)
73
77
  expect(teams[0].id).toBe('team-1')
@@ -16,7 +16,8 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
16
16
  return
17
17
  }
18
18
 
19
- const extractor = new TeamsTokenExtractor()
19
+ const debugLog = options.debug ? (msg: string) => debug(`[debug] ${msg}`) : undefined
20
+ const extractor = new TeamsTokenExtractor(undefined, undefined, debugLog)
20
21
 
21
22
  if (process.platform === 'darwin') {
22
23
  console.log('')
@@ -72,7 +73,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
72
73
  }
73
74
 
74
75
  try {
75
- const client = await new TeamsClient().login({ token })
76
+ const client = await new TeamsClient().login({ token, accountType })
76
77
  const authInfo = await client.testAuth()
77
78
  const teams = await client.listTeams()
78
79
 
@@ -88,6 +89,7 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
88
89
  const account: TeamsAccount = {
89
90
  token,
90
91
  token_expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
92
+ region: client.getRegion(),
91
93
  account_type: accountType,
92
94
  user_name: authInfo.displayName,
93
95
  current_team: teams[0]?.id ?? null,
@@ -191,6 +193,7 @@ async function extractManualToken(token: string, options: { pretty?: boolean; de
191
193
  const account: TeamsAccount = {
192
194
  token,
193
195
  token_expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
196
+ region: client.getRegion(),
194
197
  account_type: accountType,
195
198
  user_name: authInfo.displayName,
196
199
  current_team: teams[0].id,
@@ -295,6 +298,8 @@ export async function statusAction(options: { pretty?: boolean }): Promise<void>
295
298
  const client = await new TeamsClient().login({
296
299
  token: account.token,
297
300
  tokenExpiresAt: account.token_expires_at ?? undefined,
301
+ accountType: account.account_type,
302
+ region: account.region,
298
303
  })
299
304
  const authInfo = await client.testAuth()
300
305
  displayName = authInfo.displayName
@@ -67,7 +67,7 @@ afterEach(() => {
67
67
 
68
68
  test('list: returns channels from team', async () => {
69
69
  // given
70
- const client = await new TeamsClient().login({ token: 'test-token' })
70
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
71
71
 
72
72
  // when
73
73
  const channels = await client.listChannels('team-1')
@@ -80,7 +80,7 @@ test('list: returns channels from team', async () => {
80
80
 
81
81
  test('list: includes channel metadata', async () => {
82
82
  // given
83
- const client = await new TeamsClient().login({ token: 'test-token' })
83
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
84
84
  const channels = await client.listChannels('team-1')
85
85
 
86
86
  // when
@@ -95,7 +95,7 @@ test('list: includes channel metadata', async () => {
95
95
 
96
96
  test('info: returns channel details', async () => {
97
97
  // given
98
- const client = await new TeamsClient().login({ token: 'test-token' })
98
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
99
99
 
100
100
  // when
101
101
  const channel = await client.getChannel('team-1', 'ch-1')
@@ -108,7 +108,7 @@ test('info: returns channel details', async () => {
108
108
 
109
109
  test('info: throws error for non-existent channel', async () => {
110
110
  // given
111
- const client = await new TeamsClient().login({ token: 'test-token' })
111
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
112
112
 
113
113
  // when/then
114
114
  try {
@@ -121,7 +121,7 @@ test('info: throws error for non-existent channel', async () => {
121
121
 
122
122
  test('history: returns messages', async () => {
123
123
  // given
124
- const client = await new TeamsClient().login({ token: 'test-token' })
124
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
125
125
 
126
126
  // when
127
127
  const messages = await client.getMessages('team-1', 'ch-1', 50)
@@ -136,7 +136,7 @@ test('history: returns messages', async () => {
136
136
 
137
137
  test('history: includes message metadata', async () => {
138
138
  // given
139
- const client = await new TeamsClient().login({ token: 'test-token' })
139
+ const client = await new TeamsClient().login({ token: 'test-token', region: 'emea' })
140
140
  const messages = await client.getMessages('team-1', 'ch-1', 50)
141
141
 
142
142
  // when