agent-messenger 2.28.0 → 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 (157) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +18 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/discord/client.d.ts +6 -2
  5. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  6. package/dist/src/platforms/discord/client.js +10 -3
  7. package/dist/src/platforms/discord/client.js.map +1 -1
  8. package/dist/src/platforms/instagram/client.d.ts +24 -2
  9. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  10. package/dist/src/platforms/instagram/client.js +343 -53
  11. package/dist/src/platforms/instagram/client.js.map +1 -1
  12. package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
  13. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  14. package/dist/src/platforms/instagram/commands/auth.js +123 -11
  15. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  16. package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
  17. package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
  18. package/dist/src/platforms/instagram/credential-manager.js +17 -1
  19. package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
  20. package/dist/src/platforms/instagram/types.d.ts +10 -8
  21. package/dist/src/platforms/instagram/types.d.ts.map +1 -1
  22. package/dist/src/platforms/instagram/types.js.map +1 -1
  23. package/dist/src/platforms/teams/app-config.d.ts +36 -0
  24. package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
  25. package/dist/src/platforms/teams/app-config.js +69 -0
  26. package/dist/src/platforms/teams/app-config.js.map +1 -0
  27. package/dist/src/platforms/teams/client.d.ts +19 -2
  28. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  29. package/dist/src/platforms/teams/client.js +295 -7
  30. package/dist/src/platforms/teams/client.js.map +1 -1
  31. package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
  32. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  33. package/dist/src/platforms/teams/commands/auth.js +137 -3
  34. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  35. package/dist/src/platforms/teams/commands/file.d.ts +3 -0
  36. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
  37. package/dist/src/platforms/teams/commands/file.js +59 -2
  38. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  39. package/dist/src/platforms/teams/commands/message.d.ts +11 -0
  40. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  41. package/dist/src/platforms/teams/commands/message.js +121 -2
  42. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  43. package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
  44. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  45. package/dist/src/platforms/teams/credential-manager.js +46 -2
  46. package/dist/src/platforms/teams/credential-manager.js.map +1 -1
  47. package/dist/src/platforms/teams/device-code.d.ts +48 -0
  48. package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
  49. package/dist/src/platforms/teams/device-code.js +199 -0
  50. package/dist/src/platforms/teams/device-code.js.map +1 -0
  51. package/dist/src/platforms/teams/device-login.d.ts +37 -0
  52. package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
  53. package/dist/src/platforms/teams/device-login.js +129 -0
  54. package/dist/src/platforms/teams/device-login.js.map +1 -0
  55. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  56. package/dist/src/platforms/teams/ensure-auth.js +12 -0
  57. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  58. package/dist/src/platforms/teams/index.d.ts +7 -3
  59. package/dist/src/platforms/teams/index.d.ts.map +1 -1
  60. package/dist/src/platforms/teams/index.js +5 -2
  61. package/dist/src/platforms/teams/index.js.map +1 -1
  62. package/dist/src/platforms/teams/listener.d.ts +35 -0
  63. package/dist/src/platforms/teams/listener.d.ts.map +1 -0
  64. package/dist/src/platforms/teams/listener.js +236 -0
  65. package/dist/src/platforms/teams/listener.js.map +1 -0
  66. package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
  67. package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
  68. package/dist/src/platforms/teams/realm-discovery.js +33 -0
  69. package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
  70. package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
  71. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  72. package/dist/src/platforms/teams/token-extractor.js +60 -0
  73. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  74. package/dist/src/platforms/teams/token-provider.d.ts +25 -0
  75. package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
  76. package/dist/src/platforms/teams/token-provider.js +190 -0
  77. package/dist/src/platforms/teams/token-provider.js.map +1 -0
  78. package/dist/src/platforms/teams/trouter.d.ts +31 -0
  79. package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
  80. package/dist/src/platforms/teams/trouter.js +181 -0
  81. package/dist/src/platforms/teams/trouter.js.map +1 -0
  82. package/dist/src/platforms/teams/types.d.ts +94 -0
  83. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  84. package/dist/src/platforms/teams/types.js +30 -0
  85. package/dist/src/platforms/teams/types.js.map +1 -1
  86. package/dist/src/platforms/webex/types.d.ts +1 -1
  87. package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
  88. package/dist/src/tui/adapters/instagram-adapter.js +16 -0
  89. package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
  90. package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
  91. package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
  92. package/dist/src/tui/adapters/teams-adapter.js +21 -0
  93. package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
  94. package/dist/src/tui/app.js +3 -3
  95. package/docs/content/docs/cli/instagram.mdx +1 -0
  96. package/docs/content/docs/cli/teams.mdx +22 -0
  97. package/package.json +1 -1
  98. package/skills/agent-channeltalk/SKILL.md +1 -1
  99. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  100. package/skills/agent-discord/SKILL.md +1 -1
  101. package/skills/agent-discordbot/SKILL.md +1 -1
  102. package/skills/agent-imessage/SKILL.md +1 -1
  103. package/skills/agent-instagram/SKILL.md +1 -1
  104. package/skills/agent-instagram/references/authentication.md +11 -0
  105. package/skills/agent-kakaotalk/SKILL.md +1 -1
  106. package/skills/agent-line/SKILL.md +1 -1
  107. package/skills/agent-slack/SKILL.md +1 -1
  108. package/skills/agent-slackbot/SKILL.md +1 -1
  109. package/skills/agent-teams/SKILL.md +59 -6
  110. package/skills/agent-teams/references/authentication.md +31 -1
  111. package/skills/agent-telegram/SKILL.md +1 -1
  112. package/skills/agent-telegrambot/SKILL.md +1 -1
  113. package/skills/agent-webex/SKILL.md +1 -1
  114. package/skills/agent-webexbot/SKILL.md +1 -1
  115. package/skills/agent-wechatbot/SKILL.md +1 -1
  116. package/skills/agent-whatsapp/SKILL.md +1 -1
  117. package/skills/agent-whatsappbot/SKILL.md +1 -1
  118. package/src/platforms/discord/client.test.ts +86 -0
  119. package/src/platforms/discord/client.ts +10 -3
  120. package/src/platforms/instagram/client.test.ts +478 -33
  121. package/src/platforms/instagram/client.ts +449 -68
  122. package/src/platforms/instagram/commands/auth.test.ts +177 -1
  123. package/src/platforms/instagram/commands/auth.ts +182 -11
  124. package/src/platforms/instagram/credential-manager.test.ts +30 -0
  125. package/src/platforms/instagram/credential-manager.ts +23 -1
  126. package/src/platforms/instagram/types.ts +11 -8
  127. package/src/platforms/teams/app-config.ts +92 -0
  128. package/src/platforms/teams/client.test.ts +335 -1
  129. package/src/platforms/teams/client.ts +342 -7
  130. package/src/platforms/teams/commands/auth.test.ts +93 -1
  131. package/src/platforms/teams/commands/auth.ts +182 -3
  132. package/src/platforms/teams/commands/file.test.ts +92 -1
  133. package/src/platforms/teams/commands/file.ts +76 -1
  134. package/src/platforms/teams/commands/message.test.ts +95 -1
  135. package/src/platforms/teams/commands/message.ts +145 -3
  136. package/src/platforms/teams/credential-manager.test.ts +20 -0
  137. package/src/platforms/teams/credential-manager.ts +68 -3
  138. package/src/platforms/teams/device-code.test.ts +311 -0
  139. package/src/platforms/teams/device-code.ts +277 -0
  140. package/src/platforms/teams/device-login.test.ts +288 -0
  141. package/src/platforms/teams/device-login.ts +206 -0
  142. package/src/platforms/teams/ensure-auth.ts +11 -0
  143. package/src/platforms/teams/index.test.ts +10 -0
  144. package/src/platforms/teams/index.ts +16 -1
  145. package/src/platforms/teams/listener.ts +295 -0
  146. package/src/platforms/teams/realm-discovery.test.ts +73 -0
  147. package/src/platforms/teams/realm-discovery.ts +42 -0
  148. package/src/platforms/teams/token-extractor.ts +69 -0
  149. package/src/platforms/teams/token-provider.test.ts +227 -0
  150. package/src/platforms/teams/token-provider.ts +284 -0
  151. package/src/platforms/teams/trouter.test.ts +114 -0
  152. package/src/platforms/teams/trouter.ts +228 -0
  153. package/src/platforms/teams/types.test.ts +17 -0
  154. package/src/platforms/teams/types.ts +86 -0
  155. package/src/tui/adapters/instagram-adapter.ts +13 -0
  156. package/src/tui/adapters/teams-adapter.ts +23 -0
  157. package/src/tui/app.ts +3 -3
