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,42 @@
1
+ import { GET_CREDENTIAL_TYPE_URL } from './app-config'
2
+ import type { TeamsAccountType } from './types'
3
+
4
+ // GetCredentialType (the endpoint Microsoft's own login page calls) reports
5
+ // EstsProperties.DomainType, which distinguishes a personal account from a
6
+ // work account before any device code is issued.
7
+ const DOMAIN_TYPE_CONSUMER = 2
8
+
9
+ interface CredentialTypeResponse {
10
+ IfExistsResult?: number
11
+ EstsProperties?: {
12
+ DomainType?: number
13
+ IsConsumerDomain?: boolean
14
+ }
15
+ }
16
+
17
+ // Returns the account type for an email, or undefined when it can't be
18
+ // determined (network error, throttling, unknown domain) so the caller can
19
+ // fall back to its default flow rather than block sign-in.
20
+ export async function probeAccountType(email: string): Promise<TeamsAccountType | undefined> {
21
+ let body: CredentialTypeResponse
22
+ try {
23
+ const res = await fetch(GET_CREDENTIAL_TYPE_URL, {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify({ Username: email }),
27
+ })
28
+ if (!res.ok) return undefined
29
+ body = (await res.json()) as CredentialTypeResponse
30
+ } catch {
31
+ return undefined
32
+ }
33
+
34
+ const ests = body.EstsProperties
35
+ if (ests?.DomainType === DOMAIN_TYPE_CONSUMER || ests?.IsConsumerDomain === true) {
36
+ return 'personal'
37
+ }
38
+ if (typeof ests?.DomainType === 'number') {
39
+ return 'work'
40
+ }
41
+ return undefined
42
+ }
@@ -49,6 +49,12 @@ const TEAMS_HOST_PATTERNS = [
49
49
  '.microsoft.com',
50
50
  ]
51
51
 
