agent-messenger 2.28.0 → 2.29.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 (159) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +29 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/discord/client.d.ts +6 -2
  5. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  6. package/dist/src/platforms/discord/client.js +10 -3
  7. package/dist/src/platforms/discord/client.js.map +1 -1
  8. package/dist/src/platforms/instagram/client.d.ts +24 -2
  9. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  10. package/dist/src/platforms/instagram/client.js +343 -53
  11. package/dist/src/platforms/instagram/client.js.map +1 -1
  12. package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
  13. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  14. package/dist/src/platforms/instagram/commands/auth.js +123 -11
  15. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  16. package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
  17. package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
  18. package/dist/src/platforms/instagram/credential-manager.js +17 -1
  19. package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
  20. package/dist/src/platforms/instagram/types.d.ts +10 -8
  21. package/dist/src/platforms/instagram/types.d.ts.map +1 -1
  22. package/dist/src/platforms/instagram/types.js.map +1 -1
  23. package/dist/src/platforms/teams/app-config.d.ts +36 -0
  24. package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
  25. package/dist/src/platforms/teams/app-config.js +69 -0
  26. package/dist/src/platforms/teams/app-config.js.map +1 -0
  27. package/dist/src/platforms/teams/client.d.ts +20 -2
  28. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  29. package/dist/src/platforms/teams/client.js +311 -7
  30. package/dist/src/platforms/teams/client.js.map +1 -1
  31. package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
  32. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  33. package/dist/src/platforms/teams/commands/auth.js +137 -3
  34. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  35. package/dist/src/platforms/teams/commands/file.d.ts +3 -0
  36. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
  37. package/dist/src/platforms/teams/commands/file.js +59 -2
  38. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  39. package/dist/src/platforms/teams/commands/message.d.ts +11 -0
  40. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  41. package/dist/src/platforms/teams/commands/message.js +121 -2
  42. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  43. package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
  44. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  45. package/dist/src/platforms/teams/credential-manager.js +46 -2
  46. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  47. package/dist/src/platforms/teams/device-code.d.ts +48 -0
  48. package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
  49. package/dist/src/platforms/teams/device-code.js +199 -0
  50. package/dist/src/platforms/teams/device-code.js.map +1 -0
  51. package/dist/src/platforms/teams/device-login.d.ts +37 -0
  52. package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
  53. package/dist/src/platforms/teams/device-login.js +129 -0
  54. package/dist/src/platforms/teams/device-login.js.map +1 -0
  55. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  56. package/dist/src/platforms/teams/ensure-auth.js +12 -0
  57. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  58. package/dist/src/platforms/teams/index.d.ts +7 -3
  59. package/dist/src/platforms/teams/index.d.ts.map +1 -1
  60. package/dist/src/platforms/teams/index.js +5 -2
  61. package/dist/src/platforms/teams/index.js.map +1 -1
  62. package/dist/src/platforms/teams/listener.d.ts +40 -0
  63. package/dist/src/platforms/teams/listener.d.ts.map +1 -0
  64. package/dist/src/platforms/teams/listener.js +286 -0
  65. package/dist/src/platforms/teams/listener.js.map +1 -0
  66. package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
  67. package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
  68. package/dist/src/platforms/teams/realm-discovery.js +33 -0
  69. package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
  70. package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
  71. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  72. package/dist/src/platforms/teams/token-extractor.js +60 -0
  73. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  74. package/dist/src/platforms/teams/token-provider.d.ts +25 -0
  75. package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
  76. package/dist/src/platforms/teams/token-provider.js +190 -0
  77. package/dist/src/platforms/teams/token-provider.js.map +1 -0
  78. package/dist/src/platforms/teams/trouter.d.ts +38 -0
  79. package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
  80. package/dist/src/platforms/teams/trouter.js +266 -0
  81. package/dist/src/platforms/teams/trouter.js.map +1 -0
  82. package/dist/src/platforms/teams/types.d.ts +103 -0
  83. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  84. package/dist/src/platforms/teams/types.js +30 -0
  85. package/dist/src/platforms/teams/types.js.map +1 -1
  86. package/dist/src/platforms/webex/types.d.ts +1 -1
  87. package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
  88. package/dist/src/tui/adapters/instagram-adapter.js +16 -0
  89. package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
  90. package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
  91. package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
  92. package/dist/src/tui/adapters/teams-adapter.js +21 -0
  93. package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
  94. package/dist/src/tui/app.js +3 -3
  95. package/docs/content/docs/cli/instagram.mdx +1 -0
  96. package/docs/content/docs/cli/teams.mdx +36 -0
  97. package/docs/content/docs/sdk/teams.mdx +30 -0
  98. package/package.json +1 -1
  99. package/skills/agent-channeltalk/SKILL.md +1 -1
  100. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  101. package/skills/agent-discord/SKILL.md +1 -1
  102. package/skills/agent-discordbot/SKILL.md +1 -1
  103. package/skills/agent-imessage/SKILL.md +1 -1
  104. package/skills/agent-instagram/SKILL.md +1 -1
  105. package/skills/agent-instagram/references/authentication.md +11 -0
  106. package/skills/agent-kakaotalk/SKILL.md +1 -1
  107. package/skills/agent-line/SKILL.md +1 -1
  108. package/skills/agent-slack/SKILL.md +1 -1
  109. package/skills/agent-slackbot/SKILL.md +1 -1
  110. package/skills/agent-teams/SKILL.md +82 -6
  111. package/skills/agent-teams/references/authentication.md +31 -1
  112. package/skills/agent-telegram/SKILL.md +1 -1
  113. package/skills/agent-telegrambot/SKILL.md +1 -1
  114. package/skills/agent-webex/SKILL.md +1 -1
  115. package/skills/agent-webexbot/SKILL.md +1 -1
  116. package/skills/agent-wechatbot/SKILL.md +1 -1
  117. package/skills/agent-whatsapp/SKILL.md +1 -1
  118. package/skills/agent-whatsappbot/SKILL.md +1 -1
  119. package/src/platforms/discord/client.test.ts +86 -0
  120. package/src/platforms/discord/client.ts +10 -3
  121. package/src/platforms/instagram/client.test.ts +478 -33
  122. package/src/platforms/instagram/client.ts +449 -68
  123. package/src/platforms/instagram/commands/auth.test.ts +177 -1
  124. package/src/platforms/instagram/commands/auth.ts +182 -11
  125. package/src/platforms/instagram/credential-manager.test.ts +30 -0
  126. package/src/platforms/instagram/credential-manager.ts +23 -1
  127. package/src/platforms/instagram/types.ts +11 -8
  128. package/src/platforms/teams/app-config.ts +92 -0
  129. package/src/platforms/teams/client.test.ts +335 -1
  130. package/src/platforms/teams/client.ts +370 -7
  131. package/src/platforms/teams/commands/auth.test.ts +93 -1
  132. package/src/platforms/teams/commands/auth.ts +182 -3
  133. package/src/platforms/teams/commands/file.test.ts +92 -1
  134. package/src/platforms/teams/commands/file.ts +76 -1
  135. package/src/platforms/teams/commands/message.test.ts +95 -1
  136. package/src/platforms/teams/commands/message.ts +145 -3
  137. package/src/platforms/teams/credential-manager.test.ts +20 -0
  138. package/src/platforms/teams/credential-manager.ts +68 -3
  139. package/src/platforms/teams/device-code.test.ts +311 -0
  140. package/src/platforms/teams/device-code.ts +277 -0
  141. package/src/platforms/teams/device-login.test.ts +288 -0
  142. package/src/platforms/teams/device-login.ts +206 -0
  143. package/src/platforms/teams/ensure-auth.ts +11 -0
  144. package/src/platforms/teams/index.test.ts +10 -0
  145. package/src/platforms/teams/index.ts +17 -1
  146. package/src/platforms/teams/listener.test.ts +243 -0
  147. package/src/platforms/teams/listener.ts +351 -0
  148. package/src/platforms/teams/realm-discovery.test.ts +73 -0
  149. package/src/platforms/teams/realm-discovery.ts +42 -0
  150. package/src/platforms/teams/token-extractor.ts +69 -0
  151. package/src/platforms/teams/token-provider.test.ts +227 -0
  152. package/src/platforms/teams/token-provider.ts +284 -0
  153. package/src/platforms/teams/trouter.test.ts +174 -0
  154. package/src/platforms/teams/trouter.ts +323 -0
  155. package/src/platforms/teams/types.test.ts +17 -0
  156. package/src/platforms/teams/types.ts +96 -0
  157. package/src/tui/adapters/instagram-adapter.ts +13 -0
  158. package/src/tui/adapters/teams-adapter.ts +23 -0
  159. package/src/tui/app.ts +3 -3