@@ -5,17 +5,81 @@ import { InstagramCredentialManager } from './credential-manager.js';
5
5
  import { InstagramError, extractMediaUrl, extractMessageText, getMessageType, } from './types.js';
6
6
  const IG_BASE_URL = 'https://i.instagram.com/api/v1';
7
7
  const IG_APP_ID = '567067343352427';
8
- const IG_VERSION = '312.1.0.34.111';
9
- const IG_VERSION_CODE = '556927984';
10
- const ANDROID_VERSION = '13.0';
11
- const ANDROID_RELEASE = '33';
8
+ const IG_VERSION = '428.0.0.47.67';
9
+ const IG_VERSION_CODE = '719358530';
10
+ // Bloks client versioning id; required header for Bloks endpoints (2FA / CAA flows).
11
+ const IG_BLOKS_VERSION_ID = '5f56efad68e1edec7801f630b5c122704ec5378adbee6609a448f105f34a9c73';
12
+ const IG_CAPABILITIES = '3brTv10=';
13
+ const IG_LOCALE = 'en_US';
14
+ // Instagram silently holds the socket open (no response) for flagged IPs / fingerprints instead
15
+ // of returning an error, so an unbounded fetch hangs forever. Cap every request and surface a
16
+ // clear, actionable error instead of a frozen process.
17
+ const IG_REQUEST_TIMEOUT_MS = 30_000;
18
+ const ANDROID_VERSION = '14';
19
+ const ANDROID_RELEASE = '34';
12
20
  const DEVICE_DPI = '480dpi';
