nodejs-insta-private-api-mqt 1.3.87 → 1.3.89

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.
@@ -45,7 +45,7 @@ class AccountRepository extends Repository {
45
45
  }
46
46
  }
47
47
 
48
- // Simpler: look for any header that contains ig-set-authorization
48
+ // 1) Try straight headers for IG-Set-Authorization
49
49
  for (const key of Object.keys(headers)) {
50
50
  const val = headers[key];
51
51
  if (!val) continue;
@@ -60,7 +60,6 @@ class AccountRepository extends Repository {
60
60
  return token;
61
61
  }
62
62
  }
63
- // Some servers include the header value inside a JSON-like string in other headers
64
63
  if (typeof val === 'string' && val.includes('Bearer IGT:2:')) {
65
64
  const token = this._findBearerInString(val);
66
65
  if (token) {
@@ -120,10 +119,9 @@ class AccountRepository extends Repository {
120
119
  }
121
120
  }
122
121
 
123
- // 2) Check response.body for common fields
122
+ // 2) Check response.body for common fields / embedded layout/login_response
124
123
  const body = response.body;
125
124
  if (body) {
126
- // If body has headers string (some responses embed headers JSON as a string)
127
125
  if (body.headers && typeof body.headers === 'string') {
128
126
  const token = this._findBearerInString(body.headers);
129
127
  if (token) {
@@ -135,22 +133,20 @@ class AccountRepository extends Repository {
135
133
  }
136
134
  }
137
135
 
138
- // If body contains a layout object or string, stringify and search it
139
136
  try {
140
137
  let layoutStr = null;
141
138
  if (body.layout) {
142
- layoutStr =
143
- typeof body.layout === 'string'
144
- ? body.layout
145
- : JSON.stringify(body.layout);
139
+ layoutStr = typeof body.layout === 'string'
140
+ ? body.layout
141
+ : JSON.stringify(body.layout);
146
142
  } else if (body?.layout?.bloks_payload) {
147
143
  layoutStr = JSON.stringify(body.layout.bloks_payload);
148
144
  } else if (body?.bloks_payload) {
149
145
  layoutStr = JSON.stringify(body.bloks_payload);
150
- }
151
- if (!layoutStr && typeof body === 'string') {
146
+ } else if (typeof body === 'string') {
152
147
  layoutStr = body;
153
148
  }
149
+
154
150
  if (layoutStr) {
155
151
  const token = this._findBearerInString(layoutStr);
156
152
  if (token) {
@@ -161,12 +157,11 @@ class AccountRepository extends Repository {
161
157
  return token;
162
158
  }
163
159
 
164
- // Some responses embed a login_response JSON string inside layout; try to extract it
160
+ // Embedded login_response JSON string
165
161
  const loginResponseMatch = layoutStr.match(
166
162
  /"login_response"\s*:\s*"(\\?{.*?\\?})"/s
167
163
  );
168
164
  if (loginResponseMatch) {
169
- // Unescape JSON string
170
165
  let jsonStr = loginResponseMatch[1];
171
166
  try {
172
167
  jsonStr = jsonStr
@@ -174,23 +169,19 @@ class AccountRepository extends Repository {
174
169
  .replace(/\\n/g, '');
175
170
  const parsed = JSON.parse(jsonStr);
176
171
  if (parsed && parsed.headers) {
177
- const token = this._findBearerInString(
178
- parsed.headers
179
- );
180
- if (token) {
181
- this.client.state.authorization = token;
172
+ const t = this._findBearerInString(parsed.headers);
173
+ if (t) {
174
+ this.client.state.authorization = t;
182
175
  try {
183
176
  this.client.state.updateAuthorization();
184
177
  } catch (e) {}
185
- return token;
178
+ return t;
186
179
  }
187
180
  }
188
- } catch (e) {
189
- // ignore parse errors
190
- }
181
+ } catch (e) {}
191
182
  }
192
183
 
193
- // Also try to find IG-Set-Password-Encryption headers (not token but useful)
184
+ // Encryption key id + pub key in layout
194
185
  const encKeyIdMatch =
195
186
  layoutStr.match(
196
187
  /IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i
@@ -202,6 +193,7 @@ class AccountRepository extends Repository {
202
193
  this.client.state.passwordEncryptionKeyId =
203
194
  parseInt(encKeyIdMatch[1]);
204
195
  }
196
+
205
197
  const encPubKeyMatch =
206
198
  layoutStr.match(
207
199
  /IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i
@@ -214,15 +206,13 @@ class AccountRepository extends Repository {
214
206
  encPubKeyMatch[1];
215
207
  }
216
208
 
217
- // Extract user/account identifiers from layout string
209
+ // User / pk id
218
210
  const dsIdMatch =
219
211
  layoutStr.match(
220
212
  /ig-set-ig-u-ds-user-id[\\"\s:]+(\d+)/i
221
213
  ) ||
222
214
  layoutStr.match(/ig-u-ds-user-id[\\"\s:]+(\d+)/i) ||
223
- layoutStr.match(
224
- /"strong_id__"\s*:\s*"(\d+)"/i
225
- ) ||
215
+ layoutStr.match(/"strong_id__"\s*:\s*"(\d+)"/i) ||
226
216
  layoutStr.match(/"pk_id"\s*:\s*"(\d+)"/i) ||
227
217
  layoutStr.match(/"pk"\s*:\s*(\d+)/i);
228
218
  if (dsIdMatch) {
@@ -254,11 +244,8 @@ class AccountRepository extends Repository {
254
244
  this.client.state.mid = midMatch[1];
255
245
  }
256
246
  }
257
- } catch (e) {
258
- // ignore
259
- }
247
+ } catch (e) {}
260
248
 
261
- // 3) If body.logged_in_user exists and we have authorization already, return it
262
249
  if (body.logged_in_user && this.client.state.authorization) {
263
250
  return this.client.state.authorization;
264
251
  }
@@ -267,15 +254,12 @@ class AccountRepository extends Repository {
267
254
  return null;
268
255
  }
269
256
 
270
- // Normalize token string: accept either raw token or "Bearer IGT:2:..." and return trimmed token string
271
257
  _normalizeTokenString(val) {
272
258
  if (!val || typeof val !== 'string') return null;
273
259
 
274
- // If header already contains "Bearer IGT:2:..."
275
260
  const bearer = this._findBearerInString(val);
276
261
  if (bearer) return bearer;
277
262
 
278
- // If header contains JSON with IG-Set-Authorization field
279
263
  try {
280
264
  const maybeJson = JSON.parse(val);
281
265
  if (maybeJson['IG-Set-Authorization']) {
@@ -283,19 +267,14 @@ class AccountRepository extends Repository {
283
267
  maybeJson['IG-Set-Authorization']
284
268
  );
285
269
  }
286
- } catch (e) {
287
- // not JSON
288
- }
270
+ } catch (e) {}
289
271
 
290
- // fallback: return trimmed value
291
272
  return val.trim();
292
273
  }
293
274
 
294
- // Find bearer token in arbitrary string using multiple patterns
295
275
  _findBearerInString(str) {
296
276
  if (!str || typeof str !== 'string') return null;
297
277
 
298
- // Patterns to match the Bearer token in many possible encodings/escapes
299
278
  const bearerPatterns = [
300
279
  /Bearer IGT:2:[A-Za-z0-9+\/=]+/,
301
280
  /Bearer\s+IGT:2:[A-Za-z0-9+\/=]+/,
@@ -309,14 +288,12 @@ class AccountRepository extends Repository {
309
288
  for (const pattern of bearerPatterns) {
310
289
  const m = str.match(pattern);
311
290
  if (m) {
312
- // If capturing group present, prefer it
313
291
  if (m[1]) {
314
292
  const token = m[1].includes('Bearer IGT:2:')
315
293
  ? m[1]
316
294
  : `Bearer IGT:2:${m[1]}`;
317
295
  return token.replace(/\?"/g, '').trim();
318
296
  }
319
- // Otherwise m[0] contains the full token
320
297
  return m[0].replace(/\?"/g, '').trim();
321
298
  }
322
299
  }
@@ -324,8 +301,35 @@ class AccountRepository extends Repository {
324
301
  return null;
325
302
  }
326
303
 
304
+ /**
305
+ * NEW helper: resolve a valid User-Agent from state / request / deviceString
306
+ * so it works even if scriptul tău nu setează explicit state.userAgent.
307
+ */
308
+ _resolveUserAgent() {
309
+ const state = this.client.state || {};
310
+ const req = this.client.request || {};
311
+
312
+ const candidates = [
313
+ state.userAgent,
314
+ state.appUserAgent,
315
+ req.userAgent,
316
+ state.deviceString,
317
+ ].filter(Boolean);
318
+
319
+ let userAgent = candidates.length > 0 ? candidates[0] : null;
320
+
321
+ if (!userAgent) {
322
+ // Fallback – Pixel 9 Pro style UA (nu mai e Xiaomi)
323
+ userAgent =
324
+ 'Instagram 371.0.0.0.23 Android (35/15; 505dpi; 1440x3120; google; Pixel 9 Pro; pixel9pro; qcom; ro_RO; 703217507)';
325
+ }
326
+
327
+ // Sincronizăm în state ca să fie disponibil peste tot
328
+ this.client.state.userAgent = userAgent;
329
+ return userAgent;
330
+ }
331
+
327
332
  // Ensure we have a valid csrftoken cookie (and mid) before sensitive endpoints.
328
- // Instagram typically sets csrftoken via /api/v1/si/fetch_headers/.
329
333
  async ensureCsrfToken() {
330
334
  const cookieToken = this.client.state.cookieCsrfToken;
331
335
  if (
@@ -339,15 +343,12 @@ class AccountRepository extends Repository {
339
343
  return cookieToken;
340
344
  }
341
345
 
342
- // Warmup endpoint that returns/set cookies (csrftoken, mid, etc.)
343
346
  try {
344
347
  await this.client.request.send({
345
348
  method: 'GET',
346
349
  url: `/api/v1/si/fetch_headers/?challenge_type=signup&guid=${this.client.state.uuid}`,
347
350
  });
348
- } catch (e) {
349
- // ignore warmup failures
350
- }
351
+ } catch (e) {}
351
352
 
352
353
  const token = this.client.state.cookieCsrfToken;
353
354
  if (
@@ -365,9 +366,8 @@ class AccountRepository extends Repository {
365
366
  }
366
367
 
367
368
  /**
368
- * Internal: perform the Bloks OAuth token fetch step that the official app does
369
- * just before password login (com.bloks.www.caa.login.oauth.token.fetch.async).
370
- * This is best-effort only and never throws to the caller.
369
+ * Internal: pre-login Bloks OAuth token fetch, imită exact apelul oficial
370
+ * com.bloks.www.caa.login.oauth.token.fetch.async
371
371
  */
372
372
  async _prefetchOauthTokenForLogin(username) {
373
373
  if (!username) return null;
@@ -375,7 +375,6 @@ class AccountRepository extends Repository {
375
375
  try {
376
376
  const nowSec = Math.floor(Date.now() / 1000);
377
377
 
378
- // Device identifiers – keep them consistent with the main login flow
379
378
  const androidDeviceId =
380
379
  this.client.state.androidDeviceId ||
381
380
  this.client.state.deviceId ||
@@ -404,7 +403,6 @@ class AccountRepository extends Repository {
404
403
  `${Date.now()}${Math.floor(Math.random() * 1000)}`
405
404
  );
406
405
 
407
- // AAC object as seen in real traffic: timestamp + UUID + random token
408
406
  const aacInitTimestamp =
409
407
  nowSec - Math.floor(Math.random() * 50);
410
408
  const aacjid = crypto.randomUUID
@@ -460,9 +458,7 @@ class AccountRepository extends Repository {
460
458
 
461
459
  const lang = this.client.state.language || 'ro_RO';
462
460
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
463
- const userAgent =
464
- this.client.state.userAgent ||
465
- 'Instagram 371.0.0.0.23 Android (30/11; 320dpi; 720x1449; Xiaomi/Redmi; M2006C3MNG; angelican; mt6765; ro_RO; 703217507)';
461
+ const userAgent = this._resolveUserAgent();
466
462
 
467
463
  const timezoneOffset =
468
464
  typeof this.client.state.timezoneOffset === 'number'
@@ -474,7 +470,6 @@ class AccountRepository extends Repository {
474
470
  'content-type':
475
471
  'application/x-www-form-urlencoded; charset=UTF-8',
476
472
  'ig-intended-user-id': '0',
477
- // Critical: matches the captured request header
478
473
  priority: 'u=3',
479
474
 
480
475
  'x-bloks-is-layout-rtl': 'false',
@@ -556,7 +551,6 @@ class AccountRepository extends Repository {
556
551
  headers: bloksHeaders,
557
552
  });
558
553
 
559
- // Optional debug dump so you can inspect the exact server response.
560
554
  try {
561
555
  const fs = require('fs');
562
556
  const path = require('path');
@@ -584,13 +578,466 @@ class AccountRepository extends Repository {
584
578
  JSON.stringify(debugPayload, null, 2),
585
579
  'utf8'
586
580
  );
587
- } catch (e) {
588
- // Never break login on debug error
589
- }
581
+ } catch (e) {}
582
+
583
+ return response.body;
584
+ } catch (e) {
585
+ return null;
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Step 1: launcher/mobileconfig to fetch mobile config + encryption keys.
591
+ * Best effort – errors are ignored.
592
+ */
593
+ async _launcherMobileConfig(preLogin = true) {
594
+ const nowSec = Math.floor(Date.now() / 1000);
595
+ const state = this.client.state || {};
596
+
597
+ const androidDeviceId =
598
+ state.androidDeviceId ||
599
+ state.deviceId ||
600
+ `android-${crypto.randomBytes(8).toString('hex')}`;
601
+
602
+ const familyDeviceId =
603
+ state.phoneId ||
604
+ state.familyDeviceId ||
605
+ (crypto.randomUUID
606
+ ? crypto.randomUUID()
607
+ : require('uuid').v4());
608
+
609
+ const qeDeviceId =
610
+ state.deviceId ||
611
+ state.qeDeviceId ||
612
+ (crypto.randomUUID
613
+ ? crypto.randomUUID()
614
+ : require('uuid').v4());
615
+
616
+ const deviceUUID = state.uuid || qeDeviceId;
617
+ const userId = state.cookieUserId || '0';
618
+
619
+ const bloksVersionId =
620
+ state.bloksVersionId ||
621
+ '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
622
+
623
+ const lang = state.language || 'ro_RO';
624
+ const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
625
+ const userAgent = this._resolveUserAgent();
626
+
627
+ const timezoneOffset =
628
+ typeof state.timezoneOffset === 'number'
629
+ ? state.timezoneOffset
630
+ : 7200;
631
+
632
+ const params = {
633
+ bool_opt_policy: '0',
634
+ mobileconfig: '',
635
+ api_version: '9',
636
+ client_context: '["opt,value_hash"]',
637
+ unit_type: '2',
638
+ use_case: 'STANDARD',
639
+ query_hash:
640
+ 'f00b9d0869db3969378d8d06bfccb24b5ef078012c8e199cba961cd5dfedaa88',
641
+ ts: String(nowSec),
642
+ _uid: userId,
643
+ device_id: deviceUUID,
644
+ _uuid: deviceUUID,
645
+ fetch_mode: 'CONFIG_SYNC_ONLY',
646
+ fetch_type: 'ASYNC_FULL',
647
+ };
648
+
649
+ const headers = {
650
+ 'accept-language': acceptLanguage,
651
+ 'content-type':
652
+ 'application/x-www-form-urlencoded; charset=UTF-8',
653
+ priority: 'u=3',
654
+
655
+ 'x-bloks-is-layout-rtl': 'false',
656
+ 'x-bloks-prism-ax-base-colors-enabled': 'false',
657
+ 'x-bloks-prism-button-version': 'CONTROL',
658
+ 'x-bloks-prism-colors-enabled': 'true',
659
+ 'x-bloks-prism-font-enabled': 'false',
660
+ 'x-bloks-prism-indigo-link-version': '0',
661
+ 'x-bloks-version-id': bloksVersionId,
662
+
663
+ 'x-fb-client-ip': 'True',
664
+ 'x-fb-connection-type': 'WIFI',
665
+ 'x-fb-server-cluster': 'True',
666
+ 'x-fb-network-properties':
667
+ 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
668
+ 'x-fb-http-engine': 'MNS/TCP',
669
+ 'x-fb-rmd': 'state=URL_ELIGIBLE',
670
+
671
+ 'x-ig-android-id': androidDeviceId,
672
+ 'x-ig-app-id': String(
673
+ state.fbAnalyticsApplicationId || '567067343352427'
674
+ ),
675
+ 'x-ig-app-locale': lang,
676
+ 'x-ig-bandwidth-speed-kbps': (
677
+ Math.random() * 1500 +
678
+ 800
679
+ ).toFixed(3),
680
+ 'x-ig-bandwidth-totalbytes-b': '0',
681
+ 'x-ig-bandwidth-totaltime-ms': '0',
682
+ 'x-ig-client-endpoint':
683
+ 'LockoutFragment:dogfooding_lockout',
684
+ 'x-ig-capabilities': '3brTv10=',
685
+ 'x-ig-connection-type': 'WIFI',
686
+ 'x-ig-device-id': deviceUUID,
687
+ 'x-ig-device-languages': '{"system_languages":"ro-RO"}',
688
+ 'x-ig-device-locale': lang,
689
+ 'x-ig-family-device-id': familyDeviceId,
690
+ 'x-ig-mapped-locale': lang,
691
+ 'x-ig-nav-chain':
692
+ 'LockoutFragment:dogfooding_lockout:1:cold_start',
693
+ 'x-ig-salt-ids':
694
+ '220140399,332020310,974466465,974460658',
695
+ 'x-ig-timezone-offset': String(timezoneOffset),
696
+ 'x-ig-www-claim': state.igWWWClaim || '0',
697
+ 'x-mid':
698
+ state.mid ||
699
+ state.machineId ||
700
+ `aZ${crypto.randomBytes(8).toString('hex')}`,
701
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(
702
+ Math.random() * 1000
703
+ )
704
+ .toString()
705
+ .padStart(3, '0')}`,
706
+ 'x-pigeon-session-id':
707
+ state.pigeonSessionId ||
708
+ `UFS-${crypto.randomBytes(16).toString('hex')}-M`,
709
+ 'x-tigon-is-retry': 'False',
710
+
711
+ 'accept-encoding': 'gzip, deflate, br',
712
+ 'user-agent': userAgent,
713
+ };
714
+
715
+ if (state.cookieUserId) {
716
+ headers['ig-intended-user-id'] = String(state.cookieUserId);
717
+ headers['ig-u-ds-user-id'] = String(state.cookieUserId);
718
+ }
719
+ if (state.igURur) {
720
+ headers['ig-u-rur'] = state.igURur;
721
+ }
722
+ if (state.authorization) {
723
+ headers['authorization'] = state.authorization;
724
+ }
725
+
726
+ try {
727
+ const response = await this.client.request.send({
728
+ method: 'POST',
729
+ url: '/api/v1/launcher/mobileconfig/',
730
+ form: this.client.request.sign(params),
731
+ headers,
732
+ });
733
+
734
+ try {
735
+ const fs = require('fs');
736
+ const path = require('path');
737
+ const debugDir = path.join(
738
+ process.cwd(),
739
+ 'authinfo_instagram'
740
+ );
741
+ const debugFile = path.join(
742
+ debugDir,
743
+ 'launcher-mobileconfig-debug.json'
744
+ );
745
+ try {
746
+ fs.mkdirSync(debugDir, { recursive: true });
747
+ } catch (e) {}
748
+ const debugPayload = {
749
+ at: new Date().toISOString(),
750
+ statusCode:
751
+ response.statusCode || response.status || null,
752
+ headers: response.headers || null,
753
+ body: response.body || null,
754
+ };
755
+ fs.writeFileSync(
756
+ debugFile,
757
+ JSON.stringify(debugPayload, null, 2),
758
+ 'utf8'
759
+ );
760
+ } catch (e) {}
761
+
762
+ return response.body;
763
+ } catch (e) {
764
+ return null;
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Step 2: Android Keystore attestation – moves x-ig-attest-params
770
+ * payload to dedicated endpoint /api/v1/attestation/create_android_keystore/.
771
+ */
772
+ async _createAndroidKeystoreAttestation() {
773
+ const state = this.client.state || {};
774
+ const nowSec = Math.floor(Date.now() / 1000);
775
+
776
+ const androidDeviceId =
777
+ state.androidDeviceId ||
778
+ state.deviceId ||
779
+ `android-${crypto.randomBytes(8).toString('hex')}`;
780
+
781
+ const familyDeviceId =
782
+ state.phoneId ||
783
+ state.familyDeviceId ||
784
+ (crypto.randomUUID
785
+ ? crypto.randomUUID()
786
+ : require('uuid').v4());
787
+
788
+ const qeDeviceId =
789
+ state.deviceId ||
790
+ state.qeDeviceId ||
791
+ (crypto.randomUUID
792
+ ? crypto.randomUUID()
793
+ : require('uuid').v4());
794
+
795
+ const deviceUUID = state.uuid || qeDeviceId;
796
+ const userId = state.cookieUserId || '0';
797
+
798
+ const attestParams =
799
+ AccountRepository.generateAttestParams(state);
800
+
801
+ const lang = state.language || 'ro_RO';
802
+ const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
803
+ const userAgent = this._resolveUserAgent();
804
+ const timezoneOffset =
805
+ typeof state.timezoneOffset === 'number'
806
+ ? state.timezoneOffset
807
+ : 7200;
808
+
809
+ const params = {
810
+ _uid: userId,
811
+ _uuid: deviceUUID,
812
+ device_id: androidDeviceId,
813
+ family_device_id: familyDeviceId,
814
+ ts: String(nowSec),
815
+ ...attestParams,
816
+ };
817
+
818
+ const headers = {
819
+ 'accept-language': acceptLanguage,
820
+ 'content-type':
821
+ 'application/x-www-form-urlencoded; charset=UTF-8',
822
+ priority: 'u=3',
823
+
824
+ 'x-ig-android-id': androidDeviceId,
825
+ 'x-ig-device-id': qeDeviceId,
826
+ 'x-ig-family-device-id': familyDeviceId,
827
+ 'x-ig-timezone-offset': String(timezoneOffset),
828
+ 'x-ig-app-id': String(
829
+ state.fbAnalyticsApplicationId || '567067343352427'
830
+ ),
831
+ 'x-ig-app-locale': lang,
832
+ 'x-ig-device-locale': lang,
833
+ 'x-ig-mapped-locale': lang,
834
+ 'x-ig-connection-type': 'WIFI',
835
+ 'x-ig-capabilities': '3brTv10=',
836
+ 'x-ig-www-claim': state.igWWWClaim || '0',
837
+
838
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(
839
+ Math.random() * 1000
840
+ )
841
+ .toString()
842
+ .padStart(3, '0')}`,
843
+ 'x-pigeon-session-id':
844
+ state.pigeonSessionId ||
845
+ `UFS-${crypto.randomBytes(16).toString('hex')}-A`,
846
+ 'x-mid':
847
+ state.mid ||
848
+ state.machineId ||
849
+ `aZ${crypto.randomBytes(8).toString('hex')}`,
850
+
851
+ 'x-fb-client-ip': 'True',
852
+ 'x-fb-connection-type': 'WIFI',
853
+ 'x-fb-server-cluster': 'True',
854
+ 'x-fb-http-engine': 'MNS/TCP',
855
+ 'x-fb-network-properties':
856
+ 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
857
+ 'x-fb-rmd': 'state=URL_ELIGIBLE',
858
+
859
+ 'accept-encoding': 'gzip, deflate, br',
860
+ 'user-agent': userAgent,
861
+ };
862
+
863
+ if (state.cookieUserId) {
864
+ headers['ig-intended-user-id'] = String(state.cookieUserId);
865
+ }
866
+ if (state.authorization) {
867
+ headers['authorization'] = state.authorization;
868
+ }
869
+ if (state.igURur) {
870
+ headers['ig-u-rur'] = state.igURur;
871
+ }
872
+
873
+ try {
874
+ const response = await this.client.request.send({
875
+ method: 'POST',
876
+ url: '/api/v1/attestation/create_android_keystore/',
877
+ form: this.client.request.sign(params),
878
+ headers,
879
+ });
880
+
881
+ try {
882
+ const fs = require('fs');
883
+ const path = require('path');
884
+ const debugDir = path.join(
885
+ process.cwd(),
886
+ 'authinfo_instagram'
887
+ );
888
+ const debugFile = path.join(
889
+ debugDir,
890
+ 'attestation-debug.json'
891
+ );
892
+ try {
893
+ fs.mkdirSync(debugDir, { recursive: true });
894
+ } catch (e) {}
895
+ const debugPayload = {
896
+ at: new Date().toISOString(),
897
+ statusCode:
898
+ response.statusCode || response.status || null,
899
+ headers: response.headers || null,
900
+ body: response.body || null,
901
+ };
902
+ fs.writeFileSync(
903
+ debugFile,
904
+ JSON.stringify(debugPayload, null, 2),
905
+ 'utf8'
906
+ );
907
+ } catch (e) {}
908
+
909
+ return response.body;
910
+ } catch (e) {
911
+ return null;
912
+ }
913
+ }
914
+
915
+ /**
916
+ * Step 3 & 4 & 5 helpers: Terms of service preload, process client data,
917
+ * and (optional) phone number prefill.
918
+ * These are best-effort calls; failures are ignored.
919
+ */
920
+ async _preloadTermsOfService() {
921
+ const state = this.client.state || {};
922
+ const lang = state.language || 'ro_RO';
923
+ const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
924
+ const userAgent = this._resolveUserAgent();
925
+ const bloksVersionId =
926
+ state.bloksVersionId ||
927
+ '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
928
+
929
+ const paramsJson = JSON.stringify({});
930
+ const bkClientContext = JSON.stringify({
931
+ bloks_version: bloksVersionId,
932
+ styles_id: 'instagram',
933
+ });
934
+
935
+ const headers = {
936
+ 'accept-language': acceptLanguage,
937
+ 'content-type':
938
+ 'application/x-www-form-urlencoded; charset=UTF-8',
939
+ priority: 'u=3',
940
+ 'x-bloks-version-id': bloksVersionId,
941
+ 'user-agent': userAgent,
942
+ };
943
+
944
+ try {
945
+ const response = await this.client.request.send({
946
+ method: 'POST',
947
+ url: '/api/v1/bloks/apps/com.bloks.www.caa.login.oxygen_preloads_terms_of_service/',
948
+ form: {
949
+ params: paramsJson,
950
+ bk_client_context: bkClientContext,
951
+ bloks_versioning_id: bloksVersionId,
952
+ },
953
+ headers,
954
+ });
955
+ return response.body;
956
+ } catch (e) {
957
+ return null;
958
+ }
959
+ }
960
+
961
+ async _processClientDataAndRedirect(username) {
962
+ const state = this.client.state || {};
963
+ const lang = state.language || 'ro_RO';
964
+ const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
965
+ const userAgent = this._resolveUserAgent();
966
+ const bloksVersionId =
967
+ state.bloksVersionId ||
968
+ '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
969
+
970
+ const paramsJson = JSON.stringify({
971
+ username_input: username || '',
972
+ });
973
+ const bkClientContext = JSON.stringify({
974
+ bloks_version: bloksVersionId,
975
+ styles_id: 'instagram',
976
+ });
977
+
978
+ const headers = {
979
+ 'accept-language': acceptLanguage,
980
+ 'content-type':
981
+ 'application/x-www-form-urlencoded; charset=UTF-8',
982
+ priority: 'u=3',
983
+ 'x-bloks-version-id': bloksVersionId,
984
+ 'user-agent': userAgent,
985
+ };
986
+
987
+ try {
988
+ const response = await this.client.request.send({
989
+ method: 'POST',
990
+ url: '/api/v1/bloks/async_action/com.bloks.www.bloks.caa.login.process_client_data_and_redirect/',
991
+ form: {
992
+ params: paramsJson,
993
+ bk_client_context: bkClientContext,
994
+ bloks_versioning_id: bloksVersionId,
995
+ },
996
+ headers,
997
+ });
998
+ return response.body;
999
+ } catch (e) {
1000
+ return null;
1001
+ }
1002
+ }
590
1003
 
1004
+ async _phoneNumberPrefill() {
1005
+ const state = this.client.state || {};
1006
+ const lang = state.language || 'ro_RO';
1007
+ const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
1008
+ const userAgent = this._resolveUserAgent();
1009
+ const bloksVersionId =
1010
+ state.bloksVersionId ||
1011
+ '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
1012
+
1013
+ const paramsJson = JSON.stringify({});
1014
+ const bkClientContext = JSON.stringify({
1015
+ bloks_version: bloksVersionId,
1016
+ styles_id: 'instagram',
1017
+ });
1018
+
1019
+ const headers = {
1020
+ 'accept-language': acceptLanguage,
1021
+ 'content-type':
1022
+ 'application/x-www-form-urlencoded; charset=UTF-8',
1023
+ priority: 'u=3',
1024
+ 'x-bloks-version-id': bloksVersionId,
1025
+ 'user-agent': userAgent,
1026
+ };
1027
+
1028
+ try {
1029
+ const response = await this.client.request.send({
1030
+ method: 'POST',
1031
+ url: '/api/v1/bloks/async_action/com.bloks.www.bloks.caa.phone.number.prefill.async.controller/',
1032
+ form: {
1033
+ params: paramsJson,
1034
+ bk_client_context: bkClientContext,
1035
+ bloks_versioning_id: bloksVersionId,
1036
+ },
1037
+ headers,
1038
+ });
591
1039
  return response.body;
592
1040
  } catch (e) {
593
- // Completely swallow any errors; this step should never break login.
594
1041
  return null;
595
1042
  }
596
1043
  }
@@ -618,19 +1065,32 @@ class AccountRepository extends Repository {
618
1065
 
619
1066
  const { encrypted, time } = this.encryptPassword(password);
620
1067
 
621
- // optional CSRF warmup (like app does before login)
622
1068
  await this.ensureCsrfToken();
623
1069
 
624
- // Pre-login Bloks OAuth token fetch (mirrors captured request).
625
- // This helps the flow look closer to the real app, but is best-effort only.
1070
+ // Step 1–5: mobileconfig, keystore attestation, TOS preload, process client data, phone prefill
1071
+ try {
1072
+ await this._launcherMobileConfig(true);
1073
+ } catch (e) {}
1074
+ try {
1075
+ await this._createAndroidKeystoreAttestation();
1076
+ } catch (e) {}
1077
+ try {
1078
+ await this._preloadTermsOfService();
1079
+ } catch (e) {}
1080
+ try {
1081
+ await this._processClientDataAndRedirect(username);
1082
+ } catch (e) {}
1083
+ try {
1084
+ await this._phoneNumberPrefill();
1085
+ } catch (e) {}
1086
+
1087
+ // Step 6: OAuth token fetch
626
1088
  try {
627
1089
  await this._prefetchOauthTokenForLogin(username);
628
- } catch (e) {
629
- // Ignore any errors from the prefetch step.
630
- }
1090
+ } catch (e) {}
631
1091
 
1092
+ // Step 7: real login
632
1093
  return this.requestWithRetry(async () => {
633
- // ====== client_input_params (mirror of real traffic with password) ======
634
1094
  const nowSec = Math.floor(Date.now() / 1000);
635
1095
  const aacInitTimestamp =
636
1096
  nowSec - Math.floor(Math.random() * 50);
@@ -717,7 +1177,6 @@ class AccountRepository extends Repository {
717
1177
  contact_point: username,
718
1178
  };
719
1179
 
720
- // ====== server_params (mirror after reverse engineering) ======
721
1180
  const waterfallId = crypto.randomUUID
722
1181
  ? crypto.randomUUID()
723
1182
  : require('uuid').v4();
@@ -770,10 +1229,6 @@ class AccountRepository extends Repository {
770
1229
  server_params: serverParams,
771
1230
  });
772
1231
 
773
- const attestParams =
774
- AccountRepository.generateAttestParams(
775
- this.client.state
776
- );
777
1232
  const bloksVersionId =
778
1233
  this.client.state.bloksVersionId ||
779
1234
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
@@ -783,12 +1238,9 @@ class AccountRepository extends Repository {
783
1238
  styles_id: 'instagram',
784
1239
  });
785
1240
 
786
- // ====== HEADERS – modelled after real traffic ======
787
1241
  const lang = this.client.state.language || 'ro_RO';
788
1242
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
789
- const userAgent =
790
- this.client.state.userAgent ||
791
- 'Instagram 371.0.0.0.23 Android (30/11; 320dpi; 720x1449; Xiaomi/Redmi; M2006C3MNG; angelican; mt6765; ro_RO; 703217507)';
1243
+ const userAgent = this._resolveUserAgent();
792
1244
 
793
1245
  const bloksHeaders = {
794
1246
  'accept-language': acceptLanguage,
@@ -831,7 +1283,6 @@ class AccountRepository extends Repository {
831
1283
  '567067343352427'
832
1284
  ),
833
1285
  'x-ig-app-locale': lang,
834
- 'x-ig-attest-params': JSON.stringify(attestParams),
835
1286
  'x-ig-bandwidth-speed-kbps': (
836
1287
  Math.random() * 1500 +
837
1288
  800
@@ -887,7 +1338,7 @@ class AccountRepository extends Repository {
887
1338
  headers: bloksHeaders,
888
1339
  });
889
1340
 
890
- // === DEBUG LOGIN: save response for offline analysis ===
1341
+ // DEBUG login response
891
1342
  try {
892
1343
  const fs = require('fs');
893
1344
  const path = require('path');
@@ -915,14 +1366,10 @@ class AccountRepository extends Repository {
915
1366
  JSON.stringify(debugPayload, null, 2),
916
1367
  'utf8'
917
1368
  );
918
- } catch (e) {
919
- // don't break login on debug failure
920
- }
921
- // === END DEBUG LOGIN ===
1369
+ } catch (e) {}
922
1370
 
923
1371
  const body = response.body;
924
1372
 
925
- // Immediately attempt to extract and save authorization token from the response
926
1373
  this._extractAndSaveAuthorization(response);
927
1374
 
928
1375
  if (body && body.two_factor_required) {
@@ -956,7 +1403,6 @@ class AccountRepository extends Repository {
956
1403
  ? body.layout
957
1404
  : JSON.stringify(body.layout);
958
1405
 
959
- // If layout contains two-factor markers, parse and throw
960
1406
  if (
961
1407
  layoutStr.includes('two_factor_required') ||
962
1408
  layoutStr.includes('"two_factor_info"')
@@ -1000,7 +1446,6 @@ class AccountRepository extends Repository {
1000
1446
  throw err;
1001
1447
  }
1002
1448
 
1003
- // Additional attempt to extract token from layout string
1004
1449
  const tokenFromLayout =
1005
1450
  this._findBearerInString(layoutStr);
1006
1451
  if (tokenFromLayout) {
@@ -1011,7 +1456,6 @@ class AccountRepository extends Repository {
1011
1456
  } catch (e) {}
1012
1457
  }
1013
1458
 
1014
- // Extract pk (user id) from layout if present
1015
1459
  const pkPatterns = [
1016
1460
  /\\"pk\\":\s*(\d+)/,
1017
1461
  /"pk":\s*(\d+)/,
@@ -1032,7 +1476,6 @@ class AccountRepository extends Repository {
1032
1476
  }
1033
1477
  }
1034
1478
 
1035
- // Extract IG-Set-WWW-Claim if present
1036
1479
  const wwwClaimPatterns = [
1037
1480
  /IG-Set-WWW-Claim[\\"\s:]+([a-f0-9]+)/i,
1038
1481
  /ig-set-www-claim[\\"\s:]+([a-f0-9]+)/i,
@@ -1048,43 +1491,41 @@ class AccountRepository extends Repository {
1048
1491
  }
1049
1492
  }
1050
1493
 
1051
- // Extract encryption key id and pub key (already attempted earlier)
1052
- const encKeyIdMatch =
1494
+ const encKeyIdMatch2 =
1053
1495
  layoutStr.match(
1054
1496
  /IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i
1055
1497
  ) ||
1056
1498
  layoutStr.match(
1057
1499
  /password.encryption.key.id[\\"\s:]+(\d+)/i
1058
1500
  );
1059
- if (encKeyIdMatch) {
1501
+ if (encKeyIdMatch2) {
1060
1502
  this.client.state.passwordEncryptionKeyId =
1061
- parseInt(encKeyIdMatch[1]);
1503
+ parseInt(encKeyIdMatch2[1]);
1062
1504
  }
1063
1505
 
1064
- const encPubKeyMatch =
1506
+ const encPubKeyMatch2 =
1065
1507
  layoutStr.match(
1066
1508
  /IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i
1067
1509
  ) ||
1068
1510
  layoutStr.match(
1069
1511
  /password.encryption.pub.key[\\"\s:]+([A-Za-z0-9+\/=]+)/i
1070
1512
  );
1071
- if (encPubKeyMatch) {
1513
+ if (encPubKeyMatch2) {
1072
1514
  this.client.state.passwordEncryptionPubKey =
1073
- encPubKeyMatch[1];
1515
+ encPubKeyMatch2[1];
1074
1516
  }
1075
1517
 
1076
- const midMatch =
1518
+ const midMatch2 =
1077
1519
  layoutStr.match(
1078
1520
  /ig-set-x-mid[\\"\s:]+([A-Za-z0-9+\/=_-]+)/i
1079
1521
  ) ||
1080
1522
  layoutStr.match(
1081
1523
  /\\"x-mid\\"[\\"\s:]+([A-Za-z0-9+\/=_-]+)/i
1082
1524
  );
1083
- if (midMatch) {
1084
- this.client.state.mid = midMatch[1];
1525
+ if (midMatch2) {
1526
+ this.client.state.mid = midMatch2[1];
1085
1527
  }
1086
1528
 
1087
- // If layout contains a direct logged_in_user JSON, extract and return it
1088
1529
  const loginResponsePatterns = [
1089
1530
  /\\"logged_in_user\\":\{[^}]*\\"pk\\":(\d+)[^}]*\\"username\\":\\"([^"\\]+)\\"/,
1090
1531
  /"logged_in_user":\{[^}]*"pk":(\d+)[^}]*"username":"([^"]+)"/,
@@ -1106,7 +1547,6 @@ class AccountRepository extends Repository {
1106
1547
  }
1107
1548
  }
1108
1549
 
1109
- // If we have extracted a pk and we also have authorization, try to fetch current user
1110
1550
  if (
1111
1551
  extractedPk &&
1112
1552
  this.client.state.authorization
@@ -1130,19 +1570,16 @@ class AccountRepository extends Repository {
1130
1570
  }
1131
1571
  }
1132
1572
 
1133
- // If server returned logged_in_user directly in body
1134
1573
  if (body && body.logged_in_user) {
1135
1574
  const lu = body.logged_in_user;
1136
1575
  if (lu.pk) {
1137
1576
  this.client.state.cookieUserId = String(lu.pk);
1138
1577
  this.client.state._userId = String(lu.pk);
1139
1578
  }
1140
- // Ensure authorization is saved if present in response headers
1141
1579
  this._extractAndSaveAuthorization(response);
1142
1580
  return lu;
1143
1581
  }
1144
1582
 
1145
- // If we already have authorization, try to fetch current user to populate state
1146
1583
  if (this.client.state.authorization) {
1147
1584
  try {
1148
1585
  const userInfo = await this.currentUser();
@@ -1154,7 +1591,6 @@ class AccountRepository extends Repository {
1154
1591
  }
1155
1592
  return user;
1156
1593
  } catch (e) {
1157
- // fallback: return raw body
1158
1594
  return body;
1159
1595
  }
1160
1596
  }
@@ -1184,7 +1620,6 @@ class AccountRepository extends Repository {
1184
1620
  phone_id: this.client.state.phoneId,
1185
1621
  }),
1186
1622
  });
1187
- // Save authorization if present in two-factor response
1188
1623
  this._extractAndSaveAuthorization(response);
1189
1624
  return response.body;
1190
1625
  });
@@ -1572,18 +2007,6 @@ class AccountRepository extends Repository {
1572
2007
  }
1573
2008
 
1574
2009
  static generateAttestParams(state) {
1575
- // Emulate Instagram's keystore attestation as closely as possible:
1576
- // - version: 2
1577
- // - type: "keystore"
1578
- // - errors: [0]
1579
- // - challenge_nonce: random base64url
1580
- // - signed_nonce: ECDSA signature over the nonce
1581
- // - key_hash: sha256(spki(publicKey))
1582
- // - certificate_chain: 4 PEM certificates concatenated (leaf + 2 intermediates + root)
1583
- //
1584
- // NOTE: This is *not* a real hardware-backed attestation chain, but it mirrors
1585
- // the structure of the official app very closely so the server sees the same
1586
- // shape: a single attestation object with a 4-certificate chain.
1587
2010
  const challengeNonce = crypto
1588
2011
  .randomBytes(24)
1589
2012
  .toString('base64url');
@@ -1593,7 +2016,6 @@ class AccountRepository extends Repository {
1593
2016
  namedCurve: 'prime256v1',
1594
2017
  }
1595
2018
  );
1596
- // Sign the challenge nonce with the private key (simulating TEE signing).
1597
2019
  const signedData = crypto.sign(
1598
2020
  null,
1599
2021
  Buffer.from(challengeNonce),
@@ -1609,7 +2031,6 @@ class AccountRepository extends Repository {
1609
2031
  .update(publicKeyDer)
1610
2032
  .digest('hex');
1611
2033
 
1612
- // Build a 4-certificate chain (leaf + 2 intermediates + root)
1613
2034
  const leafCertPem =
1614
2035
  AccountRepository._generateSelfSignedCert(
1615
2036
  privateKey,
@@ -1876,6 +2297,4 @@ class AccountRepository extends Repository {
1876
2297
  return response.body;
1877
2298
  });
1878
2299
  }
1879
- }
1880
-
1881
- module.exports = AccountRepository;
2300
+ }module.exports = AccountRepository;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-insta-private-api-mqt",
3
- "version": "1.3.87",
3
+ "version": "1.3.89",
4
4
  "description": "Complete Instagram MQTT protocol with full-featured REALTIME and REST API — all in one project.",
5
5
 
6
6
  "main": "dist/dist/index.js",