agent-messenger 2.27.2 → 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.
Files changed (316) hide show
  1. package/.claude-plugin/README.md +11 -1
  2. package/.claude-plugin/marketplace.json +14 -1
  3. package/.claude-plugin/plugin.json +4 -2
  4. package/AGENTS.md +4 -0
  5. package/README.md +58 -10
  6. package/dist/package.json +10 -2
  7. package/dist/src/cli.d.ts.map +1 -1
  8. package/dist/src/cli.js +3 -0
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/platforms/discord/client.d.ts +6 -2
  11. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  12. package/dist/src/platforms/discord/client.js +10 -3
  13. package/dist/src/platforms/discord/client.js.map +1 -1
  14. package/dist/src/platforms/discordbot/cli.js +1 -1
  15. package/dist/src/platforms/imessage/cli.d.ts +5 -0
  16. package/dist/src/platforms/imessage/cli.d.ts.map +1 -0
  17. package/dist/src/platforms/imessage/cli.js +31 -0
  18. package/dist/src/platforms/imessage/cli.js.map +1 -0
  19. package/dist/src/platforms/imessage/client.d.ts +43 -0
  20. package/dist/src/platforms/imessage/client.d.ts.map +1 -0
  21. package/dist/src/platforms/imessage/client.js +190 -0
  22. package/dist/src/platforms/imessage/client.js.map +1 -0
  23. package/dist/src/platforms/imessage/commands/auth.d.ts +31 -0
  24. package/dist/src/platforms/imessage/commands/auth.d.ts.map +1 -0
  25. package/dist/src/platforms/imessage/commands/auth.js +100 -0
  26. package/dist/src/platforms/imessage/commands/auth.js.map +1 -0
  27. package/dist/src/platforms/imessage/commands/chat.d.ts +3 -0
  28. package/dist/src/platforms/imessage/commands/chat.d.ts.map +1 -0
  29. package/dist/src/platforms/imessage/commands/chat.js +42 -0
  30. package/dist/src/platforms/imessage/commands/chat.js.map +1 -0
  31. package/dist/src/platforms/imessage/commands/doctor.d.ts +25 -0
  32. package/dist/src/platforms/imessage/commands/doctor.d.ts.map +1 -0
  33. package/dist/src/platforms/imessage/commands/doctor.js +101 -0
  34. package/dist/src/platforms/imessage/commands/doctor.js.map +1 -0
  35. package/dist/src/platforms/imessage/commands/index.d.ts +7 -0
  36. package/dist/src/platforms/imessage/commands/index.d.ts.map +1 -0
  37. package/dist/src/platforms/imessage/commands/index.js +7 -0
  38. package/dist/src/platforms/imessage/commands/index.js.map +1 -0
  39. package/dist/src/platforms/imessage/commands/message.d.ts +5 -0
  40. package/dist/src/platforms/imessage/commands/message.d.ts.map +1 -0
  41. package/dist/src/platforms/imessage/commands/message.js +127 -0
  42. package/dist/src/platforms/imessage/commands/message.js.map +1 -0
  43. package/dist/src/platforms/imessage/commands/setup.d.ts +20 -0
  44. package/dist/src/platforms/imessage/commands/setup.d.ts.map +1 -0
  45. package/dist/src/platforms/imessage/commands/setup.js +57 -0
  46. package/dist/src/platforms/imessage/commands/setup.js.map +1 -0
  47. package/dist/src/platforms/imessage/commands/shared.d.ts +10 -0
  48. package/dist/src/platforms/imessage/commands/shared.d.ts.map +1 -0
  49. package/dist/src/platforms/imessage/commands/shared.js +53 -0
  50. package/dist/src/platforms/imessage/commands/shared.js.map +1 -0
  51. package/dist/src/platforms/imessage/commands/whoami.d.ts +3 -0
  52. package/dist/src/platforms/imessage/commands/whoami.d.ts.map +1 -0
  53. package/dist/src/platforms/imessage/commands/whoami.js +20 -0
  54. package/dist/src/platforms/imessage/commands/whoami.js.map +1 -0
  55. package/dist/src/platforms/imessage/credential-manager.d.ts +22 -0
  56. package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -0
  57. package/dist/src/platforms/imessage/credential-manager.js +111 -0
  58. package/dist/src/platforms/imessage/credential-manager.js.map +1 -0
  59. package/dist/src/platforms/imessage/ensure-auth.d.ts +2 -0
  60. package/dist/src/platforms/imessage/ensure-auth.d.ts.map +1 -0
  61. package/dist/src/platforms/imessage/ensure-auth.js +13 -0
  62. package/dist/src/platforms/imessage/ensure-auth.js.map +1 -0
  63. package/dist/src/platforms/imessage/errors.d.ts +3 -0
  64. package/dist/src/platforms/imessage/errors.d.ts.map +1 -0
  65. package/dist/src/platforms/imessage/errors.js +25 -0
  66. package/dist/src/platforms/imessage/errors.js.map +1 -0
  67. package/dist/src/platforms/imessage/index.d.ts +7 -0
  68. package/dist/src/platforms/imessage/index.d.ts.map +1 -0
  69. package/dist/src/platforms/imessage/index.js +5 -0
  70. package/dist/src/platforms/imessage/index.js.map +1 -0
  71. package/dist/src/platforms/imessage/rpc.d.ts +20 -0
  72. package/dist/src/platforms/imessage/rpc.d.ts.map +1 -0
  73. package/dist/src/platforms/imessage/rpc.js +133 -0
  74. package/dist/src/platforms/imessage/rpc.js.map +1 -0
  75. package/dist/src/platforms/imessage/types.d.ts +59 -0
  76. package/dist/src/platforms/imessage/types.d.ts.map +1 -0
  77. package/dist/src/platforms/imessage/types.js +29 -0
  78. package/dist/src/platforms/imessage/types.js.map +1 -0
  79. package/dist/src/platforms/instagram/cli.js +1 -1
  80. package/dist/src/platforms/instagram/client.d.ts +29 -2
  81. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  82. package/dist/src/platforms/instagram/client.js +360 -53
  83. package/dist/src/platforms/instagram/client.js.map +1 -1
  84. package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
  85. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  86. package/dist/src/platforms/instagram/commands/auth.js +123 -11
  87. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  88. package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
  89. package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
  90. package/dist/src/platforms/instagram/credential-manager.js +17 -1
  91. package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
  92. package/dist/src/platforms/instagram/hybrid-listener.d.ts +48 -0
  93. package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -0
  94. package/dist/src/platforms/instagram/hybrid-listener.js +154 -0
  95. package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -0
  96. package/dist/src/platforms/instagram/index.d.ts +2 -0
  97. package/dist/src/platforms/instagram/index.d.ts.map +1 -1
  98. package/dist/src/platforms/instagram/index.js +2 -0
  99. package/dist/src/platforms/instagram/index.js.map +1 -1
  100. package/dist/src/platforms/instagram/mqtt/connection.d.ts +4 -0
  101. package/dist/src/platforms/instagram/mqtt/connection.d.ts.map +1 -0
  102. package/dist/src/platforms/instagram/mqtt/connection.js +66 -0
  103. package/dist/src/platforms/instagram/mqtt/connection.js.map +1 -0
  104. package/dist/src/platforms/instagram/mqtt/thrift.d.ts +22 -0
  105. package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -0
  106. package/dist/src/platforms/instagram/mqtt/thrift.js +143 -0
  107. package/dist/src/platforms/instagram/mqtt/thrift.js.map +1 -0
  108. package/dist/src/platforms/instagram/mqtt/transport.d.ts +42 -0
  109. package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -0
  110. package/dist/src/platforms/instagram/mqtt/transport.js +246 -0
  111. package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -0
  112. package/dist/src/platforms/instagram/realtime-listener.d.ts +33 -0
  113. package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -0
  114. package/dist/src/platforms/instagram/realtime-listener.js +151 -0
  115. package/dist/src/platforms/instagram/realtime-listener.js.map +1 -0
  116. package/dist/src/platforms/instagram/types.d.ts +10 -8
  117. package/dist/src/platforms/instagram/types.d.ts.map +1 -1
  118. package/dist/src/platforms/instagram/types.js.map +1 -1
  119. package/dist/src/platforms/kakaotalk/cli.js +1 -1
  120. package/dist/src/platforms/line/cli.js +1 -1
  121. package/dist/src/platforms/teams/app-config.d.ts +36 -0
  122. package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
  123. package/dist/src/platforms/teams/app-config.js +69 -0
  124. package/dist/src/platforms/teams/app-config.js.map +1 -0
  125. package/dist/src/platforms/teams/client.d.ts +19 -2
  126. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  127. package/dist/src/platforms/teams/client.js +295 -7
  128. package/dist/src/platforms/teams/client.js.map +1 -1
  129. package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
  130. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  131. package/dist/src/platforms/teams/commands/auth.js +137 -3
  132. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  133. package/dist/src/platforms/teams/commands/file.d.ts +3 -0
  134. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
  135. package/dist/src/platforms/teams/commands/file.js +59 -2
  136. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  137. package/dist/src/platforms/teams/commands/message.d.ts +11 -0
  138. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  139. package/dist/src/platforms/teams/commands/message.js +121 -2
  140. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  141. package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
  142. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  143. package/dist/src/platforms/teams/credential-manager.js +46 -2
  144. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  145. package/dist/src/platforms/teams/device-code.d.ts +48 -0
  146. package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
  147. package/dist/src/platforms/teams/device-code.js +199 -0
  148. package/dist/src/platforms/teams/device-code.js.map +1 -0
  149. package/dist/src/platforms/teams/device-login.d.ts +37 -0
  150. package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
  151. package/dist/src/platforms/teams/device-login.js +129 -0
  152. package/dist/src/platforms/teams/device-login.js.map +1 -0
  153. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  154. package/dist/src/platforms/teams/ensure-auth.js +12 -0
  155. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  156. package/dist/src/platforms/teams/index.d.ts +7 -3
  157. package/dist/src/platforms/teams/index.d.ts.map +1 -1
  158. package/dist/src/platforms/teams/index.js +5 -2
  159. package/dist/src/platforms/teams/index.js.map +1 -1
  160. package/dist/src/platforms/teams/listener.d.ts +35 -0
  161. package/dist/src/platforms/teams/listener.d.ts.map +1 -0
  162. package/dist/src/platforms/teams/listener.js +236 -0
  163. package/dist/src/platforms/teams/listener.js.map +1 -0
  164. package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
  165. package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
  166. package/dist/src/platforms/teams/realm-discovery.js +33 -0
  167. package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
  168. package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
  169. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  170. package/dist/src/platforms/teams/token-extractor.js +60 -0
  171. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  172. package/dist/src/platforms/teams/token-provider.d.ts +25 -0
  173. package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
  174. package/dist/src/platforms/teams/token-provider.js +190 -0
  175. package/dist/src/platforms/teams/token-provider.js.map +1 -0
  176. package/dist/src/platforms/teams/trouter.d.ts +31 -0
  177. package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
  178. package/dist/src/platforms/teams/trouter.js +181 -0
  179. package/dist/src/platforms/teams/trouter.js.map +1 -0
  180. package/dist/src/platforms/teams/types.d.ts +94 -0
  181. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  182. package/dist/src/platforms/teams/types.js +30 -0
  183. package/dist/src/platforms/teams/types.js.map +1 -1
  184. package/dist/src/platforms/webex/cli.js +1 -1
  185. package/dist/src/platforms/webex/types.d.ts +1 -1
  186. package/dist/src/platforms/webexbot/cli.js +1 -1
  187. package/dist/src/platforms/wechatbot/cli.js +1 -1
  188. package/dist/src/platforms/whatsapp/cli.js +1 -1
  189. package/dist/src/platforms/whatsappbot/cli.js +1 -1
  190. package/dist/src/tui/adapters/imessage-adapter.d.ts +21 -0
  191. package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -0
  192. package/dist/src/tui/adapters/imessage-adapter.js +110 -0
  193. package/dist/src/tui/adapters/imessage-adapter.js.map +1 -0
  194. package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
  195. package/dist/src/tui/adapters/instagram-adapter.js +16 -0
  196. package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
  197. package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
  198. package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
  199. package/dist/src/tui/adapters/teams-adapter.js +21 -0
  200. package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
  201. package/dist/src/tui/app.d.ts.map +1 -1
  202. package/dist/src/tui/app.js +12 -3
  203. package/dist/src/tui/app.js.map +1 -1
  204. package/docs/content/docs/agent-skills.mdx +1 -1
  205. package/docs/content/docs/cli/imessage.mdx +99 -0
  206. package/docs/content/docs/cli/instagram.mdx +2 -1
  207. package/docs/content/docs/cli/meta.json +1 -0
  208. package/docs/content/docs/cli/teams.mdx +22 -0
  209. package/docs/content/docs/index.mdx +9 -6
  210. package/docs/content/docs/quick-start.mdx +2 -0
  211. package/docs/content/docs/sdk/instagram.mdx +90 -5
  212. package/docs/content/docs/tui.mdx +4 -3
  213. package/package.json +10 -2
  214. package/scripts/postbuild.ts +12 -15
  215. package/skills/agent-channeltalk/SKILL.md +1 -1
  216. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  217. package/skills/agent-discord/SKILL.md +1 -1
  218. package/skills/agent-discordbot/SKILL.md +1 -1
  219. package/skills/agent-imessage/SKILL.md +133 -0
  220. package/skills/agent-imessage/references/authentication.md +66 -0
  221. package/skills/agent-imessage/references/permissions.md +31 -0
  222. package/skills/agent-imessage/references/setup.md +49 -0
  223. package/skills/agent-instagram/SKILL.md +72 -2
  224. package/skills/agent-instagram/references/authentication.md +11 -0
  225. package/skills/agent-instagram/references/common-patterns.md +36 -0
  226. package/skills/agent-instagram/templates/monitor-chat.sh +5 -0
  227. package/skills/agent-kakaotalk/SKILL.md +1 -1
  228. package/skills/agent-line/SKILL.md +1 -1
  229. package/skills/agent-slack/SKILL.md +1 -1
  230. package/skills/agent-slackbot/SKILL.md +1 -1
  231. package/skills/agent-teams/SKILL.md +59 -6
  232. package/skills/agent-teams/references/authentication.md +31 -1
  233. package/skills/agent-telegram/SKILL.md +1 -1
  234. package/skills/agent-telegrambot/SKILL.md +1 -1
  235. package/skills/agent-webex/SKILL.md +1 -1
  236. package/skills/agent-webexbot/SKILL.md +1 -1
  237. package/skills/agent-wechatbot/SKILL.md +1 -1
  238. package/skills/agent-whatsapp/SKILL.md +1 -1
  239. package/skills/agent-whatsappbot/SKILL.md +1 -1
  240. package/src/cli.ts +4 -0
  241. package/src/platforms/discord/client.test.ts +86 -0
  242. package/src/platforms/discord/client.ts +10 -3
  243. package/src/platforms/imessage/cli.ts +39 -0
  244. package/src/platforms/imessage/client.test.ts +137 -0
  245. package/src/platforms/imessage/client.ts +253 -0
  246. package/src/platforms/imessage/commands/auth.ts +142 -0
  247. package/src/platforms/imessage/commands/chat.ts +51 -0
  248. package/src/platforms/imessage/commands/doctor.test.ts +80 -0
  249. package/src/platforms/imessage/commands/doctor.ts +139 -0
  250. package/src/platforms/imessage/commands/index.ts +6 -0
  251. package/src/platforms/imessage/commands/message.test.ts +20 -0
  252. package/src/platforms/imessage/commands/message.ts +151 -0
  253. package/src/platforms/imessage/commands/setup.test.ts +52 -0
  254. package/src/platforms/imessage/commands/setup.ts +76 -0
  255. package/src/platforms/imessage/commands/shared.test.ts +19 -0
  256. package/src/platforms/imessage/commands/shared.ts +69 -0
  257. package/src/platforms/imessage/commands/whoami.ts +22 -0
  258. package/src/platforms/imessage/credential-manager.test.ts +122 -0
  259. package/src/platforms/imessage/credential-manager.ts +123 -0
  260. package/src/platforms/imessage/ensure-auth.ts +18 -0
  261. package/src/platforms/imessage/errors.ts +27 -0
  262. package/src/platforms/imessage/index.test.ts +13 -0
  263. package/src/platforms/imessage/index.ts +16 -0
  264. package/src/platforms/imessage/rpc.ts +157 -0
  265. package/src/platforms/imessage/test-stub-imsg.mjs +147 -0
  266. package/src/platforms/imessage/types.test.ts +47 -0
  267. package/src/platforms/imessage/types.ts +95 -0
  268. package/src/platforms/instagram/client.test.ts +478 -33
  269. package/src/platforms/instagram/client.ts +472 -68
  270. package/src/platforms/instagram/commands/auth.test.ts +177 -1
  271. package/src/platforms/instagram/commands/auth.ts +182 -11
  272. package/src/platforms/instagram/credential-manager.test.ts +30 -0
  273. package/src/platforms/instagram/credential-manager.ts +23 -1
  274. package/src/platforms/instagram/hybrid-listener.test.ts +135 -0
  275. package/src/platforms/instagram/hybrid-listener.ts +196 -0
  276. package/src/platforms/instagram/index.ts +10 -0
  277. package/src/platforms/instagram/mqtt/connection.ts +72 -0
  278. package/src/platforms/instagram/mqtt/thrift.test.ts +91 -0
  279. package/src/platforms/instagram/mqtt/thrift.ts +159 -0
  280. package/src/platforms/instagram/mqtt/transport.ts +285 -0
  281. package/src/platforms/instagram/realtime-listener.test.ts +34 -0
  282. package/src/platforms/instagram/realtime-listener.ts +201 -0
  283. package/src/platforms/instagram/types.ts +11 -8
  284. package/src/platforms/teams/app-config.ts +92 -0
  285. package/src/platforms/teams/client.test.ts +335 -1
  286. package/src/platforms/teams/client.ts +342 -7
  287. package/src/platforms/teams/commands/auth.test.ts +93 -1
  288. package/src/platforms/teams/commands/auth.ts +182 -3
  289. package/src/platforms/teams/commands/file.test.ts +92 -1
  290. package/src/platforms/teams/commands/file.ts +76 -1
  291. package/src/platforms/teams/commands/message.test.ts +95 -1
  292. package/src/platforms/teams/commands/message.ts +145 -3
  293. package/src/platforms/teams/credential-manager.test.ts +20 -0
  294. package/src/platforms/teams/credential-manager.ts +68 -3
  295. package/src/platforms/teams/device-code.test.ts +311 -0
  296. package/src/platforms/teams/device-code.ts +277 -0
  297. package/src/platforms/teams/device-login.test.ts +288 -0
  298. package/src/platforms/teams/device-login.ts +206 -0
  299. package/src/platforms/teams/ensure-auth.ts +11 -0
  300. package/src/platforms/teams/index.test.ts +10 -0
  301. package/src/platforms/teams/index.ts +16 -1
  302. package/src/platforms/teams/listener.ts +295 -0
  303. package/src/platforms/teams/realm-discovery.test.ts +73 -0
  304. package/src/platforms/teams/realm-discovery.ts +42 -0
  305. package/src/platforms/teams/token-extractor.ts +69 -0
  306. package/src/platforms/teams/token-provider.test.ts +227 -0
  307. package/src/platforms/teams/token-provider.ts +284 -0
  308. package/src/platforms/teams/trouter.test.ts +114 -0
  309. package/src/platforms/teams/trouter.ts +228 -0
  310. package/src/platforms/teams/types.test.ts +17 -0
  311. package/src/platforms/teams/types.ts +86 -0
  312. package/src/platforms/webexbot/cli.ts +0 -0
  313. package/src/tui/adapters/imessage-adapter.ts +131 -0
  314. package/src/tui/adapters/instagram-adapter.ts +13 -0
  315. package/src/tui/adapters/teams-adapter.ts +23 -0
  316. package/src/tui/app.ts +16 -3
