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
@@ -9,24 +9,101 @@ import {
9
9
  extractMessageText,
10
10
  getMessageType,
11
11
  type InstagramChatSummary,
12
+ type InstagramDevice,
12
13
  type InstagramMessageSummary,
13
14
  type InstagramSessionState,
14
15
  } from './types'
15
16
 
16
17
  const IG_BASE_URL = 'https://i.instagram.com/api/v1'
17
18
  const IG_APP_ID = '567067343352427'
18
- const IG_VERSION = '312.1.0.34.111'
19
- const IG_VERSION_CODE = '556927984'
20
- const ANDROID_VERSION = '13.0'
21
- const ANDROID_RELEASE = '33'
19
+ const IG_VERSION = '428.0.0.47.67'
20
+ const IG_VERSION_CODE = '719358530'
21
+ // Bloks client versioning id; required header for Bloks endpoints (2FA / CAA flows).
22
+ const IG_BLOKS_VERSION_ID = '5f56efad68e1edec7801f630b5c122704ec5378adbee6609a448f105f34a9c73'
23
+ const IG_CAPABILITIES = '3brTv10='
24
+ const IG_LOCALE = 'en_US'
25
+ // Instagram silently holds the socket open (no response) for flagged IPs / fingerprints instead
26
+ // of returning an error, so an unbounded fetch hangs forever. Cap every request and surface a
27
+ // clear, actionable error instead of a frozen process.
28
+ const IG_REQUEST_TIMEOUT_MS = 30_000
29
+ const ANDROID_VERSION = '14'
30
+ const ANDROID_RELEASE = '34'
22
31
  const DEVICE_DPI = '480dpi'
23
- const DEVICE_RESOLUTION = '1080x2340'
24
- const DEVICE_MANUFACTURER = 'samsung'
25
- const DEVICE_MODEL = 'SM-S911B'
26
- const DEVICE_CHIPSET = 'qcom'
32
+ const DEVICE_RESOLUTION = '1344x2992'
33
+ const DEVICE_MANUFACTURER = 'Google/google'
34
+ const DEVICE_MODEL = 'Pixel 8 Pro'
35
+ const DEVICE_NAME = 'husky'
36
+ const DEVICE_CHIPSET = 'husky'
27
37
 