13
- const DEVICE_RESOLUTION = '1080x2340';
14
- const DEVICE_MANUFACTURER = 'samsung';
15
- const DEVICE_MODEL = 'SM-S911B';
16
- const DEVICE_CHIPSET = 'qcom';
21
+ const DEVICE_RESOLUTION = '1344x2992';
22
+ const DEVICE_MANUFACTURER = 'Google/google';
23
+ const DEVICE_MODEL = 'Pixel 8 Pro';
24
+ const DEVICE_NAME = 'husky';
25
+ const DEVICE_CHIPSET = 'husky';
17
26
  export function generateDeviceString() {
18
- return `${ANDROID_VERSION}/${ANDROID_RELEASE}; ${DEVICE_DPI}; ${DEVICE_RESOLUTION}; ${DEVICE_MANUFACTURER}; ${DEVICE_MODEL}; ${DEVICE_MODEL}; ${DEVICE_CHIPSET}; en_US; ${IG_VERSION_CODE}`;
27
+ return `${ANDROID_RELEASE}/${ANDROID_VERSION}; ${DEVICE_DPI}; ${DEVICE_RESOLUTION}; ${DEVICE_MANUFACTURER}; ${DEVICE_MODEL}; ${DEVICE_NAME}; ${DEVICE_CHIPSET}; en_US; ${IG_VERSION_CODE}`;
28
+ }
29
+ // Instagram no longer verifies the body HMAC; the current mobile API expects a literal
30
+ // "SIGNATURE." prefix (per subzeroid/instagrapi, subzeroid/aiograpi). Sending a real HMAC +
31
+ // ig_sig_key_version is rejected as malformed on /accounts/login/, surfacing as a misleading
32
+ // "account not found" error. URLSearchParams url-encodes the value exactly once.
33
+ function signBody(payload) {
34
+ return { signed_body: `SIGNATURE.${JSON.stringify(payload)}` };
35
+ }
36
+ // jazoest = "2" + sum of ASCII byte values of the input; an anti-bot checksum expected by login.
37
+ function createJazoest(input) {
38
+ let sum = 0;
39
+ for (let i = 0; i < input.length; i++) {
40
+ sum += input.charCodeAt(i);
41
+ }
42
+ return `2${sum}`;
43
+ }
44
+ function generateToken() {
45
+ return randomBytes(16).toString('hex');
46
+ }
47
+ function isTimeoutAbort(error) {
48
+ // AbortSignal.timeout aborts the connect AND the body stream, surfacing as TimeoutError on the
49
+ // fetch and AbortError when a stalled body read is aborted mid-stream; both mean "IG stalled".
50
+ return error instanceof DOMException && (error.name === 'TimeoutError' || error.name === 'AbortError');
51
+ }
52
+ function stalledError(path) {
53
+ return new InstagramError(`Instagram did not respond within ${IG_REQUEST_TIMEOUT_MS / 1000}s (${path}). The connection stalled, ` +
54
+ 'which usually means the IP is flagged (VPN/datacenter/proxy or prior automated attempts) or the ' +
55
+ 'device fingerprint was rejected. Try "agent-instagram auth extract" from a browser logged into ' +
56
+ 'instagram.com instead of password login.', 'request_stalled');
57
+ }
58
+ // One AbortSignal.timeout covers the whole request lifecycle: connect, headers, AND the body read.
59
+ // Reading the body through readText() keeps a post-header stall mapped to request_stalled instead
60
+ // of leaking out as a generic parse failure.
61
+ async function fetchWithTimeout(url, init, path) {
62
+ const signal = AbortSignal.timeout(IG_REQUEST_TIMEOUT_MS);
63
+ let response;
64
+ try {
65
+ response = await fetch(url, { ...init, signal });
66
+ }
67
+ catch (error) {
68
+ if (isTimeoutAbort(error))
69
+ throw stalledError(path);
70
+ throw error;
71
+ }
72
+ const readText = async () => {
73
+ try {
74
+ return await response.text();
75
+ }
76
+ catch (error) {
77
+ if (isTimeoutAbort(error))
78
+ throw stalledError(path);
79
+ throw error;
80
+ }
81
+ };
82
+ return { response, readText };
19
83
  }
