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,285 @@
1
+ import { EventEmitter } from 'node:events'
2
+ import tls from 'node:tls'
3
+
4
+ const MQTTOT_HOST = 'edge-mqtt.facebook.com'
5
+ const MQTTOT_PORT = 443
6
+ const PROTOCOL_NAME = 'MQTToT'
7
+ const PROTOCOL_LEVEL = 3
8
+
9
+ // CONNECT flags 0xC2 = username present + clean session; the "password" travels
10
+ // inside the Thrift payload rather than the standard MQTT password field.
11
+ const CONNECT_FLAGS = 0xc2
12
+ const KEEPALIVE_SECONDS = 60
13
+ const CONNACK_TIMEOUT_MS = 15_000
14
+
15
+ const PacketType = {
16
+ Connect: 1,
17
+ ConnAck: 2,
18
+ Publish: 3,
19
+ PubAck: 4,
20
+ Subscribe: 8,
21
+ PingReq: 12,
22
+ PingResp: 13,
23
+ Disconnect: 14,
24
+ } as const
25
+
26
+ export interface MqttPublish {
27
+ topic: string
28
+ payload: Buffer
29
+ }
30
+
31
+ export interface MqttTransportEventMap {
32
+ connect: []
33
+ publish: [MqttPublish]
34
+ error: [Error]
35
+ close: []
36
+ }
37
+
38
+ type EventKey = keyof MqttTransportEventMap
39
+
40
+ export class MqttTransport {
41
+ private socket: tls.TLSSocket | null = null
42
+ private emitter = new EventEmitter()
43
+ private buffer = Buffer.alloc(0)
44
+ private nextPacketId = 1
45
+ private pingTimer: ReturnType<typeof setInterval> | null = null
46
+ private connackTimer: ReturnType<typeof setTimeout> | null = null
47
+ private connackTimeoutMs: number
48
+
49
+ constructor(options: { connackTimeoutMs?: number } = {}) {
50
+ this.connackTimeoutMs = options.connackTimeoutMs ?? CONNACK_TIMEOUT_MS
51
+ }
52
+
53
+ connect(connectPayload: Buffer): void {
54
+ const socket = tls.connect({ host: MQTTOT_HOST, port: MQTTOT_PORT, servername: MQTTOT_HOST }, () => {
55
+ socket.write(this.buildConnectPacket(connectPayload))
56
+ })
57
+ this.socket = socket
58
+
59
+ // Guard against a peer that accepts TLS but never sends CONNACK, which would
60
+ // otherwise leave a hybrid caller waiting forever instead of failing over.
61
+ this.connackTimer = setTimeout(() => {
62
+ this.connackTimer = null
63
+ this.emitter.emit('error', new Error('MQTToT CONNACK timed out'))
64
+ socket.destroy()
65
+ }, this.connackTimeoutMs)
66
+
67
+ socket.on('data', (chunk: Buffer) => this.onData(chunk))
68
+ socket.on('error', (err: Error) => this.emitter.emit('error', err))
69
+ socket.on('close', () => {
70
+ this.clearConnackTimer()
71
+ this.stopPing()
72
+ this.emitter.emit('close')
73
+ })
74
+ }
75
+
76
+ publish(topic: string, payload: Buffer, qos: 0 | 1 = 1): void {
77
+ if (!this.socket) throw new Error('MQTT transport not connected')
78
+ this.socket.write(this.buildPublishPacket(topic, payload, qos))
79
+ }
80
+
81
+ subscribe(topics: string[]): void {
82
+ if (!this.socket) throw new Error('MQTT transport not connected')
83
+ this.socket.write(this.buildSubscribePacket(topics))
84
+ }
85
+
86
+ disconnect(): void {
87
+ this.clearConnackTimer()
88
+ this.stopPing()
89
+ if (this.socket) {
90
+ try {
91
+ this.socket.write(Buffer.from([PacketType.Disconnect << 4, 0]))
92
+ } catch {
93
+ // socket may already be torn down; closing below is sufficient
94
+ }
95
+ this.socket.destroy()
96
+ this.socket = null
97
+ }
98
+ }
99
+
100
+ on<K extends EventKey>(event: K, listener: (...args: MqttTransportEventMap[K]) => void): this {
101
+ this.emitter.on(event, listener as (...args: unknown[]) => void)
102
+ return this
103
+ }
104
+
105
+ private onData(chunk: Buffer): void {
106
+ this.buffer = Buffer.concat([this.buffer, chunk])
107
+ this.drainPackets()
108
+ }
109
+
110
+ private drainPackets(): void {
111
+ while (this.buffer.length >= 2) {
112
+ const decoded = decodeRemainingLength(this.buffer, 1)
113
+ if (!decoded) return
114
+
115
+ const { length: remainingLength, bytesUsed } = decoded
116
+ const totalLength = 1 + bytesUsed + remainingLength
117
+ if (this.buffer.length < totalLength) return
118
+
119
+ const packet = this.buffer.subarray(0, totalLength)
120
+ this.buffer = this.buffer.subarray(totalLength)
121
+ this.handlePacket(packet, 1 + bytesUsed)
122
+ }
123
+ }
124
+
125
+ private handlePacket(packet: Buffer, bodyStart: number): void {
126
+ const type = (packet[0]! >> 4) & 0x0f
127
+
128
+ switch (type) {
129
+ case PacketType.ConnAck: {
130
+ this.clearConnackTimer()
131
+ const returnCode = packet[bodyStart + 1]
132
+ if (returnCode === 0) {
133
+ this.startPing()
134
+ this.emitter.emit('connect')
135
+ } else {
136
+ this.emitter.emit('error', new Error(`MQTToT CONNACK refused with return code ${returnCode}`))
137
+ }
138
+ break
139
+ }
140
+ case PacketType.Publish:
141
+ this.handlePublish(packet, bodyStart)
142
+ break
143
+ case PacketType.PingResp:
144
+ case PacketType.PubAck:
145
+ case PacketType.Subscribe + 1:
146
+ break
147
+ }
148
+ }
149
+
150
+ private handlePublish(packet: Buffer, bodyStart: number): void {
151
+ const qos = (packet[0]! >> 1) & 0x03
152
+
153
+ // A truncated/malformed frame must not throw out of the data handler, or it
154
+ // would crash the process instead of letting the listener fail over.
155
+ if (bodyStart + 2 > packet.length) return
156
+ const topicLength = packet.readUInt16BE(bodyStart)
157
+ let offset = bodyStart + 2
158
+ if (offset + topicLength > packet.length) return
159
+ const topic = packet.toString('utf8', offset, offset + topicLength)
160
+ offset += topicLength
161
+
162
+ let packetId: number | null = null
163
+ if (qos > 0) {
164
+ if (offset + 2 > packet.length) return
165
+ packetId = packet.readUInt16BE(offset)
166
+ offset += 2
167
+ }
168
+
169
+ const payload = packet.subarray(offset)
170
+ this.emitter.emit('publish', { topic, payload })
171
+
172
+ if (qos === 1 && packetId !== null) {
173
+ this.sendPubAck(packetId)
174
+ }
175
+ }
176
+
177
+ private sendPubAck(packetId: number): void {
178
+ const packet = Buffer.from([PacketType.PubAck << 4, 2, (packetId >> 8) & 0xff, packetId & 0xff])
179
+ this.socket?.write(packet)
180
+ }
181
+
182
+ private startPing(): void {
183
+ this.stopPing()
184
+ this.pingTimer = setInterval(() => {
185
+ this.socket?.write(Buffer.from([PacketType.PingReq << 4, 0]))
186
+ }, KEEPALIVE_SECONDS * 1000)
187
+ }
188
+
189
+ private stopPing(): void {
190
+ if (this.pingTimer) {
191
+ clearInterval(this.pingTimer)
192
+ this.pingTimer = null
193
+ }
194
+ }
195
+
196
+ private clearConnackTimer(): void {
197
+ if (this.connackTimer) {
198
+ clearTimeout(this.connackTimer)
199
+ this.connackTimer = null
200
+ }
201
+ }
202
+
203
+ private buildConnectPacket(payload: Buffer): Buffer {
204
+ const variableHeader: number[] = []
205
+ appendString(variableHeader, PROTOCOL_NAME)
206
+ variableHeader.push(PROTOCOL_LEVEL, CONNECT_FLAGS)
207
+ variableHeader.push((KEEPALIVE_SECONDS >> 8) & 0xff, KEEPALIVE_SECONDS & 0xff)
208
+
209
+ const body = Buffer.concat([Buffer.from(variableHeader), payload])
210
+ return Buffer.concat([Buffer.from([PacketType.Connect << 4, ...encodeRemainingLength(body.length)]), body])
211
+ }
212
+
213
+ private buildPublishPacket(topic: string, payload: Buffer, qos: 0 | 1): Buffer {
214
+ const variableHeader: number[] = []
215
+ appendString(variableHeader, topic)
216
+ if (qos > 0) {
217
+ const packetId = this.allocatePacketId()
218
+ variableHeader.push((packetId >> 8) & 0xff, packetId & 0xff)
219
+ }
220
+
221
+ const body = Buffer.concat([Buffer.from(variableHeader), payload])
222
+ const flags = qos << 1
223
+ return Buffer.concat([
224
+ Buffer.from([(PacketType.Publish << 4) | flags, ...encodeRemainingLength(body.length)]),
225
+ body,
226
+ ])
227
+ }
228
+
229
+ private buildSubscribePacket(topics: string[]): Buffer {
230
+ const packetId = this.allocatePacketId()
231
+ const body: number[] = [(packetId >> 8) & 0xff, packetId & 0xff]
232
+ for (const topic of topics) {
233
+ appendString(body, topic)
234
+ body.push(0)
235
+ }
236
+ // SUBSCRIBE has reserved flags 0010 in the fixed-header low nibble.
237
+ return Buffer.concat([
238
+ Buffer.from([(PacketType.Subscribe << 4) | 0x02, ...encodeRemainingLength(body.length)]),
239
+ Buffer.from(body),
240
+ ])
241
+ }
242
+
243
+ private allocatePacketId(): number {
244
+ const id = this.nextPacketId
245
+ this.nextPacketId = (this.nextPacketId % 0xffff) + 1
246
+ return id
247
+ }
248
+ }
249
+
250
+ function appendString(target: number[], value: string): void {
251
+ const buf = Buffer.from(value, 'utf8')
252
+ target.push((buf.length >> 8) & 0xff, buf.length & 0xff)
253
+ for (const b of buf) target.push(b)
254
+ }
255
+
256
+ function encodeRemainingLength(length: number): number[] {
257
+ const bytes: number[] = []
258
+ let value = length
259
+ do {
260
+ let digit = value & 0x7f
261
+ value >>= 7
262
+ if (value > 0) digit |= 0x80
263
+ bytes.push(digit)
264
+ } while (value > 0)
265
+ return bytes
266
+ }
267
+
268
+ function decodeRemainingLength(buffer: Buffer, offset: number): { length: number; bytesUsed: number } | null {
269
+ let length = 0
270
+ let multiplier = 1
271
+ let bytesUsed = 0
272
+
273
+ while (offset + bytesUsed < buffer.length) {
274
+ const byte = buffer[offset + bytesUsed]!
275
+ length += (byte & 0x7f) * multiplier
276
+ bytesUsed++
277
+ if ((byte & 0x80) === 0) {
278
+ return { length, bytesUsed }
279
+ }
280
+ multiplier *= 128
281
+ if (bytesUsed > 4) return null
282
+ }
283
+
284
+ return null
285
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { InstagramRealtimeListener } from '@/platforms/instagram/realtime-listener'
4
+
5
+ function makeClient(overrides: Record<string, unknown> = {}) {
6
+ return {
7
+ getUserId: () => '123',
8
+ getSessionState: () => ({}) as never,
9
+ fetchIrisBootstrap: async () => ({ seqId: 1, snapshotAtMs: 1 }),
10
+ ...overrides,
11
+ } as never
12
+ }
13
+
14
+ describe('InstagramRealtimeListener', () => {
15
+ it('stays restartable after a bootstrap failure', async () => {
16
+ let calls = 0
17
+ const client = makeClient({
18
+ fetchIrisBootstrap: async () => {
19
+ calls++
20
+ throw new Error('bootstrap failed')
21
+ },
22
+ })
23
+ const listener = new InstagramRealtimeListener(client)
24
+
25
+ // given a failing bootstrap, when start() rejects, then running must be reset
26
+ await expect(listener.start()).rejects.toThrow('bootstrap failed')
27
+
28
+ // when start() is called again, then it must retry rather than no-op at the guard
29
+ await expect(listener.start()).rejects.toThrow('bootstrap failed')
30
+ expect(calls).toBe(2)
31
+
32
+ listener.stop()
33
+ })
34
+ })
@@ -0,0 +1,201 @@
1
+ import { EventEmitter } from 'node:events'
2
+ import { deflateSync, inflateSync } from 'node:zlib'
3
+
4
+ import type { InstagramClient } from './client'
5
+ import { buildConnectPayload } from './mqtt/connection'
6
+ import { MqttTransport } from './mqtt/transport'
7
+ import { extractMediaUrl, extractMessageText, getMessageType, type InstagramMessageSummary } from './types'
8
+
9
+ export interface InstagramRealtimeListenerEventMap {
10
+ message: [InstagramMessageSummary]
11
+ error: [Error]
12
+ connected: [{ userId: string }]
13
+ disconnected: []
14
+ }
15
+
16
+ export interface InstagramRealtimeListenerOptions {
17
+ connackTimeoutMs?: number
18
+ }
19
+
20
+ type EventKey = keyof InstagramRealtimeListenerEventMap
21
+
22
+ const TOPIC_PUBSUB = '88'
23
+ const TOPIC_REALTIME_SUB = '149'
24
+ const TOPIC_IRIS_SUB = '134'
25
+ const TOPIC_MESSAGE_SYNC = '146'
26
+
27
+ const ZLIB_MAGIC_BYTE = 0x78
28
+
29
+ const DM_THREAD_PATH_PATTERN = /^\/direct_v2\/(?:inbox\/)?threads\//
30
+ const THREAD_ID_PATTERN = /^\/direct_v2\/(?:inbox\/)?threads\/(\d+)/
31
+
32
+ interface IrisEnvelope {
33
+ data?: IrisPatch[]
34
+ }
35
+
36
+ interface IrisPatch {
37
+ op?: string
38
+ path?: string
39
+ value?: string
40
+ }
41
+
42
+ export class InstagramRealtimeListener {
43
+ private client: InstagramClient
44
+ private transport: MqttTransport | null = null
45
+ private emitter = new EventEmitter()
46
+ private running = false
47
+ private userId: string
48
+ private connackTimeoutMs: number | undefined
49
+
50
+ constructor(client: InstagramClient, options: InstagramRealtimeListenerOptions = {}) {
51
+ this.client = client
52
+ this.userId = client.getUserId() ?? ''
53
+ this.connackTimeoutMs = options.connackTimeoutMs
54
+ }
55
+
56
+ async start(): Promise<void> {
57
+ if (this.running) return
58
+ this.running = true
59
+
60
+ try {
61
+ const session = this.client.getSessionState()
62
+ const iris = await this.client.fetchIrisBootstrap()
63
+
64
+ if (!this.running) return
65
+
66
+ const connectPayload = buildConnectPayload(session)
67
+
68
+ const transport = new MqttTransport({ connackTimeoutMs: this.connackTimeoutMs })
69
+ this.transport = transport
70
+
71
+ transport.on('connect', () => {
72
+ this.sendSubscriptions(iris)
73
+ this.emitter.emit('connected', { userId: this.userId })
74
+ })
75
+ transport.on('publish', ({ topic, payload }) => this.handlePublish(topic, payload))
76
+ transport.on('error', (err) => this.emitter.emit('error', err))
77
+ transport.on('close', () => {
78
+ if (this.running) this.emitter.emit('disconnected')
79
+ })
80
+
81
+ transport.connect(connectPayload)
82
+ } catch (error) {
83
+ // Any setup failure must leave the listener cleanly restartable: reset running
84
+ // and discard the partial transport so a later start() does not no-op at the guard.
85
+ this.running = false
86
+ if (this.transport) {
87
+ this.transport.disconnect()
88
+ this.transport = null
89
+ }
90
+ throw error
91
+ }
92
+ }
93
+
94
+ stop(): void {
95
+ this.running = false
96
+ if (this.transport) {
97
+ this.transport.disconnect()
98
+ this.transport = null
99
+ }
100
+ this.emitter.emit('disconnected')
101
+ }
102
+
103
+ on<K extends EventKey>(event: K, listener: (...args: InstagramRealtimeListenerEventMap[K]) => void): this {
104
+ this.emitter.on(event, listener as (...args: unknown[]) => void)
105
+ return this
106
+ }
107
+
108
+ off<K extends EventKey>(event: K, listener: (...args: InstagramRealtimeListenerEventMap[K]) => void): this {
109
+ this.emitter.off(event, listener as (...args: unknown[]) => void)
110
+ return this
111
+ }
112
+
113
+ once<K extends EventKey>(event: K, listener: (...args: InstagramRealtimeListenerEventMap[K]) => void): this {
114
+ this.emitter.once(event, listener as (...args: unknown[]) => void)
115
+ return this
116
+ }
117
+
118
+ private sendSubscriptions(iris: { seqId: number; snapshotAtMs: number }): void {
119
+ const transport = this.transport
120
+ if (!transport) return
121
+
122
+ transport.publish(
123
+ TOPIC_IRIS_SUB,
124
+ deflateJson({
125
+ seq_id: iris.seqId,
126
+ snapshot_at_ms: iris.snapshotAtMs,
127
+ snapshot_app_version: APP_VERSION,
128
+ }),
129
+ )
130
+
131
+ transport.publish(TOPIC_PUBSUB, deflateJson({ sub: [`ig/u/v1/${this.userId}`] }))
132
+
133
+ transport.publish(
134
+ TOPIC_REALTIME_SUB,
135
+ deflateJson({
136
+ sub: [`1/graphqlsubscriptions/17867973967082385/{"input_data":{"user_id":"${this.userId}"}}`],
137
+ }),
138
+ )
139
+ }
140
+
141
+ private handlePublish(topic: string, payload: Buffer): void {
142
+ if (topic !== TOPIC_MESSAGE_SYNC) return
143
+
144
+ let envelopes: IrisEnvelope[]
145
+ try {
146
+ envelopes = JSON.parse(inflateIfNeeded(payload).toString('utf8')) as IrisEnvelope[]
147
+ } catch (error) {
148
+ this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
149
+ return
150
+ }
151
+
152
+ for (const envelope of envelopes) {
153
+ for (const patch of envelope.data ?? []) {
154
+ const message = this.decodeDmPatch(patch)
155
+ if (message) this.emitter.emit('message', message)
156
+ }
157
+ }
158
+ }
159
+
160
+ private decodeDmPatch(patch: IrisPatch): InstagramMessageSummary | null {
161
+ if (patch.op !== 'add') return null
162
+ if (!patch.path || !patch.value || !DM_THREAD_PATH_PATTERN.test(patch.path)) return null
163
+
164
+ const threadId = patch.path.match(THREAD_ID_PATTERN)?.[1] ?? ''
165
+
166
+ let item: Record<string, unknown>
167
+ try {
168
+ item = JSON.parse(patch.value) as Record<string, unknown>
169
+ } catch {
170
+ return null
171
+ }
172
+
173
+ const itemId = String(item['item_id'] ?? '')
174
+ const fromUserId = String(item['user_id'] ?? '')
175
+ const timestampUs = item['timestamp'] as number | string | undefined
176
+
177
+ return {
178
+ id: itemId,
179
+ thread_id: threadId,
180
+ from: fromUserId,
181
+ timestamp: timestampUs ? new Date(Number(timestampUs) / 1000).toISOString() : new Date().toISOString(),
182
+ is_outgoing: fromUserId === this.userId,
183
+ type: getMessageType(item),
184
+ text: extractMessageText(item),
185
+ media_url: extractMediaUrl(item),
186
+ }
187
+ }
188
+ }
189
+
190
+ const APP_VERSION = '312.1.0.34.111'
191
+
192
+ function deflateJson(value: unknown): Buffer {
193
+ return deflateSync(JSON.stringify(value), { level: 9 })
194
+ }
195
+
196
+ function inflateIfNeeded(payload: Buffer): Buffer {
197
+ if (payload.length > 0 && payload[0] === ZLIB_MAGIC_BYTE) {
198
+ return inflateSync(payload)
199
+ }
200
+ return payload
201
+ }
@@ -60,19 +60,22 @@ export interface InstagramMessageSummary {
60
60
  media_url?: string
61
61
  }
62
62
 
63
+ export interface InstagramDevice {
64
+ phone_id: string
65
+ uuid: string
66
+ android_device_id: string
67
+ advertising_id: string
68
+ client_session_id: string
69
+ device_string: string
70
+ }
71
+
63
72
  export interface InstagramSessionState {
64
73
  cookies: string
65
- device: {
66
- phone_id: string
67
- uuid: string
68
- android_device_id: string
69
- advertising_id: string
70
- client_session_id: string
71
- device_string: string
72
- }
74
+ device: InstagramDevice
73
75
  authorization?: string
74
76
  user_id?: string
75
77
  mid?: string
78
+ www_claim?: string
76
79
  challenge_path?: string
77
80
  }
78
81
 
@@ -0,0 +1,92 @@
1
+ import type { TeamsAccountType } from './types'
2
+
3
+ export const TEAMS_DESKTOP_CLIENT_ID = '1fec8e78-bce4-4aaf-ab1b-5451cc387264'
4
+ export const TEAMS_WEB_CLIENT_ID = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346'
5
+
6
+ export const CONSUMER_TENANT_ID = '9188040d-6c67-4c5b-b112-36a304b66dad'
7
+ // The Microsoft Services (MSA passthrough) tenant. Consumer/MSA tokens for the
8
+ // Skype resource carry this `tid`, so it is a placeholder that still requires
9
+ // tenant discovery before the skype-token exchange.
10
+ export const MICROSOFT_SERVICES_TENANT_ID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a'
11
+ export const WORK_TENANT_ID = 'organizations'
12
+
13
+ export const DEVICE_CODE_SCOPE_TEAMS = 'service::api.fl.teams.microsoft.com::MBI_SSL openid profile offline_access'
14
+ export const DEVICE_CODE_SCOPE_SKYPE = 'service::api.fl.spaces.skype.com::MBI_SSL openid profile offline_access'
15
+ export const WORK_DEVICE_CODE_SCOPE_TEAMS = 'https://api.spaces.skype.com/.default openid profile offline_access'
16
+ export const WORK_DEVICE_CODE_SCOPE_SKYPE = 'https://api.spaces.skype.com/.default openid profile offline_access'
17
+
18
+ export const SUBSTRATE_SEARCH_URL = 'https://substrate.office.com/searchservice/api/v2/query'
19
+ export const TEAMS_WEB_ORIGIN = 'https://teams.microsoft.com'
20
+ export const AAD_SCOPE_SUBSTRATE = 'https://substrate.office.com/.default offline_access'
21
+ export const AAD_SCOPE_GRAPH = 'https://graph.microsoft.com/.default offline_access'
22
+ export const AAD_AUDIENCE_SUBSTRATE = 'https://substrate.office.com'
23
+ export const AAD_AUDIENCE_GRAPH = 'https://graph.microsoft.com'
24
+
25
+ export const AUTHZ_CONSUMER_URL = 'https://teams.live.com/api/auth/v1.0/authz/consumer'
26
+ export const AUTHZ_WORK_URL = 'https://teams.microsoft.com/api/authsvc/v1.0/authz'
27
+
28
+ // Control-plane tenant-discovery endpoint. The region segment is not
29
+ // authoritative for this call (it lists every tenant the user belongs to
30
+ // regardless of home region), so we pin the value fossteams uses.
31
+ export const TEAMS_TENANTS_URL = 'https://teams.microsoft.com/api/mt/emea/beta/users/tenants'
32
+
33
+ export const GET_CREDENTIAL_TYPE_URL = 'https://login.microsoftonline.com/common/GetCredentialType'
34
+
35
+ export function consumerDeviceCodeUrl(): string {
36
+ return `https://login.microsoftonline.com/${CONSUMER_TENANT_ID}/oauth2/v2.0/devicecode`
37
+ }
38
+
39
+ export function consumerTokenUrl(): string {
40
+ return `https://login.microsoftonline.com/${CONSUMER_TENANT_ID}/oauth2/v2.0/token`
41
+ }
42
+
43
+ export function organizationsDeviceCodeUrl(): string {
44
+ return `https://login.microsoftonline.com/${WORK_TENANT_ID}/oauth2/v2.0/devicecode`
45
+ }
46
+
47
+ export function organizationsTokenUrl(): string {
48
+ return `https://login.microsoftonline.com/${WORK_TENANT_ID}/oauth2/v2.0/token`
49
+ }
50
+
51
+ export function tenantTokenUrl(tenantId: string): string {
52
+ return `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`
53
+ }
54
+
55
+ export function deviceCodeUrl(accountType: TeamsAccountType): string {
56
+ return accountType === 'personal' ? consumerDeviceCodeUrl() : organizationsDeviceCodeUrl()
57
+ }
58
+
59
+ export function deviceCodeTokenUrl(accountType: TeamsAccountType, tenantId?: string): string {
60
+ if (tenantId) return tenantTokenUrl(tenantId)
61
+ return accountType === 'personal' ? consumerTokenUrl() : organizationsTokenUrl()
62
+ }
63
+
64
+ export function deviceCodeTeamsScope(accountType: TeamsAccountType): string {
65
+ return accountType === 'personal' ? DEVICE_CODE_SCOPE_TEAMS : WORK_DEVICE_CODE_SCOPE_TEAMS
66
+ }
67
+
68
+ export function deviceCodeSkypeScope(accountType: TeamsAccountType): string {
69
+ return accountType === 'personal' ? DEVICE_CODE_SCOPE_SKYPE : WORK_DEVICE_CODE_SCOPE_SKYPE
70
+ }
71
+
72
+ export function authzUrl(accountType: TeamsAccountType): string {
73
+ return accountType === 'personal' ? AUTHZ_CONSUMER_URL : AUTHZ_WORK_URL
74
+ }
75
+
76
+ export interface TeamsAppClient {
77
+ clientId: string
78
+ source: 'env' | 'builtin'
79
+ }
80
+
81
+ function parseTrimmed(value: string | undefined): string | undefined {
82
+ const normalized = value?.trim()
83
+ return normalized ? normalized : undefined
84
+ }
85
+
86
+ export function getTeamsAppClientId(accountType: TeamsAccountType = 'personal'): TeamsAppClient {
87
+ const envClientId = parseTrimmed(process.env.AGENT_TEAMS_CLIENT_ID)
88
+ if (envClientId) {
89
+ return { clientId: envClientId, source: 'env' }
90
+ }
91
+ return { clientId: accountType === 'work' ? TEAMS_DESKTOP_CLIENT_ID : TEAMS_WEB_CLIENT_ID, source: 'builtin' }
92
+ }