28
38
  export function generateDeviceString(): string {
29
- return `${ANDROID_VERSION}/${ANDROID_RELEASE}; ${DEVICE_DPI}; ${DEVICE_RESOLUTION}; ${DEVICE_MANUFACTURER}; ${DEVICE_MODEL}; ${DEVICE_MODEL}; ${DEVICE_CHIPSET}; en_US; ${IG_VERSION_CODE}`
39
+ return `${ANDROID_RELEASE}/${ANDROID_VERSION}; ${DEVICE_DPI}; ${DEVICE_RESOLUTION}; ${DEVICE_MANUFACTURER}; ${DEVICE_MODEL}; ${DEVICE_NAME}; ${DEVICE_CHIPSET}; en_US; ${IG_VERSION_CODE}`
40
+ }
41
+
42
+ // Instagram no longer verifies the body HMAC; the current mobile API expects a literal
43
+ // "SIGNATURE." prefix (per subzeroid/instagrapi, subzeroid/aiograpi). Sending a real HMAC +
44
+ // ig_sig_key_version is rejected as malformed on /accounts/login/, surfacing as a misleading
45
+ // "account not found" error. URLSearchParams url-encodes the value exactly once.
46
+ function signBody(payload: Record<string, unknown>): Record<string, string> {
47
+ return { signed_body: `SIGNATURE.${JSON.stringify(payload)}` }
48
+ }
49
+
50
+ // jazoest = "2" + sum of ASCII byte values of the input; an anti-bot checksum expected by login.
51
+ function createJazoest(input: string): string {
52
+ let sum = 0
53
+ for (let i = 0; i < input.length; i++) {
54
+ sum += input.charCodeAt(i)
55
+ }
56
+ return `2${sum}`
57
+ }
58
+
59
+ function generateToken(): string {
60
+ return randomBytes(16).toString('hex')
61
+ }
62
+
63
+ function isTimeoutAbort(error: unknown): boolean {
64
+ // AbortSignal.timeout aborts the connect AND the body stream, surfacing as TimeoutError on the
65
+ // fetch and AbortError when a stalled body read is aborted mid-stream; both mean "IG stalled".
66
+ return error instanceof DOMException && (error.name === 'TimeoutError' || error.name === 'AbortError')
67
+ }
68
+
69
+ function stalledError(path: string): InstagramError {
70
+ return new InstagramError(
71
+ `Instagram did not respond within ${IG_REQUEST_TIMEOUT_MS / 1000}s (${path}). The connection stalled, ` +
72
+ 'which usually means the IP is flagged (VPN/datacenter/proxy or prior automated attempts) or the ' +
73
+ 'device fingerprint was rejected. Try "agent-instagram auth extract" from a browser logged into ' +
74
+ 'instagram.com instead of password login.',
75
+ 'request_stalled',
76
+ )
77
+ }
78
+
79
+ interface TimedRequest {
80
+ response: Response
81
+ readText: () => Promise<string>
82
+ }
83
+
84
+ // One AbortSignal.timeout covers the whole request lifecycle: connect, headers, AND the body read.
85
+ // Reading the body through readText() keeps a post-header stall mapped to request_stalled instead
86
+ // of leaking out as a generic parse failure.
87
+ async function fetchWithTimeout(url: string, init: RequestInit, path: string): Promise<TimedRequest> {
88
+ const signal = AbortSignal.timeout(IG_REQUEST_TIMEOUT_MS)
89
+ let response: Response
90
+ try {
91
+ response = await fetch(url, { ...init, signal })
92
+ } catch (error) {
93
+ if (isTimeoutAbort(error)) throw stalledError(path)
94
+ throw error
95
+ }
96
+
97
+ const readText = async (): Promise<string> => {
98
+ try {
99
+ return await response.text()
100
+ } catch (error) {
101
+ if (isTimeoutAbort(error)) throw stalledError(path)
102
+ throw error
103
+ }
104
+ }
105
+
106
+ return { response, readText }
30
107
  }
31
108
 