@@ -0,0 +1,206 @@
1
+ import { getTeamsAppClientId } from './app-config'
2
+ import { TeamsClient } from './client'
3
+ import { TeamsCredentialManager } from './credential-manager'
4
+ import {
5
+ type AadToken,
6
+ type DeviceCodeInfo,
7
+ decodeJwtTid,
8
+ exchangeDeviceCode,
9
+ exchangeForSkypeScope,
10
+ isConsumerTenant,
11
+ mintSkypeToken,
12
+ pollDeviceToken,
13
+ requestDeviceCode,
14
+ resolveWorkTenantId,
15
+ } from './device-code'
16
+ import { type TeamsAccountType, TeamsError } from './types'
17
+
18
+ const PERSONAL_ACCOUNT_HINT =
19
+ 'This looks like a personal Microsoft account. Run `agent-teams auth login --account-type personal` to sign in.'
20
+
21
+ export interface DeviceCodePrompt {
22
+ verificationUri: string
23
+ verificationUriComplete: string
24
+ userCode: string
25
+ expiresAt: number
26
+ }
27
+
28
+ export interface DeviceLoginResult {
29
+ accountType: TeamsAccountType
30
+ userName: string
31
+ teams: Array<{ id: string; name: string }>
32
+ current: string | null
33
+ }
34
+
35
+ interface LoginCallbacks {
36
+ onCode: (prompt: DeviceCodePrompt) => void | Promise<void>
37
+ onPending?: () => void
38
+ debug?: (message: string) => void
39
+ clientId?: string
40
+ accountType?: TeamsAccountType
41
+ }
42
+
43
+ export async function startDeviceCode(
44
+ clientIdOverride?: string,
45
+ accountType: TeamsAccountType = 'personal',
46
+ ): Promise<{ info: DeviceCodeInfo; clientId: string }> {
47
+ const clientId = clientIdOverride ?? getTeamsAppClientId(accountType).clientId
48
+ const info = await requestDeviceCode(clientId, accountType)
49
+ return { info, clientId }
50
+ }
51
+
52
+ export async function completeDeviceCode(
53
+ deviceCode: string,
54
+ clientId: string,
55
+ credManager: TeamsCredentialManager = new TeamsCredentialManager(),
56
+ accountType: TeamsAccountType = 'personal',
57
+ ): Promise<DeviceLoginResult> {
58
+ const first = await exchangeDeviceCode(deviceCode, clientId, accountType)
59
+ if (first.status === 'pending' || first.status === 'slow_down') {
60
+ throw new PendingApprovalError()
61
+ }
62
+ if (first.status !== 'success') {
63
+ throw new Error(
64
+ first.status === 'expired'
65
+ ? 'Device code expired.'
66
+ : first.status === 'declined'
67
+ ? 'Authorization declined.'
68
+ : `Login failed: ${first.message}`,
69
+ )
70
+ }
71
+ return finalize(first.token, clientId, accountType, credManager)
72
+ }
73
+
74
+ export async function loginWithDeviceCode(
75
+ callbacks: LoginCallbacks,
76
+ credManager: TeamsCredentialManager = new TeamsCredentialManager(),
77
+ ): Promise<DeviceLoginResult> {
78
+ const accountType = callbacks.accountType ?? 'personal'
79
+ const { info, clientId } = await startDeviceCode(callbacks.clientId, accountType)
80
+ await callbacks.onCode({
81
+ verificationUri: info.verificationUri,
82
+ verificationUriComplete: info.verificationUriComplete,
83
+ userCode: info.userCode,
84
+ expiresAt: Date.now() + info.expiresIn * 1000,
85
+ })
86
+
87
+ const aad = await pollDeviceToken(info.deviceCode, info.interval, info.expiresIn, clientId, accountType)
88
+
89
+ // A work login can resolve to a personal (MSA) account, whose token carries a
90
+ // consumer tenant. Such accounts have no org tenant and can't be finished on
91
+ // the work flow, so stop with an actionable hint rather than fail obscurely.
92
+ if (accountType === 'work' && isPersonalAccount(aad)) {
93
+ throw new TeamsError(PERSONAL_ACCOUNT_HINT, 'teams_personal_account_on_work_login')
94
+ }
95
+
96
+ callbacks.onPending?.()
97
+ return finalize(aad, clientId, accountType, credManager, callbacks.debug)
98
+ }
99
+
100
+ function isPersonalAccount(aad: AadToken): boolean {
101
+ const tid = decodeJwtTid(aad.accessToken)
102
+ return tid !== undefined && isConsumerTenant(tid)
103
+ }
104
+
105
+ async function finalize(
106
+ initialAad: AadToken,
107
+ clientId: string,
108
+ accountType: TeamsAccountType,
109
+ credManager: TeamsCredentialManager,
110
+ debug?: (message: string) => void,
111
+ ): Promise<DeviceLoginResult> {
112
+ if (!initialAad.refreshToken) {
113
+ throw new Error(
114
+ 'Sign-in did not return a refresh token (needed to mint the Teams token). Retry `auth login`, or use `auth extract` if this persists.',
115
+ )
116
+ }
117
+
118
+ let tenantId: string | undefined
119
+ if (accountType === 'work') {
120
+ debug?.('Resolving Teams tenant...')
121
+ tenantId = await resolveWorkTenantId(initialAad.accessToken)
122
+ }
123
+
124
+ debug?.('Exchanging token for skype audience...')
125
+ const skypeScoped = await exchangeForSkypeScope(initialAad.refreshToken, clientId, accountType, tenantId)
126
+
127
+ debug?.('Minting skype token...')
128
+ const minted = await mintSkypeToken(skypeScoped.accessToken, accountType)
129
+
130
+ const client = await new TeamsClient().login({ token: minted.skypeToken, accountType })
131
+ const authInfo = await client.testAuth()
132
+ const teams = await client.listTeams()
133
+
134
+ const teamMap: Record<string, { team_id: string; team_name: string }> = {}
135
+ for (const team of teams) {
136
+ teamMap[team.id] = { team_id: team.id, team_name: team.name }
137
+ }
138
+ const currentTeam = teams[0]?.id ?? null
139
+
140
+ await credManager.setDeviceCodeAccount({
141
+ accountType,
142
+ token: minted.skypeToken,
143
+ tokenExpiresAt: minted.skypeTokenExpiresAt,
144
+ aadRefreshToken: skypeScoped.refreshToken,
145
+ aadClientId: clientId,
146
+ aadTenantId: tenantId,
147
+ region: minted.region ?? (accountType === 'work' ? client.getRegion() : undefined),
148
+ userName: authInfo.displayName,
149
+ teams: teamMap,
150
+ currentTeam,
151
+ authMethod: 'device-code',
152
+ })
153
+
154
+ return {
155
+ accountType,
156
+ userName: authInfo.displayName,
157
+ teams: teams.map((t) => ({ id: t.id, name: t.name })),
158
+ current: currentTeam,
159
+ }
160
+ }
161
+
162
+ export async function refreshDeviceCodeAccount(
163
+ accountType: TeamsAccountType,
164
+ credManager: TeamsCredentialManager = new TeamsCredentialManager(),
165
+ debug?: (message: string) => void,
166
+ ): Promise<boolean> {
167
+ const config = await credManager.loadConfig()
168
+ const account = config?.accounts[accountType]
169
+ if (!account || account.auth_method !== 'device-code' || !account.aad_refresh_token) {
170
+ return false
171
+ }
172
+ const clientId = account.aad_client_id ?? getTeamsAppClientId(accountType).clientId
173
+
174
+ try {
175
+ debug?.('Silently refreshing skype token...')
176
+ const tenantId = accountType === 'work' ? account.aad_tenant_id : undefined
177
+ const skypeScoped = await exchangeForSkypeScope(account.aad_refresh_token, clientId, accountType, tenantId)
178
+ const minted = await mintSkypeToken(skypeScoped.accessToken, accountType)
179
+
180
+ await credManager.setDeviceCodeAccount({
181
+ accountType,
182
+ token: minted.skypeToken,
183
+ tokenExpiresAt: minted.skypeTokenExpiresAt,
184
+ aadRefreshToken: skypeScoped.refreshToken,
185
+ aadClientId: clientId,
186
+ aadTenantId: tenantId,
187
+ region: minted.region ?? account.region,
188
+ userName: account.user_name,
189
+ teams: account.teams,
190
+ currentTeam: account.current_team,
191
+ authMethod: 'device-code',
192
+ makeCurrent: false,
193
+ })
194
+ return true
195
+ } catch (error) {
196
+ debug?.(`Silent refresh failed: ${(error as Error).message}`)
197
+ return false
198
+ }
199
+ }
200
+
201
+ export class PendingApprovalError extends Error {
202
+ constructor() {
203
+ super('Authorization pending. Approve in the browser, then retry.')
204
+ this.name = 'PendingApprovalError'
205
+ }
206
+ }
@@ -2,6 +2,7 @@ import { warn } from '@/shared/utils/stderr'
2
2
 