@@ -0,0 +1,69 @@
1
+ export const TEAMS_DESKTOP_CLIENT_ID = '1fec8e78-bce4-4aaf-ab1b-5451cc387264';
2
+ export const TEAMS_WEB_CLIENT_ID = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346';
3
+ export const CONSUMER_TENANT_ID = '9188040d-6c67-4c5b-b112-36a304b66dad';
4
+ // The Microsoft Services (MSA passthrough) tenant. Consumer/MSA tokens for the
5
+ // Skype resource carry this `tid`, so it is a placeholder that still requires
6
+ // tenant discovery before the skype-token exchange.
7
+ export const MICROSOFT_SERVICES_TENANT_ID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a';
8
+ export const WORK_TENANT_ID = 'organizations';
9
+ export const DEVICE_CODE_SCOPE_TEAMS = 'service::api.fl.teams.microsoft.com::MBI_SSL openid profile offline_access';
10
+ export const DEVICE_CODE_SCOPE_SKYPE = 'service::api.fl.spaces.skype.com::MBI_SSL openid profile offline_access';
11
+ export const WORK_DEVICE_CODE_SCOPE_TEAMS = 'https://api.spaces.skype.com/.default openid profile offline_access';
12
+ export const WORK_DEVICE_CODE_SCOPE_SKYPE = 'https://api.spaces.skype.com/.default openid profile offline_access';
13
+ export const SUBSTRATE_SEARCH_URL = 'https://substrate.office.com/searchservice/api/v2/query';
14
+ export const TEAMS_WEB_ORIGIN = 'https://teams.microsoft.com';
15
+ export const AAD_SCOPE_SUBSTRATE = 'https://substrate.office.com/.default offline_access';
16
+ export const AAD_SCOPE_GRAPH = 'https://graph.microsoft.com/.default offline_access';
17
+ export const AAD_AUDIENCE_SUBSTRATE = 'https://substrate.office.com';
18
+ export const AAD_AUDIENCE_GRAPH = 'https://graph.microsoft.com';
19
+ export const AUTHZ_CONSUMER_URL = 'https://teams.live.com/api/auth/v1.0/authz/consumer';
20
+ export const AUTHZ_WORK_URL = 'https://teams.microsoft.com/api/authsvc/v1.0/authz';
21
+ // Control-plane tenant-discovery endpoint. The region segment is not
22
+ // authoritative for this call (it lists every tenant the user belongs to
23
+ // regardless of home region), so we pin the value fossteams uses.
24
+ export const TEAMS_TENANTS_URL = 'https://teams.microsoft.com/api/mt/emea/beta/users/tenants';
25
+ export const GET_CREDENTIAL_TYPE_URL = 'https://login.microsoftonline.com/common/GetCredentialType';
26
+ export function consumerDeviceCodeUrl() {
27
+ return `https://login.microsoftonline.com/${CONSUMER_TENANT_ID}/oauth2/v2.0/devicecode`;
28
+ }
29
+ export function consumerTokenUrl() {
30
+ return `https://login.microsoftonline.com/${CONSUMER_TENANT_ID}/oauth2/v2.0/token`;
31
+ }
32
+ export function organizationsDeviceCodeUrl() {
33
+ return `https://login.microsoftonline.com/${WORK_TENANT_ID}/oauth2/v2.0/devicecode`;
34
+ }
35
+ export function organizationsTokenUrl() {
36
+ return `https://login.microsoftonline.com/${WORK_TENANT_ID}/oauth2/v2.0/token`;
37
+ }
38
+ export function tenantTokenUrl(tenantId) {
39
+ return `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
40
+ }
41
+ export function deviceCodeUrl(accountType) {
42
+ return accountType === 'personal' ? consumerDeviceCodeUrl() : organizationsDeviceCodeUrl();
43
+ }
44
+ export function deviceCodeTokenUrl(accountType, tenantId) {
45
+ if (tenantId)
46
+ return tenantTokenUrl(tenantId);
47
+ return accountType === 'personal' ? consumerTokenUrl() : organizationsTokenUrl();
48
+ }
49
+ export function deviceCodeTeamsScope(accountType) {
50
+ return accountType === 'personal' ? DEVICE_CODE_SCOPE_TEAMS : WORK_DEVICE_CODE_SCOPE_TEAMS;
51
+ }
52
+ export function deviceCodeSkypeScope(accountType) {
53
+ return accountType === 'personal' ? DEVICE_CODE_SCOPE_SKYPE : WORK_DEVICE_CODE_SCOPE_SKYPE;
54
+ }
55
+ export function authzUrl(accountType) {
56
+ return accountType === 'personal' ? AUTHZ_CONSUMER_URL : AUTHZ_WORK_URL;
57
+ }
58
+ function parseTrimmed(value) {
59
+ const normalized = value?.trim();
60
+ return normalized ? normalized : undefined;
61
+ }
62
+ export function getTeamsAppClientId(accountType = 'personal') {
63
+ const envClientId = parseTrimmed(process.env.AGENT_TEAMS_CLIENT_ID);
64
+ if (envClientId) {
65
+ return { clientId: envClientId, source: 'env' };
66
+ }
67
+ return { clientId: accountType === 'work' ? TEAMS_DESKTOP_CLIENT_ID : TEAMS_WEB_CLIENT_ID, source: 'builtin' };
68
+ }
69
+ //# sourceMappingURL=app-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-config.js","sourceRoot":"","sources":["../../../../src/platforms/teams/app-config.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,16 @@
1
- import type { TeamsAccountType, TeamsChannel, TeamsChat, TeamsFile, TeamsMessage, TeamsRegion, TeamsTeam, TeamsUser } from './types.js';
1
+ import { TeamsCredentialManager } from './credential-manager.js';
2
+ import type { TeamsAccountType, TeamsChannel, TeamsChat, TeamsFile, TeamsMessage, TeamsRegion, TeamsSearchResult, TeamsTeam, TeamsUser } from './types.js';
2
3
  export declare class TeamsClient {
4
+ private credManager;
3
5
  private token;
4
6
  private tokenExpiresAt?;
5
7
  private isPersonalAccount;
6
8
  private region;
7
9
  private regionDiscovered;
10
+ private tokenProvider?;
8
11
  private buckets;
9
12
  private globalRateLimitUntil;
13
+ constructor(credManager?: TeamsCredentialManager);
10
14
  login(credentials?: {
11
15
  token: string;
12
16
  tokenExpiresAt?: string;
@@ -14,6 +18,9 @@ export declare class TeamsClient {
14
18
  region?: TeamsRegion;
15
19
  }): Promise<this>;
16
20
  getRegion(): TeamsRegion;
21
+ getToken(): string;
22
+ getAccountType(): TeamsAccountType;
23
+ getIdToken(): Promise<string | null>;
17
24
  private ensureAuth;
18
25
  private isTokenExpired;
19
26
  private getBucketKey;
@@ -27,14 +34,21 @@ export declare class TeamsClient {
27
34
  private requestFormData;
28
35
  testAuth(): Promise<TeamsUser>;
29
36
  listTeams(): Promise<TeamsTeam[]>;
37
+ buildChannelTeamMap(): Promise<Map<string, string>>;
30
38
  listChats(): Promise<TeamsChat[]>;
31
39
  getChatMessages(chatId: string, limit?: number): Promise<TeamsMessage[]>;
32
40
  sendChatMessage(chatId: string, content: string): Promise<TeamsMessage>;
33
41
  getTeam(teamId: string): Promise<TeamsTeam>;
34
42
  listChannels(teamId: string): Promise<TeamsChannel[]>;
35
43
  getChannel(teamId: string, channelId: string): Promise<TeamsChannel>;
36
- sendMessage(teamId: string, channelId: string, content: string): Promise<TeamsMessage>;
44
+ sendMessage(teamId: string, channelId: string, content: string, rootMessageId?: string): Promise<TeamsMessage>;
37
45
  getMessages(teamId: string, channelId: string, limit?: number): Promise<TeamsMessage[]>;
46
+ getThreadReplies(teamId: string, channelId: string, rootMessageId: string, limit?: number): Promise<TeamsMessage[]>;
47
+ searchMessages(query: string, opts?: {
48
+ limit?: number;
49
+ from?: number;
50
+ }): Promise<TeamsSearchResult[]>;
51
+ private getTokenProvider;
38
52
  getMessage(teamId: string, channelId: string, messageId: string): Promise<TeamsMessage>;
39
53
  deleteMessage(teamId: string, channelId: string, messageId: string): Promise<void>;
40
54
  addReaction(teamId: string, channelId: string, messageId: string, emoji: string): Promise<void>;
@@ -43,5 +57,9 @@ export declare class TeamsClient {
43
57
  getUser(userId: string): Promise<TeamsUser>;
44
58
  uploadFile(teamId: string, channelId: string, filePath: string): Promise<TeamsFile>;
45
59
  listFiles(teamId: string, channelId: string): Promise<TeamsFile[]>;
60
+ downloadFile(teamId: string, channelId: string, fileId: string): Promise<{
61
+ buffer: Buffer;
62
+ file: TeamsFile;
63
+ }>;
46
64
  }
47
65
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/platforms/teams/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/platforms/teams/client.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { readFile } from 'node:fs/promises';
2
3
  import { basename } from 'node:path';
4
+ import { SUBSTRATE_SEARCH_URL } from './app-config.js';
3
5
  import { TeamsCredentialManager } from './credential-manager.js';
6
+ import { TeamsTokenProvider } from './token-provider.js';
4
7
  import { TeamsError } from './types.js';
5
8
  const PERSONAL_MSG_API_BASE = 'https://msgapi.teams.live.com/v1';
6
9
  const CSA_API_BASE = 'https://teams.microsoft.com/api';
@@ -8,6 +11,20 @@ const MAX_RETRIES = 3;
8
11
  const BASE_BACKOFF_MS = 100;
9
12
  const DEFAULT_REGION = 'amer';
10
13
  const REGIONS = ['amer', 'emea', 'apac'];
14
+ const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0';
15
+ // Personal (Teams for Life) skypetokens carry a consumer `skypeid` (e.g.
16
+ // "live:..." or "8:live:..."); work/school tokens carry an org identity. Used
17
+ // only to guess the account type when a caller logs in with a bare token.
18
+ function isPersonalToken(token) {
19
+ try {
20
+ const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
21
+ const skypeId = payload.skypeid ?? '';
22
+ return skypeId.includes('live:') || skypeId.startsWith('8:live:');
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
11
28
  function stripHtml(content) {
12
29
  if (content === undefined)
13
30
  return undefined;
@@ -21,6 +38,162 @@ function stripHtml(content) {
21
38
  .replace(/\s+/g, ' ')
22
39
  .trim();
23
40
  }
41
+ function isRecord(value) {
42
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
43
+ }
44
+ function stringFrom(record, keys) {
45
+ for (const key of keys) {
46
+ const value = record[key];
47
+ if (typeof value === 'string' && value.length > 0)
48
+ return value;
49
+ }
50
+ return undefined;
51
+ }
52
+ function recordFrom(record, keys) {
53
+ for (const key of keys) {
54
+ const value = record[key];
55
+ if (isRecord(value))
56
+ return value;
57
+ }
58
+ return undefined;
59
+ }
60
+ function arrayFrom(record, keys) {
61
+ for (const key of keys) {
62
+ const value = record[key];
63
+ if (Array.isArray(value))
64
+ return value;
65
+ }
66
+ return [];
67
+ }
68
+ function propertyValue(record, names) {
69
+ const properties = arrayFrom(record, ['Properties', 'properties']);
70
+ const normalized = names.map((name) => name.toLowerCase());
71
+ for (const property of properties) {
72
+ if (!isRecord(property))
73
+ continue;
74
+ const name = stringFrom(property, ['Name', 'name', 'Key', 'key']);
75
+ if (!name || !normalized.includes(name.toLowerCase()))
76
+ continue;
77
+ const value = property.Value ?? property.value;
78
+ if (typeof value === 'string' && value.length > 0)
79
+ return value;
80
+ }
81
+ return undefined;
82
+ }
83
+ function resultString(record, keys) {
84
+ return stringFrom(record, keys) ?? propertyValue(record, keys);
85
+ }
86
+ function parseSubstrateResult(value) {
87
+ if (!isRecord(value))
88
+ return null;
89
+ const author = recordFrom(value, ['Author', 'author', 'From', 'from']);
90
+ const id = resultString(value, ['id', 'Id', 'ReferenceId', 'MessageId']);
91
+ const channelId = resultString(value, ['channel_id', 'ChannelId', 'ConversationId', 'ThreadId']);
92
+ if (!id || !channelId)
93
+ return null;
94
+ return {
95
+ id,
96
+ content: stripHtml(resultString(value, ['content', 'Content', 'HitHighlightedSummary', 'Summary', 'Preview'])) ?? '',
97
+ author: {
98
+ id: author ? (stringFrom(author, ['id', 'Id', 'ObjectId']) ?? '') : (propertyValue(value, ['AuthorId']) ?? ''),
99
+ displayName: author
100
+ ? (stringFrom(author, ['displayName', 'DisplayName', 'Name']) ?? 'Unknown')
101
+ : (propertyValue(value, ['AuthorDisplayName', 'Author']) ?? 'Unknown'),
102
+ },
103
+ channel_id: channelId,
104
+ thread_id: resultString(value, ['thread_id', 'ThreadId']),
105
+ team_name: resultString(value, ['team_name', 'TeamName']),
106
+ channel_name: resultString(value, ['channel_name', 'ChannelName']),
107
+ timestamp: resultString(value, ['timestamp', 'Timestamp', 'DateTimeSent', 'LastModifiedTime']) ?? '',
108
+ permalink: resultString(value, ['permalink', 'Permalink', 'WebUrl', 'Url']),
109
+ };
110
+ }
111
+ function parseSubstrateResults(data) {
112
+ if (!isRecord(data))
113
+ return [];
114
+ const results = [];
115
+ for (const entitySet of arrayFrom(data, ['EntitySets', 'entitySets'])) {
116
+ if (!isRecord(entitySet))
117
+ continue;
118
+ for (const resultSet of arrayFrom(entitySet, ['ResultSets', 'resultSets'])) {
119
+ if (!isRecord(resultSet))
120
+ continue;
121
+ for (const rawResult of arrayFrom(resultSet, ['Results', 'results'])) {
122
+ const result = parseSubstrateResult(rawResult);
123
+ if (result)
124
+ results.push(result);
125
+ }
126
+ }
127
+ }
128
+ return results;
129
+ }
130
+ function validateSearchLimit(value) {
131
+ if (value === undefined)
132
+ return 20;
133
+ if (!Number.isInteger(value) || value < 1) {
134
+ throw new TeamsError('Search limit must be a positive integer.', 'invalid_pagination');
135
+ }
136
+ return value;
137
+ }
138
+ function validateSearchFrom(value) {
139
+ if (value === undefined)
140
+ return 0;
141
+ if (!Number.isInteger(value) || value < 0) {
142
+ throw new TeamsError('Search from offset must be a non-negative integer.', 'invalid_pagination');
143
+ }
144
+ return value;
145
+ }
146
+ function isSharePointOrOneDriveUrl(url) {
147
+ try {
148
+ const { hostname } = new URL(url);
149
+ const normalizedHost = hostname.toLowerCase();
150
+ return (normalizedHost.includes('sharepoint.com') ||
151
+ normalizedHost.includes('-my.sharepoint') ||
152
+ normalizedHost === '1drv.ms' ||
153
+ normalizedHost.endsWith('.1drv.ms') ||
154
+ normalizedHost === 'onedrive.live.com' ||
155
+ normalizedHost.endsWith('.onedrive.live.com'));
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ }
161
+ // Only these hosts receive the Skype token on a raw download fetch. Teams file
162
+ // metadata can carry arbitrary URLs, so we never attach credentials to a host
163
+ // outside this allowlist — that would leak the token to a third party.
164
+ function isTrustedSkypeDownloadHost(url) {
165
+ try {
166
+ const host = new URL(url).hostname.toLowerCase();
167
+ return (host === 'teams.microsoft.com' ||
168
+ host.endsWith('.teams.microsoft.com') ||
169
+ host.endsWith('.asm.skype.com') ||
170
+ host.endsWith('.asyncgw.teams.microsoft.com') ||
171
+ host === 'substrate.office.com' ||
172
+ host.endsWith('.substrate.office.com'));
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }
178
+ function getFileDownloadSource(file) {
179
+ const shareUrl = [file.sharepoint_url, file.url, file.object_url].find((candidate) => Boolean(candidate && isSharePointOrOneDriveUrl(candidate)));
180
+ if (shareUrl)
181
+ return { route: 'graph', url: shareUrl };
182
+ const directUrl = file.object_url ?? file.url;
183
+ if (!directUrl) {
184
+ throw new TeamsError(`File has no downloadable URL: ${file.id}`, 'file_url_missing');
185
+ }
186
+ if (!isTrustedSkypeDownloadHost(directUrl)) {
187
+ throw new TeamsError(`Refusing to download ${file.id} from an untrusted host: ${directUrl}`, 'file_url_untrusted');
188
+ }
189
+ return { route: 'skype', url: directUrl };
190
+ }
191
+ async function readDownloadResponse(response, codePrefix) {
192
+ if (!response.ok) {
193
+ throw new TeamsError(`File download failed with HTTP ${response.status}`, `${codePrefix}_${response.status}`);
194
+ }
195
+ return Buffer.from(await response.arrayBuffer());
196
+ }
24
197
  function escapeHtml(value) {
25
198
  return value
26
199
  .replaceAll('&', '&amp;')
@@ -29,6 +202,19 @@ function escapeHtml(value) {
29
202
  .replaceAll('"', '&quot;')
30
203
  .replaceAll("'", '&#39;');
31
204
  }
205
+ function withThreadMetadata(message, rootMessageId) {
206
+ const { rootMessageId: messageRootMessageId, parentMessageId, ...teamsMessage } = message;
207
+ const rawRootMessageId = rootMessageId ?? messageRootMessageId;
208
+ const isThreadReply = Boolean(rootMessageId ||
209
+ (messageRootMessageId !== undefined && messageRootMessageId !== message.id) ||
210
+ (parentMessageId !== undefined && parentMessageId !== message.id));
211
+ return {
212
+ ...teamsMessage,
213
+ root_message_id: isThreadReply ? rawRootMessageId : undefined,
214
+ parent_message_id: isThreadReply ? (parentMessageId ?? rawRootMessageId) : undefined,
215
+ is_thread_reply: isThreadReply ? true : undefined,
216
+ };
217
+ }
32
218
  // groupId => Teams/channel thread (handled by listTeams). "48:notes"/
33
219
  // streamofnotes => the user's self ("to me") chat. Anything else without a
34
220
  // non-chat threadType is a normal 1:1 (no topic) or group (has topic) chat.
@@ -42,13 +228,18 @@ function classifyChat(id, tp) {
42
228
  return tp?.topic ? 'group' : 'oneOnOne';
43
229
  }
44
230
  export class TeamsClient {
231
+ credManager;
45
232
  token = null;
46
233
  tokenExpiresAt;
47
234
  isPersonalAccount = false;
48
235
  region = DEFAULT_REGION;
49
236
  regionDiscovered = false;
237
+ tokenProvider;
50
238
  buckets = new Map();
51
239
  globalRateLimitUntil = 0;
240
+ constructor(credManager = new TeamsCredentialManager()) {
241
+ this.credManager = credManager;
242
+ }
52
243
  async login(credentials) {
53
244
  if (credentials) {
54
245
  if (!credentials.token) {
@@ -58,17 +249,21 @@ export class TeamsClient {
58
249
  if (credentials.tokenExpiresAt) {
59
250
  this.tokenExpiresAt = new Date(credentials.tokenExpiresAt);
60
251
  }
61
- this.isPersonalAccount = credentials.accountType === 'personal';
252
+ this.isPersonalAccount = credentials.accountType
253
+ ? credentials.accountType === 'personal'
254
+ : isPersonalToken(credentials.token);
62
255
  if (credentials.region) {
63
256
  this.region = credentials.region;
64
257
  this.regionDiscovered = true;
65
258
  }
259
+ if (credentials.accountType) {
260
+ this.getTokenProvider().bindAccount(credentials.accountType);
261
+ }
66
262
  return this;
67
263
  }
68
264
  const { ensureTeamsAuth } = await import('./ensure-auth.js');
69
265
  await ensureTeamsAuth();
70
- const credManager = new TeamsCredentialManager();
71
- const creds = await credManager.getTokenWithExpiry();
266
+ const creds = await this.credManager.getTokenWithExpiry();
72
267
  if (!creds) {
73
268
  throw new TeamsError('No Teams credentials found. Make sure Microsoft Teams is logged in via the desktop app or a supported Chromium browser.', 'no_credentials');
74
269
  }
@@ -82,6 +277,17 @@ export class TeamsClient {
82
277
  getRegion() {
83
278
  return this.region;
84
279
  }
280
+ getToken() {
281
+ return this.ensureAuth();
282
+ }
283
+ getAccountType() {
284
+ return this.isPersonalAccount ? 'personal' : 'work';
285
+ }
286
+ async getIdToken() {
287
+ const { TeamsTokenExtractor } = await import('./token-extractor.js');
288
+ const extractor = new TeamsTokenExtractor();
289
+ return extractor.extractIdToken(this.getAccountType());
290
+ }
85
291
  ensureAuth() {
86
292
  if (this.token === null) {
87
293
  throw new TeamsError('Not authenticated. Call .login() first.', 'not_authenticated');
@@ -162,7 +368,7 @@ export class TeamsClient {
162
368
  }
163
369
  async request(method, path, body, baseUrl) {
164
370
  if (this.isTokenExpired()) {
165
- throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired');
371
+ throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired');
166
372
  }
167
373
  if (baseUrl === undefined && !this.regionDiscovered) {
168
374
  await this.discoverRegion();
@@ -209,7 +415,7 @@ export class TeamsClient {
209
415
  }
210
416
  async requestFormData(path, formData, baseUrl) {
211
417
  if (this.isTokenExpired()) {
212
- throw new TeamsError('Token has expired. Run "auth extract" to refresh.', 'token_expired');
418
+ throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired');
213
419
  }
214
420
  if (baseUrl === undefined && !this.regionDiscovered) {
215
421
  await this.discoverRegion();
@@ -257,6 +463,22 @@ export class TeamsClient {
257
463
  }
258
464
  return Array.from(teamsMap.values());
259
465
  }
466
+ // Realtime messages only carry a conversation id; a channel's parent teamId
467
+ // (== groupId) lives on the conversation, so the listener resolves it through
468
+ // this channelId -> teamId map.
469
+ async buildChannelTeamMap() {
470
+ const data = await this.request('GET', '/users/ME/conversations');
471
+ const channelToTeam = new Map();
472
+ for (const conv of data.conversations ?? []) {
473
+ const tp = conv.threadProperties;
474
+ if (!tp?.groupId)
475
+ continue;
476
+ if (!tp.productThreadType?.includes('Teams') && tp.threadType !== 'space')
477
+ continue;
478
+ channelToTeam.set(conv.id, tp.groupId);
479
+ }
480
+ return channelToTeam;
481
+ }
260
482
  async listChats() {
261
483
  const data = await this.request('GET', '/users/ME/conversations?view=msnp24Equivalent&pageSize=500');
262
484
  const chats = [];
@@ -317,11 +539,62 @@ export class TeamsClient {
317
539
  async getChannel(teamId, channelId) {
318
540
  return this.request('GET', `/csa/api/v1/teams/${teamId}/channels/${channelId}`, undefined, CSA_API_BASE);
319
541
  }
320
- async sendMessage(teamId, channelId, content) {
542
+ async sendMessage(teamId, channelId, content, rootMessageId) {
543
+ if (rootMessageId) {
544
+ const response = await this.request('POST', `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies`, { content, parentMessageId: rootMessageId }, CSA_API_BASE);
545
+ return withThreadMetadata(response, rootMessageId);
546
+ }
321
547
  return this.request('POST', `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages`, { content }, CSA_API_BASE);
322
548
  }
323
549
  async getMessages(teamId, channelId, limit = 50) {
324
- return this.request('GET', `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages?limit=${limit}`, undefined, CSA_API_BASE);
550
+ const messages = await this.request('GET', `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages?limit=${limit}`, undefined, CSA_API_BASE);
551
+ return messages.map((message) => withThreadMetadata(message));
552
+ }
553
+ async getThreadReplies(teamId, channelId, rootMessageId, limit = 50) {
554
+ const replies = await this.request('GET', `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${rootMessageId}/replies?limit=${limit}`, undefined, CSA_API_BASE);
555
+ return replies.map((reply) => withThreadMetadata(reply, rootMessageId));
556
+ }
557
+ async searchMessages(query, opts = {}) {
558
+ const size = validateSearchLimit(opts.limit);
559
+ const from = validateSearchFrom(opts.from);
560
+ const tokenProvider = this.getTokenProvider();
561
+ const substrateToken = await tokenProvider.getSubstrateToken();
562
+ const tenantId = await tokenProvider.getTenantId();
563
+ const userId = await tokenProvider.getUserId();
564
+ const response = await fetch(SUBSTRATE_SEARCH_URL, {
565
+ method: 'POST',
566
+ headers: {
567
+ Authorization: `Bearer ${substrateToken}`,
568
+ 'Content-Type': 'application/json',
569
+ 'x-anchormailbox': `Oid:${userId}@${tenantId}`,
570
+ },
571
+ body: JSON.stringify({
572
+ cvid: randomUUID(),
573
+ logicalId: randomUUID(),
574
+ query: { queryString: query },
575
+ entityRequests: [
576
+ {
577
+ entityType: 'Message',
578
+ contentSources: ['Teams'],
579
+ from,
580
+ size,
581
+ query: { queryString: query },
582
+ },
583
+ ],
584
+ }),
585
+ });
586
+ const data = (await response.json().catch(() => ({})));
587
+ if (!response.ok) {
588
+ const message = isRecord(data)
589
+ ? stringFrom(data, ['message', 'Message', 'error_description', 'error'])
590
+ : undefined;
591
+ throw new TeamsError(message ?? `HTTP ${response.status}`, `substrate_${response.status}`);
592
+ }
593
+ return parseSubstrateResults(data);
594
+ }
595
+ getTokenProvider() {
596
+ this.tokenProvider ??= new TeamsTokenProvider(this.credManager);
597
+ return this.tokenProvider;
325
598
  }
326
599
  async getMessage(teamId, channelId, messageId) {
327
600
  return this.request('GET', `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/messages/${messageId}`, undefined, CSA_API_BASE);
@@ -351,5 +624,36 @@ export class TeamsClient {
351
624
  async listFiles(teamId, channelId) {
352
625
  return this.request('GET', `/csa/${this.region}/api/v2/teams/${teamId}/channels/${channelId}/files`, undefined, CSA_API_BASE);
353
626
  }
627
+ async downloadFile(teamId, channelId, fileId) {
628
+ const files = await this.listFiles(teamId, channelId);
629
+ const file = files.find((candidate) => candidate.id === fileId);
630
+ if (!file) {
631
+ throw new TeamsError(`File not found: ${fileId}`, 'file_not_found');
632
+ }
633
+ const source = getFileDownloadSource(file);
634
+ if (source.route === 'graph') {
635
+ const graphToken = await new TeamsTokenProvider(this.credManager).getGraphToken();
636
+ const shareId = `u!${Buffer.from(source.url).toString('base64url').replace(/=+$/, '')}`;
637
+ const response = await fetch(`${GRAPH_API_BASE}/shares/${shareId}/driveItem/content`, {
638
+ headers: {
639
+ Authorization: `Bearer ${graphToken}`,
640
+ },
641
+ redirect: 'follow',
642
+ });
643
+ return { buffer: await readDownloadResponse(response, 'graph_download'), file };
644
+ }
645
+ if (this.isTokenExpired()) {
646
+ throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired');
647
+ }
648
+ const skypeToken = this.getToken();
649
+ const response = await fetch(source.url, {
650
+ headers: {
651
+ Authorization: `Bearer ${skypeToken}`,
652
+ 'X-Skypetoken': skypeToken,
653
+ },
654
+ redirect: 'follow',
655
+ });
656
+ return { buffer: await readDownloadResponse(response, 'skype_download'), file };
657
+ }
354
658
  }
355
659
  //# sourceMappingURL=client.js.map