32
109
  function buildUserAgent(deviceString: string): string {
@@ -37,6 +114,29 @@ export function generateAndroidDeviceId(): string {
37
114
  return `android-${randomBytes(8).toString('hex')}`
38
115
  }
39
116
 
117
+ export function generateDevice(): InstagramDevice {
118
+ return {
119
+ phone_id: randomUUID(),
120
+ uuid: randomUUID(),
121
+ android_device_id: generateAndroidDeviceId(),
122
+ advertising_id: randomUUID(),
123
+ client_session_id: randomUUID(),
124
+ device_string: generateDeviceString(),
125
+ }
126
+ }
127
+
128
+ export function parseOneClickLoginLink(input: string): { uid: string; token: string } | null {
129
+ const trimmed = input.trim()
130
+ let query = trimmed.includes('?') ? trimmed.slice(trimmed.indexOf('?') + 1) : trimmed
131
+ const hashIdx = query.indexOf('#')
132
+ if (hashIdx !== -1) query = query.slice(0, hashIdx)
133
+ const params = new URLSearchParams(query)
134
+ const uid = params.get('uid')
135
+ const token = params.get('token')
136
+ if (uid && token) return { uid, token }
137
+ return null
138
+ }
139
+
40
140
  // Instagram DM timestamps are in microseconds
41
141
  function microsecondsToISO(us: number): string {
42
142
  return new Date(us / 1000).toISOString()
@@ -48,14 +148,40 @@ export class InstagramClient {
48
148
  private sessionPath: string | null = null
49
149
  private userId: string | null = null
50
150
  private cookies: Map<string, string> = new Map()
151
+ private countryCode = '1'
51
152
 
52
153
  constructor(credentialManager?: InstagramCredentialManager) {
53
154
  this.credentialManager = credentialManager ?? new InstagramCredentialManager()
54
155
  }
55
156
 
157
+ setCountryCode(code: string): void {
158
+ this.countryCode = code
159
+ }
160
+
56
161
  async login(credentials?: { username: string; password: string }, accountId?: string): Promise<this> {
57
162
  if (credentials) {
58
- await this.authenticate(credentials.username, credentials.password)
163
+ const result = await this.authenticate(credentials.username, credentials.password)
164
+ if (!result.userId) {
165
+ if (result.requiresTwoFactor) {
166
+ throw new InstagramError(
167
+ 'Two-factor authentication required. Use the CLI (auth login/verify) to complete 2FA.',
168
+ 'two_factor_required',
169
+ )
170
+ }
171
+ if (result.challengeRequired) {
172
+ throw new InstagramError(
173
+ 'Instagram requires a security challenge. Use the CLI (auth login/challenge) to resolve it.',
174
+ 'challenge_required',
175
+ )
176
+ }
177
+ if (result.oneClickEmailAvailable) {
178
+ throw new InstagramError(
179
+ 'Password login was rejected; this account can log in by email. Use the CLI "auth login-email" to complete it.',
180
+ 'one_click_email_available',
181
+ )
182
+ }
183
+ throw new InstagramError('Login did not complete.', 'login_incomplete')
184
+ }
59
185
  return this
60
186
  }
61
187
 
@@ -100,42 +226,40 @@ export class InstagramClient {
100
226
  twoFactorInfo?: Record<string, unknown>
101
227
  challengeRequired?: boolean
102
228
  challengePath?: string
229
+ oneClickEmailAvailable?: boolean
103
230
  }> {
104
- const deviceString = generateDeviceString()
105
- const device = {
106
- phone_id: randomUUID(),
107
- uuid: randomUUID(),
108
- android_device_id: generateAndroidDeviceId(),
109
- advertising_id: randomUUID(),
110
- client_session_id: randomUUID(),
111
- device_string: deviceString,
112
- }
113
-
231
+ const device = await this.resolveDevice()
114
232
  this.session = { cookies: '', device }
115
233
 
116
234
  const encryptionKey = await this.preLoginFlow()
117
- let encPassword: string
118
- try {
119
- if (encryptionKey) {
120
- this.debugLog?.(`encrypting password with key_id=${encryptionKey.keyId}`)
121
- encPassword = this.encryptPassword(password, encryptionKey)
122
- } else {
123
- this.debugLog?.('no encryption key, using plaintext password format')
124
- encPassword = this.plaintextPassword(password)
125
- }
126
- } catch (err) {
127
- this.debugLog?.(`encryption failed: ${err}, falling back to plaintext`)
128
- encPassword = this.plaintextPassword(password)
235
+ if (!encryptionKey) {
236
+ throw new InstagramError(
237
+ 'Instagram did not return a password encryption key. Login cannot proceed safely.',
238
+ 'encryption_key_missing',
239
+ )
129
240
  }
130
241
 
131
- const { status, data } = await this.request('POST', '/accounts/login/', {
132
- username,
133
- enc_password: encPassword,
134
- guid: device.uuid,
135
- phone_id: device.phone_id,
136
- device_id: device.android_device_id,
137
- login_attempt_count: '0',
138
- })
242
+ this.debugLog?.(`encrypting password with key_id=${encryptionKey.keyId}`)
243
+ const encPassword = this.encryptPassword(password, encryptionKey)
244
+
245
+ const { status, data } = await this.request(
246
+ 'POST',
247
+ '/accounts/login/',
248
+ {
249
+ username,
250
+ enc_password: encPassword,
251
+ guid: device.uuid,
252
+ phone_id: device.phone_id,
253
+ _csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
254
+ device_id: device.android_device_id,
255
+ adid: device.advertising_id,
256
+ google_tokens: '[]',
257
+ login_attempt_count: '0',
258
+ country_codes: JSON.stringify([{ country_code: this.countryCode, source: ['default'] }]),
259
+ jazoest: createJazoest(device.phone_id),
260
+ },
261
+ { signed: true },
262
+ )
139
263
 
140
264
  this.debugLog?.(`login response status=${status} body=${JSON.stringify(data)}`)
141
265
 
@@ -156,6 +280,25 @@ export class InstagramClient {
156
280
  return { userId: '', challengeRequired: true, challengePath: challengeApiPath }
157
281
  }
158
282
 
283
+ if (this.isFacebookLinkedLogin(data)) {
284
+ throw new InstagramError(
285
+ 'This account appears to sign in with Facebook and has no usable Instagram password for CLI login. ' +
286
+ 'Most reliable fix: log in to instagram.com in your browser, then run "agent-instagram auth extract". ' +
287
+ 'Alternatively, give the account its own password. Most reliably, unlink Facebook first: in the Instagram app go to ' +
288
+ 'Menu > Settings and privacy > Accounts Center > Manage accounts > (your Facebook profile) Manage > ' +
289
+ 'Move out of this Account Center > Move account > Continue; removing it prompts you to create an Instagram password. ' +
290
+ 'If Facebook is already unlinked, set one directly under ' +
291
+ 'Menu > Settings and privacy > Accounts Center > Password and security > Change password. ' +
292
+ 'Setting a password may enable "auth login", though Instagram can still reject automated logins from a new device.',
293
+ 'facebook_linked',
294
+ )
295
+ }
296
+
297
+ if (this.hasLoginButton(data, 'send_one_click_login_email')) {
298
+ this.session.cookies = this.serializeCookies()
299
+ return { userId: '', oneClickEmailAvailable: true }
300
+ }
301
+
159
302
  if (status !== 200 || data['status'] !== 'ok') {
160
303
  const message = (data['message'] as string) ?? 'Login failed'
161
304
  throw new InstagramError(message, errorType ?? 'login_failed')
@@ -166,14 +309,32 @@ export class InstagramClient {
166
309
  }
167
310
 
168
311
  async twoFactorLogin(username: string, code: string, twoFactorIdentifier: string): Promise<{ userId: string }> {
169
- const { status, data } = await this.request('POST', '/accounts/two_factor_login/', {
170
- username,
171
- verification_code: code,
172
- two_factor_identifier: twoFactorIdentifier,
173
- trust_this_device: '1',
174
- guid: this.session?.device.uuid ?? '',
175
- device_id: this.session?.device.android_device_id ?? '',
176
- })
312
+ const device = this.session?.device
313
+ const { status, data } = await this.request(
314
+ 'POST',
315
+ '/accounts/two_factor_login/',
316
+ {
317
+ username,
318
+ verification_code: code,
319
+ two_factor_identifier: twoFactorIdentifier,
320
+ trust_this_device: '1',
321
+ verification_method: '3',
322
+ _csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
323
+ guid: device?.uuid ?? '',
324
+ phone_id: device?.phone_id ?? '',
325
+ device_id: device?.android_device_id ?? '',
326
+ },
327
+ { signed: true },
328
+ )
329
+
330
+ if (this.isBloksTwoFactorFallback(status, data)) {
331
+ this.debugLog?.('legacy two_factor_login rejected, retrying via Bloks flow')
332
+ const bloksContext =
333
+ typeof data['two_step_verification_context'] === 'string'
334
+ ? data['two_step_verification_context']
335
+ : twoFactorIdentifier
336
+ return this.bloksTwoFactorLogin(code, bloksContext)
337
+ }
177
338
 
178
339
  if (status !== 200 || data['status'] !== 'ok') {
179
340
  const message = (data['message'] as string) ?? 'Two-factor authentication failed'
@@ -184,6 +345,157 @@ export class InstagramClient {
184
345
  return { userId: this.userId ?? '' }
185
346
  }
186
347
 
348
+ // The login dialog's "send_one_click_login_email" button triggers /accounts/one_click_login/
349
+ // with auto_send=true (NOT /accounts/send_recovery_flow_email/, which is the generic password
350
+ // reset). It needs the account's numeric user id, resolved via a pre-login /users/lookup/.
351
+ async sendOneClickLoginEmail(username: string): Promise<{ sent: boolean; contactPoint: string }> {
352
+ const device = await this.ensureDeviceSession()
353
+ const uid = await this.lookupUserId(username)
354
+
355
+ this.debugLog?.(`one_click_login auto_send for uid=${uid}`)
356
+ const { status, data } = await this.request('POST', '/accounts/one_click_login/', {
357
+ uid,
358
+ source: 'one_click_login_email',
359
+ auto_send: 'true',
360
+ guid: device.uuid,
361
+ device_id: device.android_device_id,
362
+ adid: device.advertising_id,
363
+ })
364
+ this.debugLog?.(`one_click_login response status=${status} body=${JSON.stringify(data)}`)
365
+
366
+ if (status !== 200 || data['status'] === 'fail') {
367
+ const message = (data['message'] as string) ?? 'Failed to send login email'
368
+ throw new InstagramError(message, 'one_click_email_failed')
369
+ }
370
+
371
+ const contactPoint =
372
+ (data['obfuscated_email'] as string) ?? (data['email'] as string) ?? (data['contact_point'] as string) ?? ''
373
+ return { sent: true, contactPoint }
374
+ }
375
+
376
+ private async lookupUserId(username: string): Promise<string> {
377
+ const device = await this.ensureDeviceSession()
378
+
379
+ this.debugLog?.(`users/lookup for ${username}`)
380
+ const { status, data } = await this.request(
381
+ 'POST',
382
+ '/users/lookup/',
383
+ {
384
+ q: username,
385
+ directly_sign_in: 'true',
386
+ _csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
387
+ guid: device.uuid,
388
+ device_id: device.android_device_id,
389
+ country_codes: JSON.stringify([{ country_code: this.countryCode, source: ['default'] }]),
390
+ },
391
+ { signed: true },
392
+ )
393
+ this.debugLog?.(`users/lookup response status=${status} body=${JSON.stringify(data)}`)
394
+
395
+ const userId = (data['user_id'] ?? data['uid'] ?? data['pk']) as string | number | undefined
396
+ if (userId == null || String(userId).length === 0) {
397
+ const message = (data['message'] as string) ?? `Could not find an Instagram account for "${username}".`
398
+ throw new InstagramError(message, 'user_lookup_failed')
399
+ }
400
+ return String(userId)
401
+ }
402
+
403
+ async oneClickLogin(uid: string, token: string, source = 'one_click_login_email'): Promise<{ userId: string }> {
404
+ const device = await this.ensureDeviceSession()
405
+
406
+ const { status, data } = await this.request('POST', '/accounts/one_click_login/', {
407
+ uid,
408
+ token,
409
+ source,
410
+ device_id: device.android_device_id,
411
+ guid: device.uuid,
412
+ adid: device.advertising_id,
413
+ })
414
+
415
+ if (status !== 200 || (data['status'] != null && data['status'] !== 'ok')) {
416
+ const message = (data['message'] as string) ?? 'One-click login failed'
417
+ throw new InstagramError(message, 'one_click_login_failed')
418
+ }
419
+
420
+ if (!data['logged_in_user']) {
421
+ throw new InstagramError('One-click login did not return a session', 'one_click_login_failed')
422
+ }
423
+
424
+ await this.finalizeLogin(data)
425
+ return { userId: this.userId ?? '' }
426
+ }
427
+
428
+ private async ensureDeviceSession(): Promise<InstagramDevice> {
429
+ if (!this.session) {
430
+ this.session = { cookies: '', device: await this.resolveDevice() }
431
+ }
432
+ return this.session.device
433
+ }
434
+
435
+ private isFacebookLinkedLogin(data: Record<string, unknown>): boolean {
436
+ return this.hasLoginButton(data, 'login_with_facebook')
437
+ }
438
+
439
+ private hasLoginButton(data: Record<string, unknown>, action: string): boolean {
440
+ const buttons = data['buttons']
441
+ if (!Array.isArray(buttons)) return false
442
+ return buttons.some((button) => (button as Record<string, unknown>)?.['action'] === action)
443
+ }
444
+
445
+ private isBloksTwoFactorFallback(status: number, data: Record<string, unknown>): boolean {
446
+ if (status === 200 && data['status'] === 'ok') return false
447
+ const message = ((data['message'] as string) ?? '').trim().toLowerCase()
448
+ return message === 'invalid parameters' || data['two_step_verification_context'] != null
449
+ }
450
+
451
+ private async bloksTwoFactorLogin(code: string, twoFactorIdentifier: string): Promise<{ userId: string }> {
452
+ const device = this.session?.device
453
+ const params = {
454
+ client_input_params: {
455
+ code,
456
+ should_trust_device: 1,
457
+ family_device_id: device?.phone_id ?? '',
458
+ device_id: device?.android_device_id ?? '',
459
+ machine_id: this.session?.mid ?? '',
460
+ },
461
+ server_params: {
462
+ challenge: 'totp',
463
+ two_step_verification_context: twoFactorIdentifier,
464
+ flow_source: 'two_factor_login',
465
+ },
466
+ }
467
+
468
+ const { status, data } = await this.request(
469
+ 'POST',
470
+ '/bloks/async_action/com.bloks.www.two_step_verification.verify_code.async/',
471
+ { params: JSON.stringify(params), bk_client_context: JSON.stringify({ bloks_version: IG_BLOKS_VERSION_ID }) },
472
+ )
473
+
474
+ if (status !== 200 || (data['status'] != null && data['status'] !== 'ok')) {
475
+ const message = (data['message'] as string) ?? 'Two-factor authentication failed'
476
+ throw new InstagramError(message, 'two_factor_failed')
477
+ }
478
+
479
+ const loggedInUser = data['logged_in_user'] as Record<string, unknown> | undefined
480
+ if (loggedInUser) {
481
+ await this.finalizeLogin(data)
482
+ return { userId: this.userId ?? '' }
483
+ }
484
+
485
+ if (this.session?.authorization) {
486
+ const userId = this.session.user_id ?? this.cookies.get('ds_user_id')
487
+ if (!userId) {
488
+ throw new InstagramError('Two-factor authentication failed', 'two_factor_failed')
489
+ }
490
+ this.userId = userId
491
+ this.session.user_id = userId
492
+ await this.saveSession()
493
+ return { userId }
494
+ }
495
+
496
+ throw new InstagramError('Two-factor authentication failed', 'two_factor_failed')
497
+ }
498
+
187
499
  async challengeSendCode(
188
500
  apiPath: string,
189
501
  method: 'email' | 'sms' = 'email',
@@ -298,6 +610,24 @@ export class InstagramClient {
298
610
  return threads.map((t) => this.mapThread(t))
299
611
  }
300
612
 
613
+ async fetchIrisBootstrap(): Promise<{ seqId: number; snapshotAtMs: number }> {
614
+ this.ensureSession()
615
+ const { data } = await this.request('GET', '/direct_v2/inbox/?limit=1')
616
+
617
+ if (data['status'] !== 'ok') {
618
+ throw new InstagramError('Failed to fetch iris bootstrap', 'inbox_error')
619
+ }
620
+
621
+ const seqId = data['seq_id'] as number | undefined
622
+ const snapshotAtMs = data['snapshot_at_ms'] as number | undefined
623
+
624
+ if (typeof seqId !== 'number' || typeof snapshotAtMs !== 'number') {
625
+ throw new InstagramError('Iris bootstrap missing seq_id or snapshot_at_ms', 'iris_bootstrap_missing')
626
+ }
627
+
628
+ return { seqId, snapshotAtMs }
629
+ }
630
+
301
631
  async searchChats(query: string, limit = 20): Promise<InstagramChatSummary[]> {
302
632
  const allChats = await this.listChats(Math.max(limit, 50))
303
633
  const lower = query.toLowerCase()
@@ -405,6 +735,11 @@ export class InstagramClient {
405
735
  return this.userId
406
736
  }
407
737
 
738
+ getSessionState(): InstagramSessionState {
739
+ this.ensureSession()
740
+ return this.session!
741
+ }
742
+
408
743
  async getProfile(): Promise<{
409
744
  user_id: string
410
745
  username: string
@@ -427,16 +762,28 @@ export class InstagramClient {
427
762
  const url = `${IG_BASE_URL}/qe/sync/`
428
763
  const headers = this.buildHeaders()
429
764
 
430
- const response = await fetch(url, {
431
- method: 'POST',
432
- headers,
433
- body: new URLSearchParams({
434
- id: this.session!.device.uuid,
435
- experiments: 'ig_android_fci,ig_android_device_detection_info_upload,ig_android_device_verification_fb_signup',
436
- }).toString(),
437
- })
765
+ const { response } = await fetchWithTimeout(
766
+ url,
767
+ {
768
+ method: 'POST',
769
+ headers,
770
+ body: new URLSearchParams(
771
+ signBody({
772
+ id: this.session!.device.uuid,
773
+ experiments:
774
+ 'ig_android_fci,ig_android_device_detection_info_upload,ig_android_device_verification_fb_signup',
775
+ }),
776
+ ).toString(),
777
+ },
778
+ '/qe/sync/',
779
+ )
438
780
  this.extractResponseCookies(response.headers)
439
781
 
782
+ const wwwClaim = response.headers.get('x-ig-set-www-claim')
783
+ if (wwwClaim && this.session) {
784
+ this.session.www_claim = wwwClaim
785
+ }
786
+
440
787
  const pubKeyHeader = response.headers.get('ig-set-password-encryption-pub-key')
441
788
  const keyIdHeader = response.headers.get('ig-set-password-encryption-key-id')
442
789
 
@@ -464,8 +811,15 @@ export class InstagramClient {
464
811
  const pubKeyPem = Buffer.from(key.publicKey, 'base64').toString('utf-8')
465
812
  const rsaEncrypted = publicEncrypt({ key: pubKeyPem, padding: constants.RSA_PKCS1_PADDING }, aesKey)
466
813
 
814
+ if (!/^(?:0|[1-9]\d*)$/.test(key.keyId)) {
815
+ throw new InstagramError(`Invalid password encryption key id: ${key.keyId}`, 'encryption_key_invalid')
816
+ }
817
+ const keyIdNum = Number(key.keyId)
818
+ if (keyIdNum > 255) {
819
+ throw new InstagramError(`Invalid password encryption key id: ${key.keyId}`, 'encryption_key_invalid')
820
+ }
821
+
467
822
  // Wire format: version(1) | keyId(1) | iv(12) | rsaLen(2 LE) | rsaEncrypted | tag(16) | aesEncrypted
468
- const keyIdNum = Number.parseInt(key.keyId, 10)
469
823
  const buf = Buffer.alloc(1 + 1 + 12 + 2 + rsaEncrypted.length + 16 + encrypted.length)
470
824
  let offset = 0
471
825
  buf.writeUInt8(1, offset)
@@ -485,25 +839,22 @@ export class InstagramClient {
485
839
  return `#PWD_INSTAGRAM:4:${timestamp}:${buf.toString('base64')}`
486
840
  }
487
841
 
488
- private plaintextPassword(password: string): string {
489
- const timestamp = Math.floor(Date.now() / 1000).toString()
490
- return `#PWD_INSTAGRAM:0:${timestamp}:${password}`
491
- }
492
-
493
842
  private async request(
494
843
  method: string,
495
844
  path: string,
496
845
  body?: Record<string, string>,
846
+ options?: { signed?: boolean },
497
847
  ): Promise<{ status: number; data: Record<string, unknown> }> {
498
848
  const url = `${IG_BASE_URL}${path}`
499
849
  const headers = this.buildHeaders()
500
850
 
501
- const options: RequestInit = { method, headers }
851
+ const requestInit: RequestInit = { method, headers }
502
852
  if (body) {
503
- options.body = new URLSearchParams(body).toString()
853
+ const payload = options?.signed ? signBody(body) : body
854
+ requestInit.body = new URLSearchParams(payload).toString()
504
855
  }
505
856
 
506
- const response = await fetch(url, options)
857
+ const { response, readText } = await fetchWithTimeout(url, requestInit, path)
507
858
  this.extractResponseCookies(response.headers)
508
859
 
509
860
  const authHeader = response.headers.get('ig-set-authorization')
@@ -511,13 +862,27 @@ export class InstagramClient {
511
862
  this.session.authorization = authHeader
512
863
  }
513
864
 
865
+ const wwwClaim = response.headers.get('x-ig-set-www-claim')
866
+ if (wwwClaim && this.session) {
867
+ this.session.www_claim = wwwClaim
868
+ }
869
+
870
+ const rawBody = await readText()
871
+
514
872
  if (response.status === 429) {
515
- throw new InstagramError('Rate limited by Instagram. Try again later.', 'rate_limited')
873
+ this.debugLog?.(`429 from ${path} body=${rawBody}`)
874
+ const igMessage = this.extractJsonMessage(rawBody)
875
+ throw new InstagramError(
876
+ igMessage
877
+ ? `Rate limited by Instagram: ${igMessage} Wait before trying again.`
878
+ : 'Rate limited by Instagram. Try again later.',
879
+ 'rate_limited',
880
+ )
516
881
  }
517
882
 
518
883
  let data: Record<string, unknown>
519
884
  try {
520
- data = (await response.json()) as Record<string, unknown>
885
+ data = JSON.parse(rawBody) as Record<string, unknown>
521
886
  } catch {
522
887
  throw new InstagramError(`Failed to parse response from ${path}`, response.status)
523
888
  }
@@ -525,22 +890,51 @@ export class InstagramClient {
525
890
  return { status: response.status, data }
526
891
  }
527
892
 
893
+ private extractJsonMessage(body: string): string {
894
+ try {
895
+ const parsed = JSON.parse(body) as Record<string, unknown>
896
+ return (parsed['message'] as string) ?? ''
897
+ } catch {
898
+ return ''
899
+ }
900
+ }
901
+
528
902
  private buildHeaders(): Record<string, string> {
529
903
  const deviceString = this.session?.device.device_string ?? generateDeviceString()
904
+ // Instagram's edge silently drops connections whose fingerprint headers don't match a real
905
+ // Android client. This is a partial alignment toward the current app profile
906
+ // (per subzeroid/instagrapi) covering the highest-signal headers, not a full fingerprint match
907
+ // (e.g. X-Tigon-Is-Retry, X-Zero-*, X-IG-Nav-Chain, X-IG-SALT-IDS are intentionally omitted).
530
908
  const headers: Record<string, string> = {
531
909
  'User-Agent': buildUserAgent(deviceString),
910
+ 'X-IG-App-Locale': IG_LOCALE,
911
+ 'X-IG-Device-Locale': IG_LOCALE,
912
+ 'X-IG-Mapped-Locale': IG_LOCALE,
913
+ 'X-Pigeon-Session-Id': `UFS-${randomUUID()}-0`,
914
+ 'X-Pigeon-Rawclienttime': (Date.now() / 1000).toFixed(3),
915
+ 'X-IG-Bandwidth-Speed-KBPS': '-1.000',
916
+ 'X-IG-Bandwidth-TotalBytes-B': '0',
917
+ 'X-IG-Bandwidth-TotalTime-MS': '0',
918
+ 'X-IG-App-Startup-Country': 'US',
532
919
  'X-IG-App-ID': IG_APP_ID,
533
- 'X-IG-Capabilities': '3brTv10=',
920
+ 'X-IG-Capabilities': IG_CAPABILITIES,
534
921
  'X-IG-Connection-Type': 'WIFI',
535
922
  'X-IG-Timezone-Offset': String(new Date().getTimezoneOffset() * -60),
923
+ 'X-Bloks-Version-Id': IG_BLOKS_VERSION_ID,
924
+ 'X-FB-HTTP-Engine': 'Tigon/MNS/TCP',
925
+ Priority: 'u=3',
926
+ 'IG-INTENDED-USER-ID': this.userId ?? '0',
536
927
  'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
537
928
  Accept: '*/*',
538
929
  'Accept-Language': 'en-US,en;q=0.9',
539
930
  }
540
931
 
932
+ headers['X-IG-WWW-Claim'] = this.session?.www_claim ?? '0'
933
+
541
934
  if (this.session) {
542
935
  headers['X-IG-Device-ID'] = this.session.device.uuid
543
936
  headers['X-IG-Android-ID'] = this.session.device.android_device_id
937
+ headers['X-IG-Family-Device-ID'] = this.session.device.phone_id
544
938
  if (this.session.authorization) {
545
939
  headers['Authorization'] = this.session.authorization
546
940
  }
@@ -613,6 +1007,16 @@ export class InstagramClient {
613
1007
  await this.saveSession()
614
1008
  }
615
1009
 
1010
+ // Reuse one persisted device per machine so repeat logins present a consistent fingerprint.
1011
+ // Regenerating device ids each attempt looks like a brand-new phone and hurts login trust.
1012
+ private async resolveDevice(): Promise<InstagramDevice> {
1013
+ const existing = await this.credentialManager.loadDevice()
1014
+ if (existing) return existing
1015
+ const device = generateDevice()
1016
+ await this.credentialManager.saveDevice(device)
1017
+ return device
1018
+ }
1019
+
616
1020
  private async saveSession(): Promise<void> {
617
1021
  if (!this.session || !this.sessionPath) return
618
1022
  this.session.cookies = this.serializeCookies()