20
84
  function buildUserAgent(deviceString) {
21
85
  return `Instagram ${IG_VERSION} Android (${deviceString})`;
@@ -23,6 +87,29 @@ function buildUserAgent(deviceString) {
23
87
  export function generateAndroidDeviceId() {
24
88
  return `android-${randomBytes(8).toString('hex')}`;
25
89
  }
90
+ export function generateDevice() {
91
+ return {
92
+ phone_id: randomUUID(),
93
+ uuid: randomUUID(),
94
+ android_device_id: generateAndroidDeviceId(),
95
+ advertising_id: randomUUID(),
96
+ client_session_id: randomUUID(),
97
+ device_string: generateDeviceString(),
98
+ };
99
+ }
100
+ export function parseOneClickLoginLink(input) {
101
+ const trimmed = input.trim();
102
+ let query = trimmed.includes('?') ? trimmed.slice(trimmed.indexOf('?') + 1) : trimmed;
103
+ const hashIdx = query.indexOf('#');
104
+ if (hashIdx !== -1)
105
+ query = query.slice(0, hashIdx);
106
+ const params = new URLSearchParams(query);
107
+ const uid = params.get('uid');
108
+ const token = params.get('token');
109
+ if (uid && token)
110
+ return { uid, token };
111
+ return null;
112
+ }
26
113
  // Instagram DM timestamps are in microseconds
27
114
  function microsecondsToISO(us) {
28
115
  return new Date(us / 1000).toISOString();
@@ -33,12 +120,28 @@ export class InstagramClient {
33
120
  sessionPath = null;
34
121
  userId = null;
35
122
  cookies = new Map();
123
+ countryCode = '1';
36
124
  constructor(credentialManager) {
37
125
  this.credentialManager = credentialManager ?? new InstagramCredentialManager();
38
126
  }
127
+ setCountryCode(code) {
128
+ this.countryCode = code;
129
+ }
39
130
  async login(credentials, accountId) {
40
131
  if (credentials) {
41
- await this.authenticate(credentials.username, credentials.password);
132
+ const result = await this.authenticate(credentials.username, credentials.password);
133
+ if (!result.userId) {
134
+ if (result.requiresTwoFactor) {
135
+ throw new InstagramError('Two-factor authentication required. Use the CLI (auth login/verify) to complete 2FA.', 'two_factor_required');
136
+ }
137
+ if (result.challengeRequired) {
138
+ throw new InstagramError('Instagram requires a security challenge. Use the CLI (auth login/challenge) to resolve it.', 'challenge_required');
139
+ }
140
+ if (result.oneClickEmailAvailable) {
141
+ throw new InstagramError('Password login was rejected; this account can log in by email. Use the CLI "auth login-email" to complete it.', 'one_click_email_available');
142
+ }
143
+ throw new InstagramError('Login did not complete.', 'login_incomplete');
144
+ }
42
145
  return this;
43
146
  }
44
147
  const account = await this.credentialManager.getAccount(accountId);
@@ -61,40 +164,27 @@ export class InstagramClient {
61
164
  this.debugLog = fn;
62
165
  }
63
166
  async authenticate(username, password) {
64
- const deviceString = generateDeviceString();
65
- const device = {
66
- phone_id: randomUUID(),
67
- uuid: randomUUID(),
68
- android_device_id: generateAndroidDeviceId(),
69
- advertising_id: randomUUID(),
70
- client_session_id: randomUUID(),
71
- device_string: deviceString,
72
- };
167
+ const device = await this.resolveDevice();
73
168
  this.session = { cookies: '', device };
74
169
  const encryptionKey = await this.preLoginFlow();
75
- let encPassword;
76
- try {
77
- if (encryptionKey) {
78
- this.debugLog?.(`encrypting password with key_id=${encryptionKey.keyId}`);
79
- encPassword = this.encryptPassword(password, encryptionKey);
80
- }
81
- else {
82
- this.debugLog?.('no encryption key, using plaintext password format');
83
- encPassword = this.plaintextPassword(password);
84
- }
85
- }
86
- catch (err) {
87
- this.debugLog?.(`encryption failed: ${err}, falling back to plaintext`);
88
- encPassword = this.plaintextPassword(password);
170
+ if (!encryptionKey) {
171
+ throw new InstagramError('Instagram did not return a password encryption key. Login cannot proceed safely.', 'encryption_key_missing');
89
172
  }
173
+ this.debugLog?.(`encrypting password with key_id=${encryptionKey.keyId}`);
174
+ const encPassword = this.encryptPassword(password, encryptionKey);
90
175
  const { status, data } = await this.request('POST', '/accounts/login/', {
91
176
  username,
92
177
  enc_password: encPassword,
93
178
  guid: device.uuid,
94
179
  phone_id: device.phone_id,
180
+ _csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
95
181
  device_id: device.android_device_id,
182
+ adid: device.advertising_id,
183
+ google_tokens: '[]',
96
184
  login_attempt_count: '0',
97
- });
185
+ country_codes: JSON.stringify([{ country_code: this.countryCode, source: ['default'] }]),
186
+ jazoest: createJazoest(device.phone_id),
187
+ }, { signed: true });
98
188
  this.debugLog?.(`login response status=${status} body=${JSON.stringify(data)}`);
99
189
  if (data['two_factor_required'] === true) {
100
190
  const twoFactorInfo = data['two_factor_info'];
@@ -110,6 +200,20 @@ export class InstagramClient {
110
200
  await this.saveSession();
111
201
  return { userId: '', challengeRequired: true, challengePath: challengeApiPath };
112
202
  }
203
+ if (this.isFacebookLinkedLogin(data)) {
204
+ throw new InstagramError('This account appears to sign in with Facebook and has no usable Instagram password for CLI login. ' +
205
+ 'Most reliable fix: log in to instagram.com in your browser, then run "agent-instagram auth extract". ' +
206
+ 'Alternatively, give the account its own password. Most reliably, unlink Facebook first: in the Instagram app go to ' +
207
+ 'Menu > Settings and privacy > Accounts Center > Manage accounts > (your Facebook profile) Manage > ' +
208
+ 'Move out of this Account Center > Move account > Continue; removing it prompts you to create an Instagram password. ' +
209
+ 'If Facebook is already unlinked, set one directly under ' +
210
+ 'Menu > Settings and privacy > Accounts Center > Password and security > Change password. ' +
211
+ 'Setting a password may enable "auth login", though Instagram can still reject automated logins from a new device.', 'facebook_linked');
212
+ }
213
+ if (this.hasLoginButton(data, 'send_one_click_login_email')) {
214
+ this.session.cookies = this.serializeCookies();
215
+ return { userId: '', oneClickEmailAvailable: true };
216
+ }
113
217
  if (status !== 200 || data['status'] !== 'ok') {
114
218
  const message = data['message'] ?? 'Login failed';
115
219
  throw new InstagramError(message, errorType ?? 'login_failed');
@@ -118,14 +222,25 @@ export class InstagramClient {
118
222
  return { userId: this.userId ?? '' };
119
223
  }
120
224
  async twoFactorLogin(username, code, twoFactorIdentifier) {
225
+ const device = this.session?.device;
121
226
  const { status, data } = await this.request('POST', '/accounts/two_factor_login/', {
122
227
  username,
123
228
  verification_code: code,
124
229
  two_factor_identifier: twoFactorIdentifier,
125
230
  trust_this_device: '1',
126
- guid: this.session?.device.uuid ?? '',
127
- device_id: this.session?.device.android_device_id ?? '',
128
- });
231
+ verification_method: '3',
232
+ _csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
233
+ guid: device?.uuid ?? '',
234
+ phone_id: device?.phone_id ?? '',
235
+ device_id: device?.android_device_id ?? '',
236
+ }, { signed: true });
237
+ if (this.isBloksTwoFactorFallback(status, data)) {
238
+ this.debugLog?.('legacy two_factor_login rejected, retrying via Bloks flow');
239
+ const bloksContext = typeof data['two_step_verification_context'] === 'string'
240
+ ? data['two_step_verification_context']
241
+ : twoFactorIdentifier;
242
+ return this.bloksTwoFactorLogin(code, bloksContext);
243
+ }
129
244
  if (status !== 200 || data['status'] !== 'ok') {
130
245
  const message = data['message'] ?? 'Two-factor authentication failed';
131
246
  throw new InstagramError(message, 'two_factor_failed');
@@ -133,6 +248,127 @@ export class InstagramClient {
133
248
  await this.finalizeLogin(data);
134
249
  return { userId: this.userId ?? '' };
135
250
  }
251
+ // The login dialog's "send_one_click_login_email" button triggers /accounts/one_click_login/
252
+ // with auto_send=true (NOT /accounts/send_recovery_flow_email/, which is the generic password
253
+ // reset). It needs the account's numeric user id, resolved via a pre-login /users/lookup/.
254
+ async sendOneClickLoginEmail(username) {
255
+ const device = await this.ensureDeviceSession();
256
+ const uid = await this.lookupUserId(username);
257
+ this.debugLog?.(`one_click_login auto_send for uid=${uid}`);
258
+ const { status, data } = await this.request('POST', '/accounts/one_click_login/', {
259
+ uid,
260
+ source: 'one_click_login_email',
261
+ auto_send: 'true',
262
+ guid: device.uuid,
263
+ device_id: device.android_device_id,
264
+ adid: device.advertising_id,
265
+ });
266
+ this.debugLog?.(`one_click_login response status=${status} body=${JSON.stringify(data)}`);
267
+ if (status !== 200 || data['status'] === 'fail') {
268
+ const message = data['message'] ?? 'Failed to send login email';
269
+ throw new InstagramError(message, 'one_click_email_failed');
270
+ }
271
+ const contactPoint = data['obfuscated_email'] ?? data['email'] ?? data['contact_point'] ?? '';
272
+ return { sent: true, contactPoint };
273
+ }
274
+ async lookupUserId(username) {
275
+ const device = await this.ensureDeviceSession();
276
+ this.debugLog?.(`users/lookup for ${username}`);
277
+ const { status, data } = await this.request('POST', '/users/lookup/', {
278
+ q: username,
279
+ directly_sign_in: 'true',
280
+ _csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
281
+ guid: device.uuid,
282
+ device_id: device.android_device_id,
283
+ country_codes: JSON.stringify([{ country_code: this.countryCode, source: ['default'] }]),
284
+ }, { signed: true });
285
+ this.debugLog?.(`users/lookup response status=${status} body=${JSON.stringify(data)}`);
286
+ const userId = (data['user_id'] ?? data['uid'] ?? data['pk']);
287
+ if (userId == null || String(userId).length === 0) {
288
+ const message = data['message'] ?? `Could not find an Instagram account for "${username}".`;
289
+ throw new InstagramError(message, 'user_lookup_failed');
290
+ }
291
+ return String(userId);
292
+ }
293
+ async oneClickLogin(uid, token, source = 'one_click_login_email') {
294
+ const device = await this.ensureDeviceSession();
295
+ const { status, data } = await this.request('POST', '/accounts/one_click_login/', {
296
+ uid,
297
+ token,
298
+ source,
299
+ device_id: device.android_device_id,
300
+ guid: device.uuid,
301
+ adid: device.advertising_id,
302
+ });
303
+ if (status !== 200 || (data['status'] != null && data['status'] !== 'ok')) {
304
+ const message = data['message'] ?? 'One-click login failed';
305
+ throw new InstagramError(message, 'one_click_login_failed');
306
+ }
307
+ if (!data['logged_in_user']) {
308
+ throw new InstagramError('One-click login did not return a session', 'one_click_login_failed');
309
+ }
310
+ await this.finalizeLogin(data);
311
+ return { userId: this.userId ?? '' };
312
+ }
313
+ async ensureDeviceSession() {
314
+ if (!this.session) {
315
+ this.session = { cookies: '', device: await this.resolveDevice() };
316
+ }
317
+ return this.session.device;
318
+ }
319
+ isFacebookLinkedLogin(data) {
320
+ return this.hasLoginButton(data, 'login_with_facebook');
321
+ }
322
+ hasLoginButton(data, action) {
323
+ const buttons = data['buttons'];
324
+ if (!Array.isArray(buttons))
325
+ return false;
326
+ return buttons.some((button) => button?.['action'] === action);
327
+ }
328
+ isBloksTwoFactorFallback(status, data) {
329
+ if (status === 200 && data['status'] === 'ok')
330
+ return false;
331
+ const message = (data['message'] ?? '').trim().toLowerCase();
332
+ return message === 'invalid parameters' || data['two_step_verification_context'] != null;
333
+ }
334
+ async bloksTwoFactorLogin(code, twoFactorIdentifier) {
335
+ const device = this.session?.device;
336
+ const params = {
337
+ client_input_params: {
338
+ code,
339
+ should_trust_device: 1,
340
+ family_device_id: device?.phone_id ?? '',
341
+ device_id: device?.android_device_id ?? '',
342
+ machine_id: this.session?.mid ?? '',
343
+ },
344
+ server_params: {
345
+ challenge: 'totp',
346
+ two_step_verification_context: twoFactorIdentifier,
347
+ flow_source: 'two_factor_login',
348
+ },
349
+ };
350
+ const { status, data } = await this.request('POST', '/bloks/async_action/com.bloks.www.two_step_verification.verify_code.async/', { params: JSON.stringify(params), bk_client_context: JSON.stringify({ bloks_version: IG_BLOKS_VERSION_ID }) });
351
+ if (status !== 200 || (data['status'] != null && data['status'] !== 'ok')) {
352
+ const message = data['message'] ?? 'Two-factor authentication failed';
353
+ throw new InstagramError(message, 'two_factor_failed');
354
+ }
355
+ const loggedInUser = data['logged_in_user'];
356
+ if (loggedInUser) {
357
+ await this.finalizeLogin(data);
358
+ return { userId: this.userId ?? '' };
359
+ }
360
+ if (this.session?.authorization) {
361
+ const userId = this.session.user_id ?? this.cookies.get('ds_user_id');
362
+ if (!userId) {
363
+ throw new InstagramError('Two-factor authentication failed', 'two_factor_failed');
364
+ }
365
+ this.userId = userId;
366
+ this.session.user_id = userId;
367
+ await this.saveSession();
368
+ return { userId };
369
+ }
370
+ throw new InstagramError('Two-factor authentication failed', 'two_factor_failed');
371
+ }
136
372
  async challengeSendCode(apiPath, method = 'email') {
137
373
  this.ensureSession();
138
374
  const { data: getData } = await this.request('GET', `${apiPath}?guid=${this.session.device.uuid}&device_id=${this.session.device.android_device_id}`);
@@ -342,15 +578,19 @@ export class InstagramClient {
342
578
  async preLoginFlow() {
343
579
  const url = `${IG_BASE_URL}/qe/sync/`;
344
580
  const headers = this.buildHeaders();
345
- const response = await fetch(url, {
581
+ const { response } = await fetchWithTimeout(url, {
346
582
  method: 'POST',
347
583
  headers,
348
- body: new URLSearchParams({
584
+ body: new URLSearchParams(signBody({
349
585
  id: this.session.device.uuid,
350
586
  experiments: 'ig_android_fci,ig_android_device_detection_info_upload,ig_android_device_verification_fb_signup',
351
- }).toString(),
352
- });
587
+ })).toString(),
588
+ }, '/qe/sync/');
353
589
  this.extractResponseCookies(response.headers);
590
+ const wwwClaim = response.headers.get('x-ig-set-www-claim');
591
+ if (wwwClaim && this.session) {
592
+ this.session.www_claim = wwwClaim;
593
+ }
354
594
  const pubKeyHeader = response.headers.get('ig-set-password-encryption-pub-key');
355
595
  const keyIdHeader = response.headers.get('ig-set-password-encryption-key-id');
356
596
  if (pubKeyHeader && keyIdHeader) {
@@ -370,8 +610,14 @@ export class InstagramClient {
370
610
  const tag = cipher.getAuthTag();
371
611
  const pubKeyPem = Buffer.from(key.publicKey, 'base64').toString('utf-8');
372
612
  const rsaEncrypted = publicEncrypt({ key: pubKeyPem, padding: constants.RSA_PKCS1_PADDING }, aesKey);
613
+ if (!/^(?:0|[1-9]\d*)$/.test(key.keyId)) {
614
+ throw new InstagramError(`Invalid password encryption key id: ${key.keyId}`, 'encryption_key_invalid');
615
+ }
616
+ const keyIdNum = Number(key.keyId);
617
+ if (keyIdNum > 255) {
618
+ throw new InstagramError(`Invalid password encryption key id: ${key.keyId}`, 'encryption_key_invalid');
619
+ }
373
620
  // Wire format: version(1) | keyId(1) | iv(12) | rsaLen(2 LE) | rsaEncrypted | tag(16) | aesEncrypted
374
- const keyIdNum = Number.parseInt(key.keyId, 10);
375
621
  const buf = Buffer.alloc(1 + 1 + 12 + 2 + rsaEncrypted.length + 16 + encrypted.length);
376
622
  let offset = 0;
377
623
  buf.writeUInt8(1, offset);
@@ -389,50 +635,84 @@ export class InstagramClient {
389
635
  encrypted.copy(buf, offset);
390
636
  return `#PWD_INSTAGRAM:4:${timestamp}:${buf.toString('base64')}`;
391
637
  }
392
- plaintextPassword(password) {
393
- const timestamp = Math.floor(Date.now() / 1000).toString();
394
- return `#PWD_INSTAGRAM:0:${timestamp}:${password}`;
395
- }
396
- async request(method, path, body) {
638
+ async request(method, path, body, options) {
397
639
  const url = `${IG_BASE_URL}${path}`;
398
640
  const headers = this.buildHeaders();
399
- const options = { method, headers };
641
+ const requestInit = { method, headers };
400
642
  if (body) {
401
- options.body = new URLSearchParams(body).toString();
643
+ const payload = options?.signed ? signBody(body) : body;
644
+ requestInit.body = new URLSearchParams(payload).toString();
402
645
  }
403
- const response = await fetch(url, options);
646
+ const { response, readText } = await fetchWithTimeout(url, requestInit, path);
404
647
  this.extractResponseCookies(response.headers);
405
648
  const authHeader = response.headers.get('ig-set-authorization');
406
649
  if (authHeader && this.session) {
407
650
  this.session.authorization = authHeader;
408
651
  }
652
+ const wwwClaim = response.headers.get('x-ig-set-www-claim');
653
+ if (wwwClaim && this.session) {
654
+ this.session.www_claim = wwwClaim;
655
+ }
656
+ const rawBody = await readText();
409
657
  if (response.status === 429) {
410
- throw new InstagramError('Rate limited by Instagram. Try again later.', 'rate_limited');
658
+ this.debugLog?.(`429 from ${path} body=${rawBody}`);
659
+ const igMessage = this.extractJsonMessage(rawBody);
660
+ throw new InstagramError(igMessage
661
+ ? `Rate limited by Instagram: ${igMessage} Wait before trying again.`
662
+ : 'Rate limited by Instagram. Try again later.', 'rate_limited');
411
663
  }
412
664
  let data;
413
665
  try {
414
- data = (await response.json());
666
+ data = JSON.parse(rawBody);
415
667
  }
416
668
  catch {
417
669
  throw new InstagramError(`Failed to parse response from ${path}`, response.status);
418
670
  }
419
671
  return { status: response.status, data };
420
672
  }
673
+ extractJsonMessage(body) {
674
+ try {
675
+ const parsed = JSON.parse(body);
676
+ return parsed['message'] ?? '';
677
+ }
678
+ catch {
679
+ return '';
680
+ }
681
+ }
421
682
  buildHeaders() {
422
683
  const deviceString = this.session?.device.device_string ?? generateDeviceString();
684
+ // Instagram's edge silently drops connections whose fingerprint headers don't match a real
685
+ // Android client. This is a partial alignment toward the current app profile
686
+ // (per subzeroid/instagrapi) covering the highest-signal headers, not a full fingerprint match
687
+ // (e.g. X-Tigon-Is-Retry, X-Zero-*, X-IG-Nav-Chain, X-IG-SALT-IDS are intentionally omitted).
423
688
  const headers = {
424
689
  'User-Agent': buildUserAgent(deviceString),
690
+ 'X-IG-App-Locale': IG_LOCALE,
691
+ 'X-IG-Device-Locale': IG_LOCALE,
692
+ 'X-IG-Mapped-Locale': IG_LOCALE,
693
+ 'X-Pigeon-Session-Id': `UFS-${randomUUID()}-0`,
694
+ 'X-Pigeon-Rawclienttime': (Date.now() / 1000).toFixed(3),
695
+ 'X-IG-Bandwidth-Speed-KBPS': '-1.000',
696
+ 'X-IG-Bandwidth-TotalBytes-B': '0',
697
+ 'X-IG-Bandwidth-TotalTime-MS': '0',
698
+ 'X-IG-App-Startup-Country': 'US',
425
699
  'X-IG-App-ID': IG_APP_ID,
426
- 'X-IG-Capabilities': '3brTv10=',
700
+ 'X-IG-Capabilities': IG_CAPABILITIES,
427
701
  'X-IG-Connection-Type': 'WIFI',
428
702
  'X-IG-Timezone-Offset': String(new Date().getTimezoneOffset() * -60),
703
+ 'X-Bloks-Version-Id': IG_BLOKS_VERSION_ID,
704
+ 'X-FB-HTTP-Engine': 'Tigon/MNS/TCP',
705
+ Priority: 'u=3',
706
+ 'IG-INTENDED-USER-ID': this.userId ?? '0',
429
707
  'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
430
708
  Accept: '*/*',
431
709
  'Accept-Language': 'en-US,en;q=0.9',
432
710
  };
711
+ headers['X-IG-WWW-Claim'] = this.session?.www_claim ?? '0';
433
712
  if (this.session) {
434
713
  headers['X-IG-Device-ID'] = this.session.device.uuid;
435
714
  headers['X-IG-Android-ID'] = this.session.device.android_device_id;
715
+ headers['X-IG-Family-Device-ID'] = this.session.device.phone_id;
436
716
  if (this.session.authorization) {
437
717
  headers['Authorization'] = this.session.authorization;
438
718
  }
@@ -499,6 +779,16 @@ export class InstagramClient {
499
779
  }
500
780
  await this.saveSession();
501
781
  }
782
+ // Reuse one persisted device per machine so repeat logins present a consistent fingerprint.
783
+ // Regenerating device ids each attempt looks like a brand-new phone and hurts login trust.
784
+ async resolveDevice() {
785
+ const existing = await this.credentialManager.loadDevice();
786
+ if (existing)
787
+ return existing;
788
+ const device = generateDevice();
789
+ await this.credentialManager.saveDevice(device);
790
+ return device;
791
+ }
502
792
  async saveSession() {
503
793
  if (!this.session || !this.sessionPath)
504
794
  return;