3
3
  import { TeamsClient } from './client'
4
4
  import { TeamsCredentialManager } from './credential-manager'
5
+ import { refreshDeviceCodeAccount } from './device-login'
5
6
  import { TeamsTokenExtractor } from './token-extractor'
6
7
  import type { TeamsAccount, TeamsAccountType, TeamsConfig } from './types'
7
8
 
@@ -12,6 +13,8 @@ export async function ensureTeamsAuth(): Promise<void> {
12
13
 
13
14
  if (config && hasValidToken(config)) return
14
15
 
16
+ if (config && (await trySilentRefresh(config, credManager))) return
17
+
15
18
  const extractor = new TeamsTokenExtractor()
16
19
  const extracted = await extractor.extract()
17
20
  if (extracted.length === 0) return
@@ -92,6 +95,14 @@ async function resolveAccountType(
92
95
  throw lastError ?? new Error('Token validation failed')
93
96
  }
94
97
 
98
+ async function trySilentRefresh(config: TeamsConfig, credManager: TeamsCredentialManager): Promise<boolean> {
99
+ const key = TeamsCredentialManager.accountOverride ?? config.current_account
100
+ if (!key) return false
101
+ const account = config.accounts[key]
102
+ if (account?.auth_method !== 'device-code' || !account.aad_refresh_token) return false
103
+ return refreshDeviceCodeAccount(key as TeamsAccountType, credManager)
104
+ }
105
+
95
106
  function hasValidToken(config: TeamsConfig): boolean {
96
107
  const key = TeamsCredentialManager.accountOverride ?? config.current_account
97
108
  if (!key) return false
@@ -13,7 +13,9 @@ import {
13
13
  TeamsFileSchema,
14
14
  TeamsMessageSchema,
15
15
  TeamsReactionSchema,
16
+ TeamsSearchResultSchema,
16
17
  TeamsTeamSchema,
18
+ TeamsTokenProvider,
17
19
  TeamsUserSchema,
18
20
  } from '@/platforms/teams/index'
19
21
 
@@ -29,6 +31,10 @@ it('TeamsCredentialManager is exported from barrel', () => {
29
31
  expect(typeof TeamsCredentialManager).toBe('function')
30
32
  })
31
33
 
34
+ it('TeamsTokenProvider is exported from barrel', () => {
35
+ expect(typeof TeamsTokenProvider).toBe('function')
36
+ })
37
+
32
38
  it('TeamsTeamSchema is exported from barrel', () => {
33
39
  expect(typeof TeamsTeamSchema.parse).toBe('function')
34
40
  })
@@ -41,6 +47,10 @@ it('TeamsMessageSchema is exported from barrel', () => {
41
47
  expect(typeof TeamsMessageSchema.parse).toBe('function')
42
48
  })
43
49
 
50
+ it('TeamsSearchResultSchema is exported from barrel', () => {
51
+ expect(typeof TeamsSearchResultSchema.parse).toBe('function')
52
+ })
53
+
44
54
  it('TeamsUserSchema is exported from barrel', () => {
45
55
  expect(typeof TeamsUserSchema.parse).toBe('function')
46
56
  })
@@ -1,6 +1,16 @@
1
1
  export { TeamsClient } from './client'
2
2
  export { TeamsCredentialManager } from './credential-manager'
3
- export { TeamsError } from './types'
3
+ export { TeamsTokenProvider } from './token-provider'
4
+ export {
5
+ completeDeviceCode,
6
+ loginWithDeviceCode,
7
+ PendingApprovalError,
8
+ refreshDeviceCodeAccount,
9
+ startDeviceCode,
10
+ } from './device-login'
11
+ export type { DeviceCodePrompt, DeviceLoginResult } from './device-login'
12
+ export { TeamsListener } from './listener'
13
+ export { TeamsAuthCapabilityError, TeamsError } from './types'
4
14
  export type {
5
15
  TeamsAccount,
6
16
  TeamsAccountType,
@@ -9,9 +19,13 @@ export type {
9
19
  TeamsConfigLegacy,
10
20
  TeamsCredentials,
11
21
  TeamsFile,
22
+ TeamsListenerEventMap,
12
23
  TeamsMessage,
24
+ TeamsRealtimeMessage,
13
25
  TeamsReaction,
26
+ TeamsSearchResult,
14
27
  TeamsTeam,
28
+ TeamsTrouterGenericEvent,
15
29
  TeamsUser,
16
30
  } from './types'
17
31
  export {
@@ -24,6 +38,7 @@ export {
24
38
  TeamsFileSchema,
25
39
  TeamsMessageSchema,
26
40
  TeamsReactionSchema,
41
+ TeamsSearchResultSchema,
27
42
  TeamsTeamSchema,
28
43
  TeamsUserSchema,
29
44
  } from './types'
@@ -0,0 +1,295 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ import WebSocket from 'ws'
5
+
6
+ import type { TeamsClient } from './client'
7
+ import {
8
+ buildActivityFrame,
9
+ buildAuthenticateFrame,
10
+ buildEventAck,
11
+ buildPingFrame,
12
+ buildRequestAck,
13
+ buildWebSocketUrl,
14
+ decodeMessageBody,
15
+ extractChatId,
16
+ fetchTrouterInfo,
17
+ fetchTrouterSessionId,
18
+ isMessageLossFrame,
19
+ parseRequestFrame,
20
+ registerEndpoint,
21
+ type TrouterInfo,
22
+ } from './trouter'
23
+ import { TeamsError } from './types'
24
+ import type { TeamsListenerEventMap, TeamsRealtimeMessage } from './types'
25
+
26
+ const PING_INTERVAL = 30_000
27
+ const RECONNECT_BASE_DELAY = 1_000
28
+ const RECONNECT_MAX_DELAY = 30_000
29
+ const REGISTRATION_REFRESH_INTERVAL = (86400 - 60) * 1000
30
+ const MESSAGE_CACHE_LIMIT = 500
31
+ const TEXT_MESSAGE_TYPES = new Set(['Text', 'RichText/Html'])
32
+
33
+ type EventKey = keyof TeamsListenerEventMap
34
+
35
+ interface IncomingResource {
36
+ id?: string
37
+ messagetype?: string
38
+ content?: string
39
+ from?: string
40
+ imdisplayname?: string
41
+ conversationLink?: string
42
+ resourceLink?: string
43
+ }
44
+
45
+ export class TeamsListener {
46
+ private client: TeamsClient
47
+ private running = false
48
+ private ws: WebSocket | null = null
49
+ private emitter = new EventEmitter()
50
+ private endpointId = randomUUID()
51
+ private idToken: string | null = null
52
+ private info: TrouterInfo | null = null
53
+ private sequence = 0
54
+ private reconnectAttempts = 0
55
+ private pingTimer: ReturnType<typeof setInterval> | null = null
56
+ private reregisterTimer: ReturnType<typeof setInterval> | null = null
57
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null
58
+ private seenMessageIds = new Set<string>()
59
+
60
+ constructor(client: TeamsClient) {
61
+ this.client = client
62
+ }
63
+
64
+ async start(): Promise<void> {
65
+ if (this.running) return
66
+ this.running = true
67
+ this.reconnectAttempts = 0
68
+ await this.connect()
69
+ }
70
+
71
+ stop(): void {
72
+ this.running = false
73
+ this.clearTimers()
74
+ if (this.ws) {
75
+ this.ws.close()
76
+ this.ws = null
77
+ }
78
+ }
79
+
80
+ on<K extends EventKey>(event: K, listener: (...args: TeamsListenerEventMap[K]) => void): this {
81
+ this.emitter.on(event, listener as (...args: any[]) => void)
82
+ return this
83
+ }
84
+
85
+ off<K extends EventKey>(event: K, listener: (...args: TeamsListenerEventMap[K]) => void): this {
86
+ this.emitter.off(event, listener as (...args: any[]) => void)
87
+ return this
88
+ }
89
+
90
+ once<K extends EventKey>(event: K, listener: (...args: TeamsListenerEventMap[K]) => void): this {
91
+ this.emitter.once(event, listener as (...args: any[]) => void)
92
+ return this
93
+ }
94
+
95
+ private async connect(): Promise<void> {
96
+ if (!this.running) return
97
+
98
+ try {
99
+ const skypeToken = this.client.getToken()
100
+
101
+ // Re-extract on every attempt so a reconnect after token expiry picks up
102
+ // a fresh id_token rather than reusing the stale cached one.
103
+ this.idToken = await this.client.getIdToken()
104
+ if (!this.idToken) {
105
+ throw new TeamsError(
106
+ 'Could not obtain Teams id_token for real-time auth. Ensure the Teams desktop app is logged in, then re-run "auth extract".',
107
+ 'no_id_token',
108
+ )
109
+ }
110
+
111
+ const info = await fetchTrouterInfo(skypeToken, this.endpointId)
112
+ if (!this.running) return
113
+ const sessionId = await fetchTrouterSessionId(info, skypeToken, this.endpointId)
114
+ if (!this.running) return
115
+
116
+ this.info = info
117
+ const ws = new WebSocket(buildWebSocketUrl(info, sessionId, this.endpointId), {
118
+ headers: { 'X-Skypetoken': skypeToken, 'User-Agent': 'AgentMessenger' },
119
+ })
120
+ this.ws = ws
121
+
122
+ ws.on('message', (raw) => this.handleFrame(raw.toString()))
123
+
124
+ ws.on('close', () => {
125
+ this.clearTimers()
126
+ if (this.ws === ws) this.ws = null
127
+ if (this.running) {
128
+ this.emitter.emit('disconnected')
129
+ this.scheduleReconnect()
130
+ }
131
+ })
132
+
133
+ ws.on('error', (err) => {
134
+ this.emitter.emit('error', err instanceof Error ? err : new Error(String(err)))
135
+ })
136
+ } catch (error) {
137
+ this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
138
+ if (this.running) this.scheduleReconnect()
139
+ }
140
+ }
141
+
142
+ private handleFrame(frame: string): void {
143
+ if (frame.startsWith('1::')) {
144
+ void this.onConnected()
145
+ return
146
+ }
147
+
148
+ if (frame.startsWith('3:::')) {
149
+ this.handleRequestFrame(frame)
150
+ return
151
+ }
152
+
153
+ if (frame.startsWith('5:')) {
154
+ const ack = buildEventAck(frame)
155
+ if (ack) this.ws?.send(ack)
156
+ if (isMessageLossFrame(frame)) void this.register()
157
+ }
158
+ }
159
+
160
+ private async onConnected(): Promise<void> {
161
+ if (!this.ws || !this.info || !this.idToken) return
162
+
163
+ this.ws.send(buildAuthenticateFrame(this.info, this.idToken))
164
+ this.ws.send(buildActivityFrame(++this.sequence))
165
+
166
+ // Only report the endpoint as connected once it has actually registered;
167
+ // an unregistered endpoint receives no messages. On failure, drop the
168
+ // possibly-stale id_token and close so the reconnect path re-authenticates.
169
+ const registered = await this.register()
170
+ if (!registered) {
171
+ this.idToken = null
172
+ this.ws.close()
173
+ return
174
+ }
175
+
176
+ this.reconnectAttempts = 0
177
+ this.pingTimer = setInterval(() => {
178
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(buildPingFrame(++this.sequence))
179
+ }, PING_INTERVAL)
180
+ this.reregisterTimer = setInterval(() => void this.register(), REGISTRATION_REFRESH_INTERVAL)
181
+
182
+ this.emitter.emit('connected', { endpointId: this.endpointId })
183
+ }
184
+
185
+ private async register(): Promise<boolean> {
186
+ if (!this.info || !this.idToken) return false
187
+ try {
188
+ await registerEndpoint(
189
+ this.info,
190
+ this.client.getToken(),
191
+ this.idToken,
192
+ this.endpointId,
193
+ this.client.getAccountType(),
194
+ () => randomUUID(),
195
+ )
196
+ return true
197
+ } catch (error) {
198
+ this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
199
+ return false
200
+ }
201
+ }
202
+
203
+ private handleRequestFrame(frame: string): void {
204
+ const request = parseRequestFrame(frame)
205
+ if (!request) return
206
+
207
+ this.ws?.send(buildRequestAck(request.id))
208
+
209
+ if (!request.url?.endsWith('/messaging') || !request.body) return
210
+
211
+ try {
212
+ const decoded = decodeMessageBody(request.headers ?? {}, request.body) as {
213
+ resourceType?: string
214
+ resource?: IncomingResource
215
+ }
216
+ if (decoded.resourceType === 'NewMessage' && decoded.resource) {
217
+ this.emitMessage(decoded.resource)
218
+ }
219
+ this.emitter.emit('teams_event', { resourceType: decoded.resourceType ?? 'unknown', ...decoded })
220
+ } catch (error) {
221
+ this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
222
+ }
223
+ }
224
+
225
+ private emitMessage(resource: IncomingResource): void {
226
+ if (!resource.messagetype || !TEXT_MESSAGE_TYPES.has(resource.messagetype)) return
227
+
228
+ const chatId = extractChatId(resource.conversationLink ?? resource.resourceLink)
229
+ if (!chatId || !resource.id) return
230
+ if (this.seenMessageIds.has(resource.id)) return
231
+ this.rememberMessageId(resource.id)
232
+
233
+ const message: TeamsRealtimeMessage = {
234
+ id: resource.id,
235
+ chatId,
236
+ content: stripHtml(resource.content ?? ''),
237
+ author: {
238
+ id: extractUserId(resource.from) ?? 'unknown',
239
+ displayName: resource.imdisplayname || extractUserId(resource.from) || 'unknown',
240
+ },
241
+ messageType: resource.messagetype,
242
+ timestamp: new Date().toISOString(),
243
+ }
244
+ this.emitter.emit('message', message)
245
+ }
246
+
247
+ private rememberMessageId(id: string): void {
248
+ this.seenMessageIds.add(id)
249
+ if (this.seenMessageIds.size > MESSAGE_CACHE_LIMIT) {
250
+ const oldest = this.seenMessageIds.values().next().value
251
+ if (oldest !== undefined) this.seenMessageIds.delete(oldest)
252
+ }
253
+ }
254
+
255
+ private scheduleReconnect(): void {
256
+ const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** this.reconnectAttempts, RECONNECT_MAX_DELAY)
257
+ this.reconnectAttempts++
258
+ this.endpointId = randomUUID()
259
+ this.sequence = 0
260
+ this.reconnectTimer = setTimeout(() => void this.connect(), delay)
261
+ }
262
+
263
+ private clearTimers(): void {
264
+ if (this.pingTimer) {
265
+ clearInterval(this.pingTimer)
266
+ this.pingTimer = null
267
+ }
268
+ if (this.reregisterTimer) {
269
+ clearInterval(this.reregisterTimer)
270
+ this.reregisterTimer = null
271
+ }
272
+ if (this.reconnectTimer) {
273
+ clearTimeout(this.reconnectTimer)
274
+ this.reconnectTimer = null
275
+ }
276
+ }
277
+ }
278
+
279
+ function stripHtml(html: string): string {
280
+ return html
281
+ .replace(/<[^>]*>/g, '')
282
+ .replace(/&amp;/g, '&')
283
+ .replace(/&lt;/g, '<')
284
+ .replace(/&gt;/g, '>')
285
+ .replace(/&quot;/g, '"')
286
+ .replace(/&#39;/g, "'")
287
+ .replace(/\s+/g, ' ')
288
+ .trim()
289
+ }
290
+
291
+ function extractUserId(from: string | undefined): string | null {
292
+ if (!from) return null
293
+ const match = from.match(/contacts\/(.+)$/)
294
+ return match ? match[1] : from
295
+ }
@@ -0,0 +1,73 @@
1
+ import { afterEach, describe, expect, it, mock } from 'bun:test'
2
+
3
+ import { probeAccountType } from './realm-discovery'
4
+
5
+ const originalFetch = globalThis.fetch
6
+
7
+ function mockFetch(response: { status?: number; json?: unknown; reject?: boolean }): {
8
+ calls: Array<{ url: string; init: RequestInit }>
9
+ } {
10
+ const calls: Array<{ url: string; init: RequestInit }> = []
11
+ globalThis.fetch = mock((input: string | URL | Request, init?: RequestInit) => {
12
+ calls.push({ url: String(input), init: init ?? {} })
13
+ if (response.reject) return Promise.reject(new Error('network down'))
14
+ const status = response.status ?? 200
15
+ return Promise.resolve({
16
+ ok: status >= 200 && status < 300,
17
+ status,
18
+ json: () => Promise.resolve(response.json),
19
+ } as Response)
20
+ }) as typeof fetch
21
+ return { calls }
22
+ }
23
+
24
+ afterEach(() => {
25
+ globalThis.fetch = originalFetch
26
+ })
27
+
28
+ describe('probeAccountType', () => {
29
+ it('posts the email to GetCredentialType as JSON', async () => {
30
+ const { calls } = mockFetch({ json: { EstsProperties: { DomainType: 3 } } })
31
+
32
+ await probeAccountType('user@contoso.com')
33
+
34
+ expect(calls[0].url).toBe('https://login.microsoftonline.com/common/GetCredentialType')
35
+ expect(calls[0].init.method).toBe('POST')
36
+ expect(JSON.parse(calls[0].init.body as string)).toEqual({ Username: 'user@contoso.com' })
37
+ })
38
+
39
+ it('maps a consumer DomainType to personal', async () => {
40
+ mockFetch({ json: { EstsProperties: { DomainType: 2 } } })
41
+ expect(await probeAccountType('user@outlook.com')).toBe('personal')
42
+ })
43
+
44
+ it('treats a known consumer domain as personal even without DomainType', async () => {
45
+ mockFetch({ json: { EstsProperties: { IsConsumerDomain: true } } })
46
+ expect(await probeAccountType('user@hotmail.com')).toBe('personal')
47
+ })
48
+
49
+ it('maps a managed DomainType to work', async () => {
50
+ mockFetch({ json: { EstsProperties: { DomainType: 3 } } })
51
+ expect(await probeAccountType('user@contoso.com')).toBe('work')
52
+ })
53
+
54
+ it('maps a federated DomainType to work', async () => {
55
+ mockFetch({ json: { EstsProperties: { DomainType: 4 } } })
56
+ expect(await probeAccountType('user@fabrikam.com')).toBe('work')
57
+ })
58
+
59
+ it('returns undefined when the domain type is missing', async () => {
60
+ mockFetch({ json: { EstsProperties: {} } })
61
+ expect(await probeAccountType('user@unknown.test')).toBeUndefined()
62
+ })
63
+
64
+ it('returns undefined on a non-ok response', async () => {
65
+ mockFetch({ status: 429, json: {} })
66
+ expect(await probeAccountType('user@contoso.com')).toBeUndefined()
67
+ })
68
+
69
+ it('returns undefined when the request throws', async () => {
70
+ mockFetch({ reject: true })
71
+ expect(await probeAccountType('user@contoso.com')).toBeUndefined()
72
+ })
73
+ })