52
+ // The trouter real-time WebSocket needs an OAuth Bearer token (a JWE) for its
53
+ // user.authenticate step. Teams stores it in the `authtoken` cookie, prefixed
54
+ // with "Bearer=" and URL-encoded.
55
+ const AUTHTOKEN_COOKIE_NAME = 'authtoken'
56
+ const AUTHTOKEN_HOST_PATTERNS = ['teams.live.com', 'teams.microsoft.com']
57
+
52
58
  const TEAMS_KEYCHAIN_VARIANTS: KeychainVariant[] = [
53
59
  { service: 'Microsoft Teams Safe Storage', account: 'Microsoft Teams' },
54
60
  {
@@ -252,6 +258,69 @@ export class TeamsTokenExtractor {
252
258
  return this.extractFromCookiesDB()
253
259
  }
254
260
 
261
+ async extractIdToken(accountType?: TeamsAccountType): Promise<string | null> {
262
+ await this.decryptor.loadCachedKey()
263
+
264
+ const desktopPaths = this.getDesktopCookiesPaths().filter(
265
+ (p) => accountType === undefined || !p.accountTypeKnown || p.accountType === accountType,
266
+ )
267
+ const candidatePaths = [...desktopPaths, ...this.getBrowserCookiesPaths()]
268
+
269
+ for (const { path: dbPath } of candidatePaths) {
270
+ if (!dbPath || !existsSync(dbPath)) continue
271
+
272
+ const bearer = await this.extractAuthTokenFromSQLite(dbPath)
273
+ if (bearer) return bearer
274
+ }
275
+
276
+ return null
277
+ }
278
+
279
+ private async extractAuthTokenFromSQLite(dbPath: string): Promise<string | null> {
280
+ try {
281
+ let localStatePath: string | undefined
282
+ if (this.platform === 'win32') {
283
+ localStatePath = findLocalStatePath(dbPath) ?? undefined
284
+ }
285
+
286
+ for (const hostPattern of AUTHTOKEN_HOST_PATTERNS) {
287
+ const sql = `
288
+ SELECT value, encrypted_value
289
+ FROM cookies
290
+ WHERE name = '${AUTHTOKEN_COOKIE_NAME}'
291
+ AND host_key LIKE '%${hostPattern}%'
292
+ LIMIT 1
293
+ `
294
+
295
+ type CookieRow = { value?: string; encrypted_value?: Uint8Array | Buffer } | null
296
+ const row = await this.cookieReader.queryFirst<CookieRow>(dbPath, sql)
297
+ if (!row) continue
298
+
299
+ let value = row.value ?? ''
300
+ if ((!value || value.length < 20) && row.encrypted_value && row.encrypted_value.length > 0) {
301
+ const decrypted = this.decryptor.decryptCookieRaw(Buffer.from(row.encrypted_value), localStatePath)
302
+ if (!decrypted) continue
303
+ value = ChromiumCookieDecryptor.stripIntegrityHash(decrypted).toString('utf8')
304
+ }
305
+
306
+ const bearer = this.normalizeAuthToken(value)
307
+ if (bearer) return bearer
308
+ }
309
+
310
+ return null
311
+ } catch (error) {
312
+ this.debug(` authtoken query error: ${(error as Error).message}`)
313
+ return null
314
+ }
315
+ }
316
+
317
+ private normalizeAuthToken(rawValue: string): string | null {
318
+ if (!rawValue) return null
319
+ const decoded = decodeURIComponent(rawValue)
320
+ const token = decoded.replace(/^Bearer=/i, '').trim()
321
+ return token.length > 20 ? token : null
322
+ }
323
+
255
324
  async clearKeyCache(): Promise<void> {
256
325
  await this.decryptor.clearKeyCache()
257
326
  }
@@ -0,0 +1,227 @@
1
+ import { afterAll, afterEach, describe, expect, it, mock } from 'bun:test'
2
+ import { rmSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ import { TeamsCredentialManager } from './credential-manager'
6
+ import { TeamsTokenProvider } from './token-provider'
7
+ import { TeamsAuthCapabilityError } from './types'
8
+
9
+ const TENANT_ID = '11111111-1111-1111-1111-111111111111'
10
+ const USER_ID = '22222222-2222-2222-2222-222222222222'
11
+ const CLIENT_ID = '1fec8e78-bce4-4aaf-ab1b-5451cc387264'
12
+ const SUBSTRATE_AUD = 'https://substrate.office.com'
13
+ const GRAPH_AUD = 'https://graph.microsoft.com'
14
+ const originalFetch = globalThis.fetch
15
+ const testDirs: string[] = []
16
+
17
+ interface FetchCall {
18
+ url: string
19
+ init: RequestInit
20
+ body: URLSearchParams
21
+ }
22
+
23
+ interface TokenResponseConfig {
24
+ aud: string
25
+ refreshToken: string
26
+ expiresIn?: number
27
+ marker?: string
28
+ }
29
+
30
+ function setup(): TeamsCredentialManager {
31
+ const dir = join(import.meta.dir, `.test-teams-provider-${Date.now()}-${Math.random().toString(36).slice(2)}`)
32
+ testDirs.push(dir)
33
+ return new TeamsCredentialManager(dir)
34
+ }
35
+
36
+ async function seedDeviceAccount(manager: TeamsCredentialManager, refreshToken = 'old-refresh'): Promise<void> {
37
+ await manager.setDeviceCodeAccount({
38
+ accountType: 'work',
39
+ token: 'skype-token',
40
+ tokenExpiresAt: '2100-01-01T00:00:00Z',
41
+ aadRefreshToken: refreshToken,
42
+ aadClientId: CLIENT_ID,
43
+ teams: {},
44
+ currentTeam: null,
45
+ })
46
+ }
47
+
48
+ async function seedPersonalDeviceAccount(
49
+ manager: TeamsCredentialManager,
50
+ refreshToken = 'personal-refresh',
51
+ ): Promise<void> {
52
+ await manager.setDeviceCodeAccount({
53
+ accountType: 'personal',
54
+ token: 'personal-skype-token',
55
+ tokenExpiresAt: '2100-01-01T00:00:00Z',
56
+ aadRefreshToken: refreshToken,
57
+ aadClientId: CLIENT_ID,
58
+ teams: {},
59
+ currentTeam: null,
60
+ })
61
+ }
62
+
63
+ function createJwt(aud: string, marker = 'token'): string {
64
+ const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url')
65
+ const payload = Buffer.from(
66
+ JSON.stringify({ aud, tid: TENANT_ID, oid: USER_ID, exp: Math.floor(Date.now() / 1000) + 3600, jti: marker }),
67
+ ).toString('base64url')
68
+ return `${header}.${payload}.signature`
69
+ }
70
+
71
+ function bodyParams(body: BodyInit | null | undefined): URLSearchParams {
72
+ if (body instanceof URLSearchParams) return body
73
+ if (typeof body === 'string') return new URLSearchParams(body)
74
+ return new URLSearchParams()
75
+ }
76
+
77
+ function headerValue(init: RequestInit, name: string): string | undefined {
78
+ const headers = init.headers
79
+ if (headers instanceof Headers) return headers.get(name) ?? undefined
80
+ if (Array.isArray(headers)) {
81
+ const pair = headers.find(([key]) => key.toLowerCase() === name.toLowerCase())
82
+ return pair?.[1]
83
+ }
84
+ return headers?.[name]
85
+ }
86
+
87
+ function mockTokenResponses(responses: TokenResponseConfig[]): { calls: FetchCall[] } {
88
+ const calls: FetchCall[] = []
89
+ const queue = [...responses]
90
+ globalThis.fetch = mock((input: string | URL | Request, init?: RequestInit) => {
91
+ const requestInit = init ?? {}
92
+ calls.push({ url: String(input), init: requestInit, body: bodyParams(requestInit.body) })
93
+ const next = queue.shift()
94
+ if (!next) {
95
+ return Promise.resolve(new Response(JSON.stringify({ error: 'no_mock_response' }), { status: 500 }))
96
+ }
97
+ return Promise.resolve(
98
+ new Response(
99
+ JSON.stringify({
100
+ access_token: createJwt(next.aud, next.marker),
101
+ refresh_token: next.refreshToken,
102
+ expires_in: next.expiresIn ?? 3600,
103
+ }),
104
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
105
+ ),
106
+ )
107
+ }) as typeof fetch
108
+ return { calls }
109
+ }
110
+
111
+ afterEach(() => {
112
+ globalThis.fetch = originalFetch
113
+ })
114
+
115
+ afterAll(() => {
116
+ for (const dir of testDirs) rmSync(dir, { recursive: true, force: true })
117
+ })
118
+
119
+ describe('TeamsTokenProvider', () => {
120
+ it('refresh-exchanges substrate tokens with Origin and persists rotated refresh tokens', async () => {
121
+ const manager = setup()
122
+ await seedDeviceAccount(manager)
123
+ const { calls } = mockTokenResponses([{ aud: SUBSTRATE_AUD, refreshToken: 'rotated-refresh' }])
124
+
125
+ const provider = new TeamsTokenProvider(manager)
126
+ const token = await provider.getSubstrateToken()
127
+
128
+ expect(token).toContain('.signature')
129
+ expect(calls[0].url).toContain('/organizations/oauth2/v2.0/token')
130
+ expect(calls[0].body.get('grant_type')).toBe('refresh_token')
131
+ expect(calls[0].body.get('client_id')).toBe(CLIENT_ID)
132
+ expect(calls[0].body.get('refresh_token')).toBe('old-refresh')
133
+ expect(calls[0].body.get('scope')).toBe('https://substrate.office.com/.default offline_access')
134
+ expect(headerValue(calls[0].init, 'Origin')).toBe('https://teams.microsoft.com')
135
+
136
+ const config = await manager.loadConfig()
137
+ expect(config?.accounts.work?.aad_refresh_token).toBe('rotated-refresh')
138
+ })
139
+
140
+ it('throws when the acquired token audience does not match the requested audience', async () => {
141
+ const manager = setup()
142
+ await seedDeviceAccount(manager)
143
+ mockTokenResponses([{ aud: GRAPH_AUD, refreshToken: 'rotated-refresh' }])
144
+
145
+ const provider = new TeamsTokenProvider(manager)
146
+ await expect(provider.getSubstrateToken()).rejects.toThrow(/audience/i)
147
+ })
148
+
149
+ it('throws TeamsAuthCapabilityError for cookie-only credentials', async () => {
150
+ const manager = setup()
151
+ await manager.setToken('skype-token', 'work', '2100-01-01T00:00:00Z')
152
+
153
+ const provider = new TeamsTokenProvider(manager)
154
+ await expect(provider.getSubstrateToken()).rejects.toThrow(TeamsAuthCapabilityError)
155
+ await expect(provider.getSubstrateToken()).rejects.toThrow(/auth login/)
156
+ })
157
+
158
+ it('uses the in-memory cache until the five-minute refresh buffer', async () => {
159
+ const manager = setup()
160
+ await seedDeviceAccount(manager)
161
+ const { calls } = mockTokenResponses([{ aud: SUBSTRATE_AUD, refreshToken: 'rotated-refresh', marker: 'cached' }])
162
+
163
+ const provider = new TeamsTokenProvider(manager)
164
+ const first = await provider.getSubstrateToken()
165
+ const second = await provider.getSubstrateToken()
166
+
167
+ expect(second).toBe(first)
168
+ expect(calls).toHaveLength(1)
169
+ })
170
+
171
+ it('refreshes cached tokens inside the five-minute buffer', async () => {
172
+ const manager = setup()
173
+ await seedDeviceAccount(manager)
174
+ const { calls } = mockTokenResponses([
175
+ { aud: SUBSTRATE_AUD, refreshToken: 'near-expiry-refresh', expiresIn: 299, marker: 'near-expiry' },
176
+ { aud: SUBSTRATE_AUD, refreshToken: 'fresh-refresh', expiresIn: 3600, marker: 'fresh' },
177
+ ])
178
+
179
+ const provider = new TeamsTokenProvider(manager)
180
+ const first = await provider.getSubstrateToken()
181
+ const second = await provider.getSubstrateToken()
182
+
183
+ expect(second).not.toBe(first)
184
+ expect(calls).toHaveLength(2)
185
+ const config = await manager.loadConfig()
186
+ expect(config?.accounts.work?.aad_refresh_token).toBe('fresh-refresh')
187
+ })
188
+
189
+ it('does not reuse bearer tokens across bound accounts', async () => {
190
+ const manager = setup()
191
+ await seedDeviceAccount(manager, 'work-refresh')
192
+ await seedPersonalDeviceAccount(manager, 'personal-refresh')
193
+ const { calls } = mockTokenResponses([
194
+ { aud: SUBSTRATE_AUD, refreshToken: 'work-rotated-refresh', marker: 'work-token' },
195
+ { aud: SUBSTRATE_AUD, refreshToken: 'personal-rotated-refresh', marker: 'personal-token' },
196
+ ])
197
+
198
+ const provider = new TeamsTokenProvider(manager)
199
+ const workToken = await provider.bindAccount('work').getSubstrateToken()
200
+ const personalToken = await provider.bindAccount('personal').getSubstrateToken()
201
+
202
+ expect(personalToken).not.toBe(workToken)
203
+ expect(calls).toHaveLength(2)
204
+ expect(calls[0].body.get('refresh_token')).toBe('work-refresh')
205
+ expect(calls[1].body.get('refresh_token')).toBe('personal-refresh')
206
+ })
207
+
208
+ it('restores tenant/user identity on a cached bearer hit after switching accounts back', async () => {
209
+ const manager = setup()
210
+ await seedDeviceAccount(manager, 'work-refresh')
211
+ await seedPersonalDeviceAccount(manager, 'personal-refresh')
212
+ mockTokenResponses([
213
+ { aud: SUBSTRATE_AUD, refreshToken: 'work-rotated', marker: 'work-token' },
214
+ { aud: SUBSTRATE_AUD, refreshToken: 'personal-rotated', marker: 'personal-token' },
215
+ ])
216
+
217
+ const provider = new TeamsTokenProvider(manager)
218
+ // work mints + caches (identity set), personal switch resets identity,
219
+ // then work again is served from cache and must re-restore identity
220
+ await provider.bindAccount('work').getSubstrateToken()
221
+ await provider.bindAccount('personal').getSubstrateToken()
222
+ await provider.bindAccount('work').getSubstrateToken()
223
+
224
+ expect(await provider.getTenantId()).toBe(TENANT_ID)
225
+ expect(await provider.getUserId()).toBe(USER_ID)
226
+ })
227
+ })
@@ -0,0 +1,284 @@
1
+ import {
2
+ AAD_AUDIENCE_GRAPH,
3
+ AAD_AUDIENCE_SUBSTRATE,
4
+ AAD_SCOPE_GRAPH,
5
+ AAD_SCOPE_SUBSTRATE,
6
+ consumerTokenUrl,
7
+ getTeamsAppClientId,
8
+ organizationsTokenUrl,
9
+ TEAMS_WEB_ORIGIN,
10
+ } from './app-config'
11
+ import { TeamsCredentialManager } from './credential-manager'
12
+ import type { TeamsAccount, TeamsAccountType } from './types'
13
+ import { TeamsAuthCapabilityError, TeamsError } from './types'
14
+
15
+ export type TokenAudience = 'skype' | 'substrate' | 'graph'
16
+
17
+ type BearerAudience = Exclude<TokenAudience, 'skype'>
18
+
19
+ interface CachedToken {
20
+ accessToken: string
21
+ expiresAt: number
22
+ aud: string
23
+ tenantId?: string
24
+ userId?: string
25
+ }
26
+
27
+ type CacheKey = TokenAudience | `${TeamsAccountType}:${BearerAudience}`
28
+
29
+ interface CurrentAccountContext {
30
+ accountType: TeamsAccountType
31
+ account: TeamsAccount
32
+ }
33
+
34
+ interface BearerAudienceConfig {
35
+ scope: string
36
+ aud: string
37
+ }
38
+
39
+ interface JwtClaims {
40
+ aud?: string
41
+ tid?: string
42
+ oid?: string
43
+ exp?: number
44
+ [key: string]: unknown
45
+ }
46
+
47
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000
48
+ const DEFAULT_ACCESS_TOKEN_TTL_SEC = 3600
49
+
50
+ const BEARER_AUDIENCES: Record<BearerAudience, BearerAudienceConfig> = {
51
+ substrate: { scope: AAD_SCOPE_SUBSTRATE, aud: AAD_AUDIENCE_SUBSTRATE },
52
+ graph: { scope: AAD_SCOPE_GRAPH, aud: AAD_AUDIENCE_GRAPH },
53
+ }
54
+
55
+ export class TeamsTokenProvider {
56
+ private cache: Map<CacheKey, CachedToken> = new Map()
57
+ private tenantId?: string
58
+ private userId?: string
59
+ private boundAccountType?: TeamsAccountType
60
+ private lastAccountType?: TeamsAccountType
61
+
62
+ constructor(private credManager: TeamsCredentialManager = new TeamsCredentialManager()) {}
63
+
64
+ bindAccount(accountType: TeamsAccountType): this {
65
+ this.boundAccountType = accountType
66
+ return this
67
+ }
68
+
69
+ async getSkypeToken(): Promise<string> {
70
+ const cached = this.getCached('skype')
71
+ if (cached) return cached.accessToken
72
+
73
+ const cred = await this.credManager.getTokenWithExpiry()
74
+ if (!cred?.token) {
75
+ throw new TeamsError(
76
+ 'No Teams credentials found. Run `agent-teams auth login` or `agent-teams auth extract`.',
77
+ 'no_credentials',
78
+ )
79
+ }
80
+
81
+ const expiresAt = cred.tokenExpiresAt ? new Date(cred.tokenExpiresAt).getTime() : Number.POSITIVE_INFINITY
82
+ if (expiresAt <= Date.now()) {
83
+ throw new TeamsError('Token has expired. Run "auth login" or "auth extract" to refresh.', 'token_expired')
84
+ }
85
+
86
+ this.cache.set('skype', { accessToken: cred.token, expiresAt, aud: 'skype' })
87
+ return cred.token
88
+ }
89
+
90
+ async getSubstrateToken(): Promise<string> {
91
+ return this.getBearerToken('substrate')
92
+ }
93
+
94
+ async getGraphToken(): Promise<string> {
95
+ return this.getBearerToken('graph')
96
+ }
97
+
98
+ async getTenantId(): Promise<string> {
99
+ await this.ensureIdentity()
100
+ if (!this.tenantId) {
101
+ throw new TeamsError(
102
+ 'Microsoft token did not include a tenant id needed for Teams search.',
103
+ 'aad_identity_missing',
104
+ )
105
+ }
106
+ return this.tenantId
107
+ }
108
+
109
+ async getUserId(): Promise<string> {
110
+ await this.ensureIdentity()
111
+ if (!this.userId) {
112
+ throw new TeamsError('Microsoft token did not include a user id needed for Teams search.', 'aad_identity_missing')
113
+ }
114
+ return this.userId
115
+ }
116
+
117
+ private getCached(key: CacheKey): CachedToken | undefined {
118
+ const cached = this.cache.get(key)
119
+ if (!cached) return undefined
120
+ return cached.expiresAt - Date.now() > REFRESH_BUFFER_MS ? cached : undefined
121
+ }
122
+
123
+ private async getBearerToken(audience: BearerAudience): Promise<string> {
124
+ const current = await this.getCurrentAccount()
125
+ this.resetIdentityIfAccountChanged(current.accountType)
126
+ const cacheKey: CacheKey = `${current.accountType}:${audience}`
127
+ const cached = this.getCached(cacheKey)
128
+ if (cached) {
129
+ // a cache hit after an account switch reset our identity, so restore the
130
+ // tenant/user claims captured when this token was minted
131
+ this.tenantId = cached.tenantId ?? this.tenantId
132
+ this.userId = cached.userId ?? this.userId
133
+ return cached.accessToken
134
+ }
135
+
136
+ if (!current.account.aad_refresh_token) {
137
+ throw new TeamsAuthCapabilityError()
138
+ }
139
+
140
+ const clientId = current.account.aad_client_id ?? getTeamsAppClientId(current.accountType).clientId
141
+ const config = BEARER_AUDIENCES[audience]
142
+ const token = await this.refreshBearerToken({
143
+ accountType: current.accountType,
144
+ clientId,
145
+ refreshToken: current.account.aad_refresh_token,
146
+ audience,
147
+ config,
148
+ })
149
+
150
+ this.cache.set(cacheKey, token)
151
+ return token.accessToken
152
+ }
153
+
154
+ private async refreshBearerToken(params: {
155
+ accountType: TeamsAccountType
156
+ clientId: string
157
+ refreshToken: string
158
+ audience: BearerAudience
159
+ config: BearerAudienceConfig
160
+ }): Promise<CachedToken> {
161
+ const response = await fetch(tokenUrlForAccount(params.accountType), {
162
+ method: 'POST',
163
+ headers: {
164
+ 'Content-Type': 'application/x-www-form-urlencoded',
165
+ Origin: TEAMS_WEB_ORIGIN,
166
+ },
167
+ body: new URLSearchParams({
168
+ grant_type: 'refresh_token',
169
+ client_id: params.clientId,
170
+ refresh_token: params.refreshToken,
171
+ scope: params.config.scope,
172
+ }),
173
+ })
174
+
175
+ const body = (await response.json().catch(() => ({}))) as Record<string, unknown>
176
+ if (!response.ok) {
177
+ throw new TeamsError(
178
+ `Microsoft token refresh failed: ${describeAadError(body, response.status)}`,
179
+ 'aad_refresh_failed',
180
+ )
181
+ }
182
+
183
+ const accessToken = stringField(body, 'access_token')
184
+ if (!accessToken) {
185
+ throw new TeamsError('Microsoft token refresh response did not include an access token.', 'aad_access_missing')
186
+ }
187
+
188
+ const claims = decodeJwt(accessToken)
189
+ if (claims.aud !== params.config.aud) {
190
+ throw new TeamsError(
191
+ `Microsoft token audience mismatch: expected ${params.config.aud}, got ${claims.aud ?? 'missing'}.`,
192
+ 'aad_audience_mismatch',
193
+ )
194
+ }
195
+
196
+ this.tenantId = claims.tid ?? this.tenantId
197
+ this.userId = claims.oid ?? this.userId
198
+
199
+ const rotatedRefreshToken = stringField(body, 'refresh_token')
200
+ if (rotatedRefreshToken) {
201
+ await this.credManager.updateAadRefreshToken(params.accountType, rotatedRefreshToken, params.clientId)
202
+ }
203
+
204
+ const expiresIn = numberField(body, 'expires_in') ?? DEFAULT_ACCESS_TOKEN_TTL_SEC
205
+ return {
206
+ accessToken,
207
+ expiresAt: Date.now() + expiresIn * 1000,
208
+ aud: params.config.aud,
209
+ tenantId: claims.tid,
210
+ userId: claims.oid,
211
+ }
212
+ }
213
+
214
+ private async getCurrentAccount(): Promise<CurrentAccountContext> {
215
+ const config = await this.credManager.loadConfig()
216
+ if (!config) {
217
+ throw new TeamsError(
218
+ 'No Teams credentials found. Run `agent-teams auth login` or `agent-teams auth extract`.',
219
+ 'no_credentials',
220
+ )
221
+ }
222
+ const accountKey = this.boundAccountType ?? TeamsCredentialManager.accountOverride ?? config.current_account
223
+ if (accountKey !== 'work' && accountKey !== 'personal') {
224
+ throw new TeamsError(
225
+ 'No active Teams account selected. Run `agent-teams auth switch-account`.',
226
+ 'no_current_account',
227
+ )
228
+ }
229
+ const account = config.accounts[accountKey]
230
+ if (!account) {
231
+ throw new TeamsError(
232
+ 'No active Teams account selected. Run `agent-teams auth switch-account`.',
233
+ 'no_current_account',
234
+ )
235
+ }
236
+ return { accountType: accountKey, account }
237
+ }
238
+
239
+ private resetIdentityIfAccountChanged(accountType: TeamsAccountType): void {
240
+ if (this.lastAccountType === accountType) return
241
+ this.tenantId = undefined
242
+ this.userId = undefined
243
+ this.lastAccountType = accountType
244
+ }
245
+
246
+ private async ensureIdentity(): Promise<void> {
247
+ if (this.tenantId && this.userId) return
248
+ await this.getSubstrateToken()
249
+ }
250
+ }
251
+
252
+ function tokenUrlForAccount(accountType: TeamsAccountType): string {
253
+ return accountType === 'personal' ? consumerTokenUrl() : organizationsTokenUrl()
254
+ }
255
+
256
+ function decodeJwt(token: string): JwtClaims {
257
+ const payload = token.split('.')[1]
258
+ if (!payload) {
259
+ throw new TeamsError('Microsoft token response was not a JWT.', 'aad_token_invalid')
260
+ }
261
+ const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record<string, unknown>
262
+ return {
263
+ ...decoded,
264
+ aud: typeof decoded.aud === 'string' ? decoded.aud : undefined,
265
+ tid: typeof decoded.tid === 'string' ? decoded.tid : undefined,
266
+ oid: typeof decoded.oid === 'string' ? decoded.oid : undefined,
267
+ exp: typeof decoded.exp === 'number' ? decoded.exp : undefined,
268
+ }
269
+ }
270
+
271
+ function stringField(record: Record<string, unknown>, key: string): string | undefined {
272
+ const value = record[key]
273
+ return typeof value === 'string' ? value : undefined
274
+ }
275
+
276
+ function numberField(record: Record<string, unknown>, key: string): number | undefined {
277
+ const value = record[key]
278
+ return typeof value === 'number' ? value : undefined
279
+ }
280
+
281
+ function describeAadError(body: Record<string, unknown>, status: number): string {
282
+ const desc = body.error_description ?? body.error
283
+ return typeof desc === 'string' ? desc.split('\n')[0] : `HTTP ${status}`
284
+ }