nodejs-insta-private-api-mqt 1.4.4 → 1.4.5

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.
@@ -1,6 +1,55 @@
1
1
  const Repository = require('../core/repository');
2
2
  const crypto = require('crypto');
3
3
 
4
+ // ─── DEBUG helper ────────────────────────────────────────────────────────────
5
+ // Set env DEBUG_IG=true to enable disk writes.
6
+ // In production leave it undefined / false.
7
+ const DEBUG_IG = process.env.DEBUG_IG === 'true';
8
+
9
+ function debugWrite(filename, payload) {
10
+ if (!DEBUG_IG) return;
11
+ try {
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const debugDir = path.join(process.cwd(), 'authinfo_instagram');
15
+ try { fs.mkdirSync(debugDir, { recursive: true }); } catch (e) {}
16
+ fs.writeFileSync(
17
+ path.join(debugDir, filename),
18
+ JSON.stringify(payload, null, 2),
19
+ 'utf8'
20
+ );
21
+ } catch (e) {}
22
+ }
23
+
24
+ // ─── Local IP helper ──────────────────────────────────────────────────────────
25
+ // Detects the first non-loopback IP of the machine for x-fb-network-properties.
26
+ function getLocalIp() {
27
+ try {
28
+ const os = require('os');
29
+ const ifaces = os.networkInterfaces();
30
+ for (const name of Object.keys(ifaces)) {
31
+ for (const iface of ifaces[name]) {
32
+ if (iface.family === 'IPv4' && !iface.internal) {
33
+ return iface.address;
34
+ }
35
+ }
36
+ }
37
+ } catch (e) {}
38
+ return '192.168.1.100'; // reasonable fallback
39
+ }
40
+
41
+ // ─── Network properties generator ────────────────────────────────────────────
42
+ // Dynamically generates x-fb-network-properties header, no hardcoded VPN.
43
+ function buildNetworkProperties() {
44
+ const localIp = getLocalIp();
45
+ // If IP is 10.x.x.x or 172.16-31.x.x.x it may be a real VPN — keep prefix
46
+ const isVpnRange =
47
+ /^10\./.test(localIp) ||
48
+ /^172\.(1[6-9]|2\d|3[01])\./.test(localIp);
49
+ const prefix = isVpnRange ? 'VPN;Validated' : 'Validated';
50
+ return `${prefix};LocalAddrs=/${localIp},;`;
51
+ }
52
+
4
53
  class AccountRepository extends Repository {
5
54
  constructor(client) {
6
55
  super(client);
@@ -9,8 +58,7 @@ class AccountRepository extends Repository {
9
58
 
10
59
  async requestWithRetry(requestFn, retries = 0) {
11
60
  try {
12
- const result = await requestFn();
13
- return result;
61
+ return await requestFn();
14
62
  } catch (error) {
15
63
  const shouldRetry =
16
64
  (error.data?.error_type === 'server_error' ||
@@ -26,18 +74,12 @@ class AccountRepository extends Repository {
26
74
  }
27
75
 
28
76
  /**
29
- * Helper: try to extract an IGT bearer token from various sources:
30
- * - response.headers (IG-Set-Authorization or ig-set-authorization)
31
- * - response.body top-level fields
32
- * - body.layout (stringified layout)
33
- * - any embedded login_response.headers JSON inside layout or body
34
- *
35
- * When found, sets this.client.state.authorization and calls updateAuthorization().
77
+ * Helper: extracts Bearer IGT token from headers / body / layout.
78
+ * When found, saves to state and calls updateAuthorization().
36
79
  */
37
80
  _extractAndSaveAuthorization(response) {
38
81
  if (!response) return null;
39
82
 
40
- // Normalize headers (case-insensitive)
41
83
  const headers = {};
42
84
  if (response.headers && typeof response.headers === 'object') {
43
85
  for (const k of Object.keys(response.headers)) {
@@ -45,7 +87,7 @@ class AccountRepository extends Repository {
45
87
  }
46
88
  }
47
89
 
48
- // 1) Try straight headers for IG-Set-Authorization
90
+ // 1) Header IG-Set-Authorization
49
91
  for (const key of Object.keys(headers)) {
50
92
  const val = headers[key];
51
93
  if (!val) continue;
@@ -54,9 +96,7 @@ class AccountRepository extends Repository {
54
96
  const token = this._normalizeTokenString(val);
55
97
  if (token) {
56
98
  this.client.state.authorization = token;
57
- try {
58
- this.client.state.updateAuthorization();
59
- } catch (e) {}
99
+ try { this.client.state.updateAuthorization(); } catch (e) {}
60
100
  return token;
61
101
  }
62
102
  }
@@ -64,15 +104,13 @@ class AccountRepository extends Repository {
64
104
  const token = this._findBearerInString(val);
65
105
  if (token) {
66
106
  this.client.state.authorization = token;
67
- try {
68
- this.client.state.updateAuthorization();
69
- } catch (e) {}
107
+ try { this.client.state.updateAuthorization(); } catch (e) {}
70
108
  return token;
71
109
  }
72
110
  }
73
111
  }
74
112
 
75
- // Extract IG-U headers (user id, region, claim) when present
113
+ // IG-U headers
76
114
  const dsUserIdHeader =
77
115
  headers['ig-set-ig-u-ds-user-id'] ||
78
116
  headers['ig-u-ds-user-id'] ||
@@ -80,9 +118,8 @@ class AccountRepository extends Repository {
80
118
  if (dsUserIdHeader) {
81
119
  const m = String(dsUserIdHeader).match(/(\d{3,})/);
82
120
  if (m) {
83
- const uid = m[1];
84
- this.client.state.cookieUserId = uid;
85
- this.client.state._userId = uid;
121
+ this.client.state.cookieUserId = m[1];
122
+ this.client.state._userId = m[1];
86
123
  }
87
124
  }
88
125
 
@@ -92,9 +129,7 @@ class AccountRepository extends Repository {
92
129
  headers['x-ig-set-ig-u-rur'];
93
130
  if (rurHeader) {
94
131
  const rur = String(rurHeader).trim().replace(/^"|"$/g, '');
95
- if (rur) {
96
- this.client.state.igURur = rur;
97
- }
132
+ if (rur) this.client.state.igURur = rur;
98
133
  }
99
134
 
100
135
  const wwwClaimHeader =
@@ -103,9 +138,7 @@ class AccountRepository extends Repository {
103
138
  headers['ig-u-www-claim'];
104
139
  if (wwwClaimHeader) {
105
140
  const claim = String(wwwClaimHeader).trim().replace(/^"|"$/g, '');
106
- if (claim) {
107
- this.client.state.igWWWClaim = claim;
108
- }
141
+ if (claim) this.client.state.igWWWClaim = claim;
109
142
  }
110
143
 
111
144
  const midHeader =
@@ -114,21 +147,17 @@ class AccountRepository extends Repository {
114
147
  headers['ig-set-mid'];
115
148
  if (midHeader) {
116
149
  const mid = String(midHeader).trim().replace(/^"|"$/g, '');
117
- if (mid) {
118
- this.client.state.mid = mid;
119
- }
150
+ if (mid) this.client.state.mid = mid;
120
151
  }
121
152
 
122
- // 2) Check response.body for common fields / embedded layout/login_response
153
+ // 2) body
123
154
  const body = response.body;
124
155
  if (body) {
125
156
  if (body.headers && typeof body.headers === 'string') {
126
157
  const token = this._findBearerInString(body.headers);
127
158
  if (token) {
128
159
  this.client.state.authorization = token;
129
- try {
130
- this.client.state.updateAuthorization();
131
- } catch (e) {}
160
+ try { this.client.state.updateAuthorization(); } catch (e) {}
132
161
  return token;
133
162
  }
134
163
  }
@@ -139,8 +168,6 @@ class AccountRepository extends Repository {
139
168
  layoutStr = typeof body.layout === 'string'
140
169
  ? body.layout
141
170
  : JSON.stringify(body.layout);
142
- } else if (body?.layout?.bloks_payload) {
143
- layoutStr = JSON.stringify(body.layout.bloks_payload);
144
171
  } else if (body?.bloks_payload) {
145
172
  layoutStr = JSON.stringify(body.bloks_payload);
146
173
  } else if (typeof body === 'string') {
@@ -151,9 +178,7 @@ class AccountRepository extends Repository {
151
178
  const token = this._findBearerInString(layoutStr);
152
179
  if (token) {
153
180
  this.client.state.authorization = token;
154
- try {
155
- this.client.state.updateAuthorization();
156
- } catch (e) {}
181
+ try { this.client.state.updateAuthorization(); } catch (e) {}
157
182
  return token;
158
183
  }
159
184
 
@@ -172,45 +197,31 @@ class AccountRepository extends Repository {
172
197
  const t = this._findBearerInString(parsed.headers);
173
198
  if (t) {
174
199
  this.client.state.authorization = t;
175
- try {
176
- this.client.state.updateAuthorization();
177
- } catch (e) {}
200
+ try { this.client.state.updateAuthorization(); } catch (e) {}
178
201
  return t;
179
202
  }
180
203
  }
181
204
  } catch (e) {}
182
205
  }
183
206
 
184
- // Encryption key id + pub key in layout
207
+ // Encryption keys
185
208
  const encKeyIdMatch =
186
- layoutStr.match(
187
- /IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i
188
- ) ||
189
- layoutStr.match(
190
- /password.encryption.key.id[\\"\s:]+(\d+)/i
191
- );
209
+ layoutStr.match(/IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i) ||
210
+ layoutStr.match(/password.encryption.key.id[\\"\s:]+(\d+)/i);
192
211
  if (encKeyIdMatch) {
193
- this.client.state.passwordEncryptionKeyId =
194
- parseInt(encKeyIdMatch[1]);
212
+ this.client.state.passwordEncryptionKeyId = parseInt(encKeyIdMatch[1]);
195
213
  }
196
214
 
197
215
  const encPubKeyMatch =
198
- layoutStr.match(
199
- /IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i
200
- ) ||
201
- layoutStr.match(
202
- /password.encryption.pub.key[\\"\s:]+([A-Za-z0-9+\/=]+)/i
203
- );
216
+ layoutStr.match(/IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i) ||
217
+ layoutStr.match(/password.encryption.pub.key[\\"\s:]+([A-Za-z0-9+\/=]+)/i);
204
218
  if (encPubKeyMatch) {
205
- this.client.state.passwordEncryptionPubKey =
206
- encPubKeyMatch[1];
219
+ this.client.state.passwordEncryptionPubKey = encPubKeyMatch[1];
207
220
  }
208
221
 
209
- // User / pk id
222
+ // User ID
210
223
  const dsIdMatch =
211
- layoutStr.match(
212
- /ig-set-ig-u-ds-user-id[\\"\s:]+(\d+)/i
213
- ) ||
224
+ layoutStr.match(/ig-set-ig-u-ds-user-id[\\"\s:]+(\d+)/i) ||
214
225
  layoutStr.match(/ig-u-ds-user-id[\\"\s:]+(\d+)/i) ||
215
226
  layoutStr.match(/"strong_id__"\s*:\s*"(\d+)"/i) ||
216
227
  layoutStr.match(/"pk_id"\s*:\s*"(\d+)"/i) ||
@@ -222,27 +233,15 @@ class AccountRepository extends Repository {
222
233
  }
223
234
 
224
235
  const rurMatch =
225
- layoutStr.match(
226
- /ig-set-ig-u-rur[\\"\s:]+"([^"\\]+)/i
227
- ) ||
236
+ layoutStr.match(/ig-set-ig-u-rur[\\"\s:]+"([^"\\]+)/i) ||
228
237
  layoutStr.match(/ig-u-rur[\\"\s:]+"([^"\\]+)/i);
229
- if (rurMatch) {
230
- this.client.state.igURur = rurMatch[1];
231
- }
238
+ if (rurMatch) this.client.state.igURur = rurMatch[1];
232
239
 
233
- const wwwClaimMatch = layoutStr.match(
234
- /x-ig-set-www-claim[\\"\s:]+"([^"\\]+)/i
235
- );
236
- if (wwwClaimMatch) {
237
- this.client.state.igWWWClaim = wwwClaimMatch[1];
238
- }
240
+ const wwwClaimMatch = layoutStr.match(/x-ig-set-www-claim[\\"\s:]+"([^"\\]+)/i);
241
+ if (wwwClaimMatch) this.client.state.igWWWClaim = wwwClaimMatch[1];
239
242
 
240
- const midMatch = layoutStr.match(
241
- /"mid"\s*:\s*"([^"\\]+)"/i
242
- );
243
- if (midMatch) {
244
- this.client.state.mid = midMatch[1];
245
- }
243
+ const midMatch = layoutStr.match(/"mid"\s*:\s*"([^"\\]+)"/i);
244
+ if (midMatch) this.client.state.mid = midMatch[1];
246
245
  }
247
246
  } catch (e) {}
248
247
 
@@ -256,19 +255,14 @@ class AccountRepository extends Repository {
256
255
 
257
256
  _normalizeTokenString(val) {
258
257
  if (!val || typeof val !== 'string') return null;
259
-
260
258
  const bearer = this._findBearerInString(val);
261
259
  if (bearer) return bearer;
262
-
263
260
  try {
264
261
  const maybeJson = JSON.parse(val);
265
262
  if (maybeJson['IG-Set-Authorization']) {
266
- return this._findBearerInString(
267
- maybeJson['IG-Set-Authorization']
268
- );
263
+ return this._findBearerInString(maybeJson['IG-Set-Authorization']);
269
264
  }
270
265
  } catch (e) {}
271
-
272
266
  return val.trim();
273
267
  }
274
268
 
@@ -297,13 +291,13 @@ class AccountRepository extends Repository {
297
291
  return m[0].replace(/\?"/g, '').trim();
298
292
  }
299
293
  }
300
-
301
294
  return null;
302
295
  }
303
296
 
304
297
  /**
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.
298
+ * Resolves a valid User-Agent from state / realistic fallback.
299
+ * IMPORTANT: IG version, build number and bloksVersionId must be
300
+ * in sync. Update all three when Instagram updates.
307
301
  */
308
302
  _resolveUserAgent() {
309
303
  const state = this.client.state || {};
@@ -319,27 +313,20 @@ class AccountRepository extends Repository {
319
313
  let userAgent = candidates.length > 0 ? candidates[0] : null;
320
314
 
321
315
  if (!userAgent) {
322
- // Fallback Pixel 9 Pro style UA (nu mai e Xiaomi)
316
+ // FIX: real existing version 415.0.0.36.76 (build 580610226)
317
+ // Keep in sync with bloksVersionId below.
323
318
  userAgent =
324
- 'Instagram 371.0.0.0.23 Android (35/15; 505dpi; 1440x3120; google; Pixel 9 Pro; pixel9pro; qcom; ro_RO; 703217507)';
319
+ 'Instagram 415.0.0.36.76 Android (35/15; 480dpi; 1280x2856; Google; Pixel 9 Pro; caiman; google; en_US; 580610226)';
325
320
  }
326
321
 
327
- // Sincronizăm în state ca să fie disponibil peste tot
328
322
  this.client.state.userAgent = userAgent;
329
323
  return userAgent;
330
324
  }
331
325
 
332
- // Ensure we have a valid csrftoken cookie (and mid) before sensitive endpoints.
333
326
  async ensureCsrfToken() {
334
327
  const cookieToken = this.client.state.cookieCsrfToken;
335
- if (
336
- cookieToken &&
337
- cookieToken !== 'missing' &&
338
- cookieToken !== 'pending'
339
- ) {
340
- try {
341
- this.client.state.csrfToken = cookieToken;
342
- } catch {}
328
+ if (cookieToken && cookieToken !== 'missing' && cookieToken !== 'pending') {
329
+ try { this.client.state.csrfToken = cookieToken; } catch {}
343
330
  return cookieToken;
344
331
  }
345
332
 
@@ -351,24 +338,13 @@ class AccountRepository extends Repository {
351
338
  } catch (e) {}
352
339
 
353
340
  const token = this.client.state.cookieCsrfToken;
354
- if (
355
- token &&
356
- token !== 'missing' &&
357
- token !== 'pending'
358
- ) {
359
- try {
360
- this.client.state.csrfToken = token;
361
- } catch {}
341
+ if (token && token !== 'missing' && token !== 'pending') {
342
+ try { this.client.state.csrfToken = token; } catch {}
362
343
  return token;
363
344
  }
364
-
365
345
  return null;
366
346
  }
367
347
 
368
- /**
369
- * Internal: pre-login Bloks OAuth token fetch, imită exact apelul oficial
370
- * com.bloks.www.caa.login.oauth.token.fetch.async
371
- */
372
348
  async _prefetchOauthTokenForLogin(username) {
373
349
  if (!username) return null;
374
350
 
@@ -383,16 +359,12 @@ class AccountRepository extends Repository {
383
359
  const familyDeviceId =
384
360
  this.client.state.phoneId ||
385
361
  this.client.state.familyDeviceId ||
386
- (crypto.randomUUID
387
- ? crypto.randomUUID()
388
- : require('uuid').v4());
362
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
389
363
 
390
364
  const qeDeviceId =
391
365
  this.client.state.deviceId ||
392
366
  this.client.state.qeDeviceId ||
393
- (crypto.randomUUID
394
- ? crypto.randomUUID()
395
- : require('uuid').v4());
367
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
396
368
 
397
369
  const waterfallId = crypto.randomUUID
398
370
  ? crypto.randomUUID()
@@ -403,23 +375,15 @@ class AccountRepository extends Repository {
403
375
  `${Date.now()}${Math.floor(Math.random() * 1000)}`
404
376
  );
405
377
 
406
- const aacInitTimestamp =
407
- nowSec - Math.floor(Math.random() * 50);
378
+ const aacInitTimestamp = nowSec - Math.floor(Math.random() * 50);
408
379
  const aacjid = crypto.randomUUID
409
380
  ? crypto.randomUUID()
410
381
  : require('uuid').v4();
411
- const aaccs = crypto
412
- .randomBytes(32)
413
- .toString('base64')
414
- .replace(/=/g, '');
382
+ const aaccs = crypto.randomBytes(32).toString('base64').replace(/=/g, '');
415
383
 
416
384
  const clientInputParams = {
417
385
  username_input: username,
418
- aac: JSON.stringify({
419
- aac_init_timestamp: aacInitTimestamp,
420
- aacjid,
421
- aaccs,
422
- }),
386
+ aac: JSON.stringify({ aac_init_timestamp: aacInitTimestamp, aacjid, aaccs }),
423
387
  lois_settings: { lois_token: '' },
424
388
  cloud_trust_token: null,
425
389
  zero_balance_state: '',
@@ -447,6 +411,7 @@ class AccountRepository extends Repository {
447
411
  server_params: serverParams,
448
412
  });
449
413
 
414
+ // FIX: bloksVersionId kept in sync with IG version from _resolveUserAgent
450
415
  const bloksVersionId =
451
416
  this.client.state.bloksVersionId ||
452
417
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
@@ -459,19 +424,19 @@ class AccountRepository extends Repository {
459
424
  const lang = this.client.state.language || 'ro_RO';
460
425
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
461
426
  const userAgent = this._resolveUserAgent();
462
-
463
427
  const timezoneOffset =
464
428
  typeof this.client.state.timezoneOffset === 'number'
465
429
  ? this.client.state.timezoneOffset
466
430
  : 7200;
467
431
 
432
+ // FIX: dynamic network properties
433
+ const networkProps = buildNetworkProperties();
434
+
468
435
  const bloksHeaders = {
469
436
  'accept-language': acceptLanguage,
470
- 'content-type':
471
- 'application/x-www-form-urlencoded; charset=UTF-8',
437
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
472
438
  'ig-intended-user-id': '0',
473
439
  priority: 'u=3',
474
-
475
440
  'x-bloks-is-layout-rtl': 'false',
476
441
  'x-bloks-prism-ax-base-colors-enabled': 'false',
477
442
  'x-bloks-prism-button-version': 'CONTROL',
@@ -479,53 +444,35 @@ class AccountRepository extends Repository {
479
444
  'x-bloks-prism-font-enabled': 'false',
480
445
  'x-bloks-prism-indigo-link-version': '0',
481
446
  'x-bloks-version-id': bloksVersionId,
482
-
483
447
  'x-ig-android-id': androidDeviceId,
484
448
  'x-ig-device-id': qeDeviceId,
485
449
  'x-ig-family-device-id': familyDeviceId,
486
450
  'x-ig-timezone-offset': String(timezoneOffset),
487
- 'x-ig-app-id': String(
488
- this.client.state.fbAnalyticsApplicationId ||
489
- '567067343352427'
490
- ),
451
+ 'x-ig-app-id': String(this.client.state.fbAnalyticsApplicationId || '567067343352427'),
491
452
  'x-ig-app-locale': lang,
492
453
  'x-ig-device-locale': lang,
493
454
  'x-ig-mapped-locale': lang,
494
-
495
- 'x-ig-client-endpoint':
496
- 'com.bloks.www.caa.login.login_homepage',
455
+ 'x-ig-client-endpoint': 'com.bloks.www.caa.login.login_homepage',
497
456
  'x-ig-nav-chain':
498
457
  'com.bloks.www.caa.login.login_homepage:com.bloks.www.caa.login.login_homepage:1:button:0:::0',
499
-
500
458
  'x-ig-connection-type': 'WIFI',
501
459
  'x-ig-capabilities': '3brTv10=',
502
460
  'x-ig-www-claim': this.client.state.igWWWClaim || '0',
503
-
504
- 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(
505
- Math.random() * 1000
506
- )
507
- .toString()
508
- .padStart(3, '0')}`,
461
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
509
462
  'x-pigeon-session-id':
510
463
  this.client.state.pigeonSessionId ||
511
464
  `UFS-${crypto.randomBytes(16).toString('hex')}-2`,
512
-
513
465
  'x-mid':
514
466
  this.client.state.mid ||
515
467
  `aZ${crypto.randomBytes(8).toString('hex')}`,
516
468
  'x-tigon-is-retry': 'False',
517
-
518
469
  'x-fb-client-ip': 'True',
519
470
  'x-fb-connection-type': 'WIFI',
520
471
  'x-fb-server-cluster': 'True',
521
- 'x-fb-network-properties':
522
- 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
472
+ 'x-fb-network-properties': networkProps, // FIX: dynamic
523
473
  'x-fb-request-analytics-tags': JSON.stringify({
524
474
  network_tags: {
525
- product: String(
526
- this.client.state.fbAnalyticsApplicationId ||
527
- '567067343352427'
528
- ),
475
+ product: String(this.client.state.fbAnalyticsApplicationId || '567067343352427'),
529
476
  purpose: 'fetch',
530
477
  surface: 'undefined',
531
478
  request_category: 'api',
@@ -536,7 +483,6 @@ class AccountRepository extends Repository {
536
483
  'IgApi: bloks/async_action/com.bloks.www.caa.login.oauth.token.fetch.async/',
537
484
  'x-fb-http-engine': 'MNS/TCP',
538
485
  'x-fb-rmd': 'state=URL_ELIGIBLE',
539
-
540
486
  'user-agent': userAgent,
541
487
  };
542
488
 
@@ -551,34 +497,12 @@ class AccountRepository extends Repository {
551
497
  headers: bloksHeaders,
552
498
  });
553
499
 
554
- try {
555
- const fs = require('fs');
556
- const path = require('path');
557
- const debugDir = path.join(
558
- process.cwd(),
559
- 'authinfo_instagram'
560
- );
561
- const debugFile = path.join(
562
- debugDir,
563
- 'oauth-token-debug.json'
564
- );
565
- try {
566
- fs.mkdirSync(debugDir, { recursive: true });
567
- } catch (e) {}
568
-
569
- const debugPayload = {
570
- at: new Date().toISOString(),
571
- statusCode:
572
- response.statusCode || response.status || null,
573
- headers: response.headers || null,
574
- body: response.body || null,
575
- };
576
- fs.writeFileSync(
577
- debugFile,
578
- JSON.stringify(debugPayload, null, 2),
579
- 'utf8'
580
- );
581
- } catch (e) {}
500
+ debugWrite('oauth-token-debug.json', {
501
+ at: new Date().toISOString(),
502
+ statusCode: response.statusCode || response.status || null,
503
+ headers: response.headers || null,
504
+ body: response.body || null,
505
+ });
582
506
 
583
507
  return response.body;
584
508
  } catch (e) {
@@ -586,10 +510,6 @@ class AccountRepository extends Repository {
586
510
  }
587
511
  }
588
512
 
589
- /**
590
- * Step 1: launcher/mobileconfig to fetch mobile config + encryption keys.
591
- * Best effort – errors are ignored.
592
- */
593
513
  async _launcherMobileConfig(preLogin = true) {
594
514
  const nowSec = Math.floor(Date.now() / 1000);
595
515
  const state = this.client.state || {};
@@ -602,16 +522,12 @@ class AccountRepository extends Repository {
602
522
  const familyDeviceId =
603
523
  state.phoneId ||
604
524
  state.familyDeviceId ||
605
- (crypto.randomUUID
606
- ? crypto.randomUUID()
607
- : require('uuid').v4());
525
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
608
526
 
609
527
  const qeDeviceId =
610
528
  state.deviceId ||
611
529
  state.qeDeviceId ||
612
- (crypto.randomUUID
613
- ? crypto.randomUUID()
614
- : require('uuid').v4());
530
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
615
531
 
616
532
  const deviceUUID = state.uuid || qeDeviceId;
617
533
  const userId = state.cookieUserId || '0';
@@ -623,11 +539,12 @@ class AccountRepository extends Repository {
623
539
  const lang = state.language || 'ro_RO';
624
540
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
625
541
  const userAgent = this._resolveUserAgent();
626
-
627
542
  const timezoneOffset =
628
- typeof state.timezoneOffset === 'number'
629
- ? state.timezoneOffset
630
- : 7200;
543
+ typeof state.timezoneOffset === 'number' ? state.timezoneOffset : 7200;
544
+
545
+ // FIX: salt-ids from state or fallback
546
+ const saltIds = state.igSaltIds || '220140399,332020310,974466465,974460658';
547
+ const networkProps = buildNetworkProperties();
631
548
 
632
549
  const params = {
633
550
  bool_opt_policy: '0',
@@ -636,8 +553,7 @@ class AccountRepository extends Repository {
636
553
  client_context: '["opt,value_hash"]',
637
554
  unit_type: '2',
638
555
  use_case: 'STANDARD',
639
- query_hash:
640
- 'f00b9d0869db3969378d8d06bfccb24b5ef078012c8e199cba961cd5dfedaa88',
556
+ query_hash: 'f00b9d0869db3969378d8d06bfccb24b5ef078012c8e199cba961cd5dfedaa88',
641
557
  ts: String(nowSec),
642
558
  _uid: userId,
643
559
  device_id: deviceUUID,
@@ -648,10 +564,8 @@ class AccountRepository extends Repository {
648
564
 
649
565
  const headers = {
650
566
  'accept-language': acceptLanguage,
651
- 'content-type':
652
- 'application/x-www-form-urlencoded; charset=UTF-8',
567
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
653
568
  priority: 'u=3',
654
-
655
569
  'x-bloks-is-layout-rtl': 'false',
656
570
  'x-bloks-prism-ax-base-colors-enabled': 'false',
657
571
  'x-bloks-prism-button-version': 'CONTROL',
@@ -659,28 +573,19 @@ class AccountRepository extends Repository {
659
573
  'x-bloks-prism-font-enabled': 'false',
660
574
  'x-bloks-prism-indigo-link-version': '0',
661
575
  'x-bloks-version-id': bloksVersionId,
662
-
663
576
  'x-fb-client-ip': 'True',
664
577
  'x-fb-connection-type': 'WIFI',
665
578
  'x-fb-server-cluster': 'True',
666
- 'x-fb-network-properties':
667
- 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
579
+ 'x-fb-network-properties': networkProps, // FIX: dynamic
668
580
  'x-fb-http-engine': 'MNS/TCP',
669
581
  'x-fb-rmd': 'state=URL_ELIGIBLE',
670
-
671
582
  'x-ig-android-id': androidDeviceId,
672
- 'x-ig-app-id': String(
673
- state.fbAnalyticsApplicationId || '567067343352427'
674
- ),
583
+ 'x-ig-app-id': String(state.fbAnalyticsApplicationId || '567067343352427'),
675
584
  'x-ig-app-locale': lang,
676
- 'x-ig-bandwidth-speed-kbps': (
677
- Math.random() * 1500 +
678
- 800
679
- ).toFixed(3),
585
+ 'x-ig-bandwidth-speed-kbps': (Math.random() * 1500 + 800).toFixed(3),
680
586
  'x-ig-bandwidth-totalbytes-b': '0',
681
587
  'x-ig-bandwidth-totaltime-ms': '0',
682
- 'x-ig-client-endpoint':
683
- 'LockoutFragment:dogfooding_lockout',
588
+ 'x-ig-client-endpoint': 'LockoutFragment:dogfooding_lockout',
684
589
  'x-ig-capabilities': '3brTv10=',
685
590
  'x-ig-connection-type': 'WIFI',
686
591
  'x-ig-device-id': deviceUUID,
@@ -688,26 +593,19 @@ class AccountRepository extends Repository {
688
593
  'x-ig-device-locale': lang,
689
594
  'x-ig-family-device-id': familyDeviceId,
690
595
  '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',
596
+ 'x-ig-nav-chain': 'LockoutFragment:dogfooding_lockout:1:cold_start',
597
+ 'x-ig-salt-ids': saltIds, // FIX: from state or fallback
695
598
  'x-ig-timezone-offset': String(timezoneOffset),
696
599
  'x-ig-www-claim': state.igWWWClaim || '0',
697
600
  'x-mid':
698
601
  state.mid ||
699
602
  state.machineId ||
700
603
  `aZ${crypto.randomBytes(8).toString('hex')}`,
701
- 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(
702
- Math.random() * 1000
703
- )
704
- .toString()
705
- .padStart(3, '0')}`,
604
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
706
605
  'x-pigeon-session-id':
707
606
  state.pigeonSessionId ||
708
607
  `UFS-${crypto.randomBytes(16).toString('hex')}-M`,
709
608
  'x-tigon-is-retry': 'False',
710
-
711
609
  'accept-encoding': 'gzip, deflate, br',
712
610
  'user-agent': userAgent,
713
611
  };
@@ -716,12 +614,8 @@ class AccountRepository extends Repository {
716
614
  headers['ig-intended-user-id'] = String(state.cookieUserId);
717
615
  headers['ig-u-ds-user-id'] = String(state.cookieUserId);
718
616
  }
719
- if (state.igURur) {
720
- headers['ig-u-rur'] = state.igURur;
721
- }
722
- if (state.authorization) {
723
- headers['authorization'] = state.authorization;
724
- }
617
+ if (state.igURur) headers['ig-u-rur'] = state.igURur;
618
+ if (state.authorization) headers['authorization'] = state.authorization;
725
619
 
726
620
  try {
727
621
  const response = await this.client.request.send({
@@ -731,7 +625,6 @@ class AccountRepository extends Repository {
731
625
  headers,
732
626
  });
733
627
 
734
- // NEW: salvăm cheia de criptare a parolei direct din headers de la mobileconfig
735
628
  try {
736
629
  const mcHeaders = response.headers || {};
737
630
  const mcLower = {};
@@ -739,10 +632,8 @@ class AccountRepository extends Repository {
739
632
  mcLower[k.toLowerCase()] = mcHeaders[k];
740
633
  }
741
634
 
742
- const mcKeyIdStr =
743
- mcLower['ig-set-password-encryption-key-id'];
744
- const mcPubKey =
745
- mcLower['ig-set-password-encryption-pub-key'];
635
+ const mcKeyIdStr = mcLower['ig-set-password-encryption-key-id'];
636
+ const mcPubKey = mcLower['ig-set-password-encryption-pub-key'];
746
637
 
747
638
  if (mcKeyIdStr) {
748
639
  const parsedKeyId = parseInt(mcKeyIdStr, 10);
@@ -750,39 +641,15 @@ class AccountRepository extends Repository {
750
641
  state.passwordEncryptionKeyId = parsedKeyId;
751
642
  }
752
643
  }
753
-
754
- if (mcPubKey) {
755
- state.passwordEncryptionPubKey = mcPubKey;
756
- }
644
+ if (mcPubKey) state.passwordEncryptionPubKey = mcPubKey;
757
645
  } catch (e) {}
758
646
 
759
- try {
760
- const fs = require('fs');
761
- const path = require('path');
762
- const debugDir = path.join(
763
- process.cwd(),
764
- 'authinfo_instagram'
765
- );
766
- const debugFile = path.join(
767
- debugDir,
768
- 'launcher-mobileconfig-debug.json'
769
- );
770
- try {
771
- fs.mkdirSync(debugDir, { recursive: true });
772
- } catch (e) {}
773
- const debugPayload = {
774
- at: new Date().toISOString(),
775
- statusCode:
776
- response.statusCode || response.status || null,
777
- headers: response.headers || null,
778
- body: response.body || null,
779
- };
780
- fs.writeFileSync(
781
- debugFile,
782
- JSON.stringify(debugPayload, null, 2),
783
- 'utf8'
784
- );
785
- } catch (e) {}
647
+ debugWrite('launcher-mobileconfig-debug.json', {
648
+ at: new Date().toISOString(),
649
+ statusCode: response.statusCode || response.status || null,
650
+ headers: response.headers || null,
651
+ body: response.body || null,
652
+ });
786
653
 
787
654
  return response.body;
788
655
  } catch (e) {
@@ -790,10 +657,6 @@ class AccountRepository extends Repository {
790
657
  }
791
658
  }
792
659
 
793
- /**
794
- * Step 2: Android Keystore attestation – moves x-ig-attest-params
795
- * payload to dedicated endpoint /api/v1/attestation/create_android_keystore/.
796
- */
797
660
  async _createAndroidKeystoreAttestation() {
798
661
  const state = this.client.state || {};
799
662
  const nowSec = Math.floor(Date.now() / 1000);
@@ -806,30 +669,25 @@ class AccountRepository extends Repository {
806
669
  const familyDeviceId =
807
670
  state.phoneId ||
808
671
  state.familyDeviceId ||
809
- (crypto.randomUUID
810
- ? crypto.randomUUID()
811
- : require('uuid').v4());
672
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
812
673
 
813
674
  const qeDeviceId =
814
675
  state.deviceId ||
815
676
  state.qeDeviceId ||
816
- (crypto.randomUUID
817
- ? crypto.randomUUID()
818
- : require('uuid').v4());
677
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
819
678
 
820
679
  const deviceUUID = state.uuid || qeDeviceId;
821
680
  const userId = state.cookieUserId || '0';
822
681
 
823
- const attestParams =
824
- AccountRepository.generateAttestParams(state);
682
+ // FIX: each cert in the chain has its own key pair
683
+ const attestParams = AccountRepository.generateAttestParams(state);
825
684
 
826
685
  const lang = state.language || 'ro_RO';
827
686
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
828
687
  const userAgent = this._resolveUserAgent();
829
688
  const timezoneOffset =
830
- typeof state.timezoneOffset === 'number'
831
- ? state.timezoneOffset
832
- : 7200;
689
+ typeof state.timezoneOffset === 'number' ? state.timezoneOffset : 7200;
690
+ const networkProps = buildNetworkProperties();
833
691
 
834
692
  const params = {
835
693
  _uid: userId,
@@ -842,29 +700,20 @@ class AccountRepository extends Repository {
842
700
 
843
701
  const headers = {
844
702
  'accept-language': acceptLanguage,
845
- 'content-type':
846
- 'application/x-www-form-urlencoded; charset=UTF-8',
703
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
847
704
  priority: 'u=3',
848
-
849
705
  'x-ig-android-id': androidDeviceId,
850
706
  'x-ig-device-id': qeDeviceId,
851
707
  'x-ig-family-device-id': familyDeviceId,
852
708
  'x-ig-timezone-offset': String(timezoneOffset),
853
- 'x-ig-app-id': String(
854
- state.fbAnalyticsApplicationId || '567067343352427'
855
- ),
709
+ 'x-ig-app-id': String(state.fbAnalyticsApplicationId || '567067343352427'),
856
710
  'x-ig-app-locale': lang,
857
711
  'x-ig-device-locale': lang,
858
712
  'x-ig-mapped-locale': lang,
859
713
  'x-ig-connection-type': 'WIFI',
860
714
  'x-ig-capabilities': '3brTv10=',
861
715
  'x-ig-www-claim': state.igWWWClaim || '0',
862
-
863
- 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(
864
- Math.random() * 1000
865
- )
866
- .toString()
867
- .padStart(3, '0')}`,
716
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
868
717
  'x-pigeon-session-id':
869
718
  state.pigeonSessionId ||
870
719
  `UFS-${crypto.randomBytes(16).toString('hex')}-A`,
@@ -872,28 +721,19 @@ class AccountRepository extends Repository {
872
721
  state.mid ||
873
722
  state.machineId ||
874
723
  `aZ${crypto.randomBytes(8).toString('hex')}`,
875
-
876
724
  'x-fb-client-ip': 'True',
877
725
  'x-fb-connection-type': 'WIFI',
878
726
  'x-fb-server-cluster': 'True',
879
727
  'x-fb-http-engine': 'MNS/TCP',
880
- 'x-fb-network-properties':
881
- 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
728
+ 'x-fb-network-properties': networkProps, // FIX: dynamic
882
729
  'x-fb-rmd': 'state=URL_ELIGIBLE',
883
-
884
730
  'accept-encoding': 'gzip, deflate, br',
885
731
  'user-agent': userAgent,
886
732
  };
887
733
 
888
- if (state.cookieUserId) {
889
- headers['ig-intended-user-id'] = String(state.cookieUserId);
890
- }
891
- if (state.authorization) {
892
- headers['authorization'] = state.authorization;
893
- }
894
- if (state.igURur) {
895
- headers['ig-u-rur'] = state.igURur;
896
- }
734
+ if (state.cookieUserId) headers['ig-intended-user-id'] = String(state.cookieUserId);
735
+ if (state.authorization) headers['authorization'] = state.authorization;
736
+ if (state.igURur) headers['ig-u-rur'] = state.igURur;
897
737
 
898
738
  try {
899
739
  const response = await this.client.request.send({
@@ -903,33 +743,12 @@ class AccountRepository extends Repository {
903
743
  headers,
904
744
  });
905
745
 
906
- try {
907
- const fs = require('fs');
908
- const path = require('path');
909
- const debugDir = path.join(
910
- process.cwd(),
911
- 'authinfo_instagram'
912
- );
913
- const debugFile = path.join(
914
- debugDir,
915
- 'attestation-debug.json'
916
- );
917
- try {
918
- fs.mkdirSync(debugDir, { recursive: true });
919
- } catch (e) {}
920
- const debugPayload = {
921
- at: new Date().toISOString(),
922
- statusCode:
923
- response.statusCode || response.status || null,
924
- headers: response.headers || null,
925
- body: response.body || null,
926
- };
927
- fs.writeFileSync(
928
- debugFile,
929
- JSON.stringify(debugPayload, null, 2),
930
- 'utf8'
931
- );
932
- } catch (e) {}
746
+ debugWrite('attestation-debug.json', {
747
+ at: new Date().toISOString(),
748
+ statusCode: response.statusCode || response.status || null,
749
+ headers: response.headers || null,
750
+ body: response.body || null,
751
+ });
933
752
 
934
753
  return response.body;
935
754
  } catch (e) {
@@ -937,11 +756,6 @@ class AccountRepository extends Repository {
937
756
  }
938
757
  }
939
758
 
940
- /**
941
- * Step 3 & 4 & 5 helpers: Terms of service preload, process client data,
942
- * and (optional) phone number prefill.
943
- * These are best-effort calls; failures are ignored.
944
- */
945
759
  async _preloadTermsOfService() {
946
760
  const state = this.client.state || {};
947
761
  const lang = state.language || 'ro_RO';
@@ -952,29 +766,19 @@ class AccountRepository extends Repository {
952
766
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
953
767
 
954
768
  const paramsJson = JSON.stringify({});
955
- const bkClientContext = JSON.stringify({
956
- bloks_version: bloksVersionId,
957
- styles_id: 'instagram',
958
- });
959
-
769
+ const bkClientContext = JSON.stringify({ bloks_version: bloksVersionId, styles_id: 'instagram' });
960
770
  const headers = {
961
771
  'accept-language': acceptLanguage,
962
- 'content-type':
963
- 'application/x-www-form-urlencoded; charset=UTF-8',
772
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
964
773
  priority: 'u=3',
965
774
  'x-bloks-version-id': bloksVersionId,
966
775
  'user-agent': userAgent,
967
776
  };
968
-
969
777
  try {
970
778
  const response = await this.client.request.send({
971
779
  method: 'POST',
972
780
  url: '/api/v1/bloks/apps/com.bloks.www.caa.login.oxygen_preloads_terms_of_service/',
973
- form: {
974
- params: paramsJson,
975
- bk_client_context: bkClientContext,
976
- bloks_versioning_id: bloksVersionId,
977
- },
781
+ form: { params: paramsJson, bk_client_context: bkClientContext, bloks_versioning_id: bloksVersionId },
978
782
  headers,
979
783
  });
980
784
  return response.body;
@@ -992,32 +796,20 @@ class AccountRepository extends Repository {
992
796
  state.bloksVersionId ||
993
797
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
994
798
 
995
- const paramsJson = JSON.stringify({
996
- username_input: username || '',
997
- });
998
- const bkClientContext = JSON.stringify({
999
- bloks_version: bloksVersionId,
1000
- styles_id: 'instagram',
1001
- });
1002
-
799
+ const paramsJson = JSON.stringify({ username_input: username || '' });
800
+ const bkClientContext = JSON.stringify({ bloks_version: bloksVersionId, styles_id: 'instagram' });
1003
801
  const headers = {
1004
802
  'accept-language': acceptLanguage,
1005
- 'content-type':
1006
- 'application/x-www-form-urlencoded; charset=UTF-8',
803
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
1007
804
  priority: 'u=3',
1008
805
  'x-bloks-version-id': bloksVersionId,
1009
806
  'user-agent': userAgent,
1010
807
  };
1011
-
1012
808
  try {
1013
809
  const response = await this.client.request.send({
1014
810
  method: 'POST',
1015
811
  url: '/api/v1/bloks/async_action/com.bloks.www.bloks.caa.login.process_client_data_and_redirect/',
1016
- form: {
1017
- params: paramsJson,
1018
- bk_client_context: bkClientContext,
1019
- bloks_versioning_id: bloksVersionId,
1020
- },
812
+ form: { params: paramsJson, bk_client_context: bkClientContext, bloks_versioning_id: bloksVersionId },
1021
813
  headers,
1022
814
  });
1023
815
  return response.body;
@@ -1036,29 +828,19 @@ class AccountRepository extends Repository {
1036
828
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
1037
829
 
1038
830
  const paramsJson = JSON.stringify({});
1039
- const bkClientContext = JSON.stringify({
1040
- bloks_version: bloksVersionId,
1041
- styles_id: 'instagram',
1042
- });
1043
-
831
+ const bkClientContext = JSON.stringify({ bloks_version: bloksVersionId, styles_id: 'instagram' });
1044
832
  const headers = {
1045
833
  'accept-language': acceptLanguage,
1046
- 'content-type':
1047
- 'application/x-www-form-urlencoded; charset=UTF-8',
834
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
1048
835
  priority: 'u=3',
1049
836
  'x-bloks-version-id': bloksVersionId,
1050
837
  'user-agent': userAgent,
1051
838
  };
1052
-
1053
839
  try {
1054
840
  const response = await this.client.request.send({
1055
841
  method: 'POST',
1056
842
  url: '/api/v1/bloks/async_action/com.bloks.www.bloks.caa.phone.number.prefill.async.controller/',
1057
- form: {
1058
- params: paramsJson,
1059
- bk_client_context: bkClientContext,
1060
- bloks_versioning_id: bloksVersionId,
1061
- },
843
+ form: { params: paramsJson, bk_client_context: bkClientContext, bloks_versioning_id: bloksVersionId },
1062
844
  headers,
1063
845
  });
1064
846
  return response.body;
@@ -1069,10 +851,7 @@ class AccountRepository extends Repository {
1069
851
 
1070
852
  async login(credentialsOrUsername, passwordArg) {
1071
853
  let username, password;
1072
- if (
1073
- typeof credentialsOrUsername === 'object' &&
1074
- credentialsOrUsername !== null
1075
- ) {
854
+ if (typeof credentialsOrUsername === 'object' && credentialsOrUsername !== null) {
1076
855
  username = credentialsOrUsername.username;
1077
856
  password = credentialsOrUsername.password;
1078
857
  } else {
@@ -1080,57 +859,37 @@ class AccountRepository extends Repository {
1080
859
  password = passwordArg;
1081
860
  }
1082
861
 
1083
- if (!username || !password) {
1084
- throw new Error('Username and password are required');
1085
- }
862
+ if (!username || !password) throw new Error('Username and password are required');
1086
863
 
1087
- // 0: ne asigurăm că avem csrf + cookies de bază
1088
864
  await this.ensureCsrfToken();
1089
865
 
1090
- // 1: încercăm luăm cheia de criptare a parolei din /launcher/mobileconfig/
1091
- try {
1092
- await this._launcherMobileConfig(true);
1093
- } catch (e) {}
866
+ try { await this._launcherMobileConfig(true); } catch (e) {}
1094
867
 
1095
- // Fallback: dacă mobileconfig nu a setat cheia, folosim mecanismul vechi de LOGIN_EXPERIMENTS
1096
868
  if (!this.client.state.passwordEncryptionPubKey) {
1097
869
  await this.syncLoginExperiments();
1098
870
  }
1099
871
 
1100
- // Acum parola este criptată CU cheia venită din mobileconfig (dacă a existat)
1101
- const { encrypted, time } = this.encryptPassword(password);
872
+ // FIX: if we still have no public key, throw error do not send password in plaintext
873
+ if (!this.client.state.passwordEncryptionPubKey) {
874
+ throw new Error(
875
+ 'Could not obtain the password encryption public key. ' +
876
+ 'Check that mobileconfig and syncLoginExperiments return valid data.'
877
+ );
878
+ }
1102
879
 
1103
- // Step 2–5: keystore attestation, TOS preload, process client data, phone prefill
1104
- try {
1105
- await this._createAndroidKeystoreAttestation();
1106
- } catch (e) {}
1107
- try {
1108
- await this._preloadTermsOfService();
1109
- } catch (e) {}
1110
- try {
1111
- await this._processClientDataAndRedirect(username);
1112
- } catch (e) {}
1113
- try {
1114
- await this._phoneNumberPrefill();
1115
- } catch (e) {}
880
+ const { encrypted, time } = this.encryptPassword(password);
1116
881
 
1117
- // Step 6: OAuth token fetch
1118
- try {
1119
- await this._prefetchOauthTokenForLogin(username);
1120
- } catch (e) {}
882
+ try { await this._createAndroidKeystoreAttestation(); } catch (e) {}
883
+ try { await this._preloadTermsOfService(); } catch (e) {}
884
+ try { await this._processClientDataAndRedirect(username); } catch (e) {}
885
+ try { await this._phoneNumberPrefill(); } catch (e) {}
886
+ try { await this._prefetchOauthTokenForLogin(username); } catch (e) {}
1121
887
 
1122
- // Step 7: real login
1123
888
  return this.requestWithRetry(async () => {
1124
889
  const nowSec = Math.floor(Date.now() / 1000);
1125
- const aacInitTimestamp =
1126
- nowSec - Math.floor(Math.random() * 50);
1127
- const aacjid = crypto.randomUUID
1128
- ? crypto.randomUUID()
1129
- : require('uuid').v4();
1130
- const aaccs = crypto
1131
- .randomBytes(32)
1132
- .toString('base64')
1133
- .replace(/=/g, '');
890
+ const aacInitTimestamp = nowSec - Math.floor(Math.random() * 50);
891
+ const aacjid = crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4();
892
+ const aaccs = crypto.randomBytes(32).toString('base64').replace(/=/g, '');
1134
893
 
1135
894
  const androidDeviceId =
1136
895
  this.client.state.androidDeviceId ||
@@ -1143,18 +902,13 @@ class AccountRepository extends Repository {
1143
902
  const familyDeviceId =
1144
903
  this.client.state.phoneId ||
1145
904
  this.client.state.familyDeviceId ||
1146
- (crypto.randomUUID
1147
- ? crypto.randomUUID()
1148
- : require('uuid').v4());
905
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
1149
906
  const qeDeviceId =
1150
907
  this.client.state.deviceId ||
1151
908
  this.client.state.qeDeviceId ||
1152
- (crypto.randomUUID
1153
- ? crypto.randomUUID()
1154
- : require('uuid').v4());
909
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
1155
910
 
1156
911
  const accountsList = this.client.state.accountsList || [];
1157
-
1158
912
  const flashCallPermissionStatus = {
1159
913
  READ_PHONE_STATE: 'GRANTED',
1160
914
  READ_CALL_LOG: 'GRANTED',
@@ -1162,11 +916,7 @@ class AccountRepository extends Repository {
1162
916
  };
1163
917
 
1164
918
  const clientInputParams = {
1165
- aac: JSON.stringify({
1166
- aac_init_timestamp: aacInitTimestamp,
1167
- aacjid,
1168
- aaccs,
1169
- }),
919
+ aac: JSON.stringify({ aac_init_timestamp: aacInitTimestamp, aacjid, aaccs }),
1170
920
  sim_phones: [],
1171
921
  aymh_accounts: [],
1172
922
  network_bssid: null,
@@ -1194,8 +944,7 @@ class AccountRepository extends Repository {
1194
944
  machine_id: machineId,
1195
945
  flash_call_permission_status: flashCallPermissionStatus,
1196
946
  accounts_list: accountsList,
1197
- gms_incoming_call_retriever_eligibility:
1198
- 'client_not_supported',
947
+ gms_incoming_call_retriever_eligibility: 'client_not_supported',
1199
948
  family_device_id: familyDeviceId,
1200
949
  fb_ig_device_id: [],
1201
950
  device_emails: [],
@@ -1207,13 +956,9 @@ class AccountRepository extends Repository {
1207
956
  contact_point: username,
1208
957
  };
1209
958
 
1210
- const waterfallId = crypto.randomUUID
1211
- ? crypto.randomUUID()
1212
- : require('uuid').v4();
959
+ const waterfallId = crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4();
1213
960
  const latencyMarkerId = 36707139;
1214
- const latencyInstanceId = Number(
1215
- `${Date.now()}${Math.floor(Math.random() * 1000)}`
1216
- );
961
+ const latencyInstanceId = Number(`${Date.now()}${Math.floor(Math.random() * 1000)}`);
1217
962
 
1218
963
  const serverParams = {
1219
964
  should_trigger_override_login_2fa_action: 0,
@@ -1228,8 +973,7 @@ class AccountRepository extends Repository {
1228
973
  is_platform_login: 0,
1229
974
  INTERNAL__latency_qpl_marker_id: latencyMarkerId,
1230
975
  is_from_aymh: 0,
1231
- offline_experiment_group:
1232
- 'caa_iteration_v3_perf_ig_4',
976
+ offline_experiment_group: 'caa_iteration_v3_perf_ig_4',
1233
977
  is_from_landing_page: 0,
1234
978
  left_nav_button_action: 'NONE',
1235
979
  password_text_input_id: 'z0jejq:194',
@@ -1240,10 +984,9 @@ class AccountRepository extends Repository {
1240
984
  username_text_input_id: 'z0jejq:193',
1241
985
  layered_homepage_experiment_group: null,
1242
986
  device_id: androidDeviceId,
1243
- login_surface: 'switcher',
987
+ login_surface: 'login_home',
1244
988
  INTERNAL__latency_qpl_instance_id: latencyInstanceId,
1245
- reg_flow_source:
1246
- 'login_home_native_integration_point',
989
+ reg_flow_source: 'aymh_multi_profiles_native_integration_point',
1247
990
  is_caa_perf_enabled: 1,
1248
991
  credential_type: 'password',
1249
992
  is_from_password_entry_page: 0,
@@ -1263,22 +1006,17 @@ class AccountRepository extends Repository {
1263
1006
  this.client.state.bloksVersionId ||
1264
1007
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
1265
1008
 
1266
- const bkClientContext = JSON.stringify({
1267
- bloks_version: bloksVersionId,
1268
- styles_id: 'instagram',
1269
- });
1270
-
1009
+ const bkClientContext = JSON.stringify({ bloks_version: bloksVersionId, styles_id: 'instagram' });
1271
1010
  const lang = this.client.state.language || 'ro_RO';
1272
1011
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
1273
1012
  const userAgent = this._resolveUserAgent();
1013
+ const networkProps = buildNetworkProperties();
1274
1014
 
1275
1015
  const bloksHeaders = {
1276
1016
  'accept-language': acceptLanguage,
1277
- 'content-type':
1278
- 'application/x-www-form-urlencoded; charset=UTF-8',
1017
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
1279
1018
  'ig-intended-user-id': '0',
1280
1019
  priority: 'u=3',
1281
-
1282
1020
  'x-bloks-is-layout-rtl': 'false',
1283
1021
  'x-bloks-prism-ax-base-colors-enabled': 'false',
1284
1022
  'x-bloks-prism-button-version': 'CONTROL',
@@ -1286,19 +1024,14 @@ class AccountRepository extends Repository {
1286
1024
  'x-bloks-prism-font-enabled': 'false',
1287
1025
  'x-bloks-prism-indigo-link-version': '0',
1288
1026
  'x-bloks-version-id': bloksVersionId,
1289
-
1290
1027
  'x-fb-client-ip': 'True',
1291
- 'x-fb-connection-type': 'WIFI',
1028
+ 'x-fb-connection-type': 'MOBILE.UNKNOWN',
1292
1029
  'x-fb-friendly-name':
1293
1030
  'IgApi: bloks/async_action/com.bloks.www.bloks.caa.login.async.send_login_request/',
1294
- 'x-fb-network-properties':
1295
- 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
1031
+ 'x-fb-network-properties': networkProps, // FIX: dynamic
1296
1032
  'x-fb-request-analytics-tags': JSON.stringify({
1297
1033
  network_tags: {
1298
- product: String(
1299
- this.client.state.fbAnalyticsApplicationId ||
1300
- '567067343352427'
1301
- ),
1034
+ product: String(this.client.state.fbAnalyticsApplicationId || '567067343352427'),
1302
1035
  purpose: 'fetch',
1303
1036
  surface: 'undefined',
1304
1037
  request_category: 'api',
@@ -1306,23 +1039,16 @@ class AccountRepository extends Repository {
1306
1039
  },
1307
1040
  }),
1308
1041
  'x-fb-server-cluster': 'True',
1309
-
1310
1042
  'x-ig-android-id': androidDeviceId,
1311
- 'x-ig-app-id': String(
1312
- this.client.state.fbAnalyticsApplicationId ||
1313
- '567067343352427'
1314
- ),
1043
+ 'x-ig-app-id': String(this.client.state.fbAnalyticsApplicationId || '567067343352427'),
1315
1044
  'x-ig-app-locale': lang,
1316
- 'x-ig-bandwidth-speed-kbps': (
1317
- Math.random() * 1500 +
1318
- 800
1319
- ).toFixed(3),
1045
+ 'x-ig-bandwidth-speed-kbps': (Math.random() * 1500 + 800).toFixed(3),
1320
1046
  'x-ig-bandwidth-totalbytes-b': '0',
1321
1047
  'x-ig-bandwidth-totaltime-ms': '0',
1322
1048
  'x-ig-client-endpoint':
1323
1049
  'com.bloks.www.caa.login.aymh_single_profile_screen_entry',
1324
1050
  'x-ig-capabilities': '3brTv10=',
1325
- 'x-ig-connection-type': 'WIFI',
1051
+ 'x-ig-connection-type': 'MOBILE(UNKNOWN)',
1326
1052
  'x-ig-device-id': qeDeviceId,
1327
1053
  'x-ig-device-locale': lang,
1328
1054
  'x-ig-family-device-id': familyDeviceId,
@@ -1330,29 +1056,20 @@ class AccountRepository extends Repository {
1330
1056
  'x-ig-nav-chain':
1331
1057
  'LockoutFragment:dogfooding_lockout:1:cold_start:...,com.bloks.www.caa.login.aymh_single_profile_screen_entry:com.bloks.www.caa.login.aymh_single_profile_screen_entry:14:button:0:::0',
1332
1058
  'x-ig-timezone-offset': String(
1333
- typeof this.client.state.timezoneOffset ===
1334
- 'number'
1059
+ typeof this.client.state.timezoneOffset === 'number'
1335
1060
  ? this.client.state.timezoneOffset
1336
1061
  : 7200
1337
1062
  ),
1338
1063
  'x-ig-www-claim': this.client.state.igWWWClaim || '0',
1339
1064
  'x-mid': machineId,
1340
- 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(
1341
- Math.random() * 1000
1342
- )
1343
- .toString()
1344
- .padStart(3, '0')}`,
1065
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
1345
1066
  'x-pigeon-session-id':
1346
1067
  this.client.state.pigeonSessionId ||
1347
- `UFS-${crypto.randomBytes(16).toString(
1348
- 'hex'
1349
- )}-1`,
1068
+ `UFS-${crypto.randomBytes(16).toString('hex')}-1`,
1350
1069
  'x-tigon-is-retry': 'False',
1351
-
1352
- 'accept-encoding': 'gzip, deflate, br',
1070
+ 'accept-encoding': 'gzip, deflate',
1353
1071
  'user-agent': userAgent,
1354
- 'x-fb-conn-uuid-client':
1355
- crypto.randomBytes(16).toString('hex'),
1072
+ 'x-fb-conn-uuid-client': crypto.randomBytes(16).toString('hex'),
1356
1073
  'x-fb-http-engine': 'MNS/TCP',
1357
1074
  'x-fb-rmd': 'state=URL_ELIGIBLE',
1358
1075
  };
@@ -1368,44 +1085,18 @@ class AccountRepository extends Repository {
1368
1085
  headers: bloksHeaders,
1369
1086
  });
1370
1087
 
1371
- // DEBUG login response
1372
- try {
1373
- const fs = require('fs');
1374
- const path = require('path');
1375
- const debugDir = path.join(
1376
- process.cwd(),
1377
- 'authinfo_instagram'
1378
- );
1379
- const debugFile = path.join(
1380
- debugDir,
1381
- 'login-debug.json'
1382
- );
1383
- try {
1384
- fs.mkdirSync(debugDir, { recursive: true });
1385
- } catch (e) {}
1386
-
1387
- const debugPayload = {
1388
- at: new Date().toISOString(),
1389
- statusCode:
1390
- response.statusCode || response.status || null,
1391
- headers: response.headers || null,
1392
- body: response.body || null,
1393
- };
1394
- fs.writeFileSync(
1395
- debugFile,
1396
- JSON.stringify(debugPayload, null, 2),
1397
- 'utf8'
1398
- );
1399
- } catch (e) {}
1088
+ debugWrite('login-debug.json', {
1089
+ at: new Date().toISOString(),
1090
+ statusCode: response.statusCode || response.status || null,
1091
+ headers: response.headers || null,
1092
+ body: response.body || null,
1093
+ });
1400
1094
 
1401
1095
  const body = response.body;
1402
-
1403
1096
  this._extractAndSaveAuthorization(response);
1404
1097
 
1405
1098
  if (body && body.two_factor_required) {
1406
- const err = new Error(
1407
- 'Two factor authentication required'
1408
- );
1099
+ const err = new Error('Two factor authentication required');
1409
1100
  err.name = 'IgLoginTwoFactorRequiredError';
1410
1101
  err.twoFactorInfo = body.two_factor_info;
1411
1102
  throw err;
@@ -1429,42 +1120,26 @@ class AccountRepository extends Repository {
1429
1120
 
1430
1121
  if (body && body.layout) {
1431
1122
  const layoutStr =
1432
- typeof body.layout === 'string'
1433
- ? body.layout
1434
- : JSON.stringify(body.layout);
1123
+ typeof body.layout === 'string' ? body.layout : JSON.stringify(body.layout);
1435
1124
 
1436
- if (
1437
- layoutStr.includes('two_factor_required') ||
1438
- layoutStr.includes('"two_factor_info"')
1439
- ) {
1125
+ if (layoutStr.includes('two_factor_required') || layoutStr.includes('"two_factor_info"')) {
1440
1126
  let twoFactorInfo = null;
1441
1127
  try {
1442
- const match = layoutStr.match(
1443
- /"two_factor_info"\s*:\s*(\{[^}]+\})/
1444
- );
1445
- if (match)
1446
- twoFactorInfo = JSON.parse(match[1]);
1128
+ const match = layoutStr.match(/"two_factor_info"\s*:\s*(\{[^}]+\})/);
1129
+ if (match) twoFactorInfo = JSON.parse(match[1]);
1447
1130
  } catch (e) {}
1448
- const err = new Error(
1449
- 'Two factor authentication required'
1450
- );
1131
+ const err = new Error('Two factor authentication required');
1451
1132
  err.name = 'IgLoginTwoFactorRequiredError';
1452
1133
  err.twoFactorInfo = twoFactorInfo;
1453
1134
  throw err;
1454
1135
  }
1455
1136
 
1456
- if (
1457
- layoutStr.includes('bad_password') ||
1458
- layoutStr.includes('incorrect_password')
1459
- ) {
1137
+ if (layoutStr.includes('bad_password') || layoutStr.includes('incorrect_password')) {
1460
1138
  const err = new Error('Bad password');
1461
1139
  err.name = 'IgLoginBadPasswordError';
1462
1140
  throw err;
1463
1141
  }
1464
- if (
1465
- layoutStr.includes('invalid_user') ||
1466
- layoutStr.includes('user_not_found')
1467
- ) {
1142
+ if (layoutStr.includes('invalid_user') || layoutStr.includes('user_not_found')) {
1468
1143
  const err = new Error('Invalid user');
1469
1144
  err.name = 'IgLoginInvalidUserError';
1470
1145
  throw err;
@@ -1476,14 +1151,10 @@ class AccountRepository extends Repository {
1476
1151
  throw err;
1477
1152
  }
1478
1153
 
1479
- const tokenFromLayout =
1480
- this._findBearerInString(layoutStr);
1154
+ const tokenFromLayout = this._findBearerInString(layoutStr);
1481
1155
  if (tokenFromLayout) {
1482
- this.client.state.authorization =
1483
- tokenFromLayout;
1484
- try {
1485
- this.client.state.updateAuthorization();
1486
- } catch (e) {}
1156
+ this.client.state.authorization = tokenFromLayout;
1157
+ try { this.client.state.updateAuthorization(); } catch (e) {}
1487
1158
  }
1488
1159
 
1489
1160
  const pkPatterns = [
@@ -1499,8 +1170,7 @@ class AccountRepository extends Repository {
1499
1170
  const pkMatch = layoutStr.match(pattern);
1500
1171
  if (pkMatch) {
1501
1172
  extractedPk = pkMatch[1];
1502
- this.client.state.cookieUserId =
1503
- extractedPk;
1173
+ this.client.state.cookieUserId = extractedPk;
1504
1174
  this.client.state._userId = extractedPk;
1505
1175
  break;
1506
1176
  }
@@ -1513,89 +1183,54 @@ class AccountRepository extends Repository {
1513
1183
  /x-ig-set-www-claim[\\"\s:]+([a-f0-9]+)/i,
1514
1184
  ];
1515
1185
  for (const pattern of wwwClaimPatterns) {
1516
- const wwwClaimMatch = layoutStr.match(pattern);
1517
- if (wwwClaimMatch) {
1518
- this.client.state.igWWWClaim =
1519
- wwwClaimMatch[1];
1520
- break;
1521
- }
1186
+ const m = layoutStr.match(pattern);
1187
+ if (m) { this.client.state.igWWWClaim = m[1]; break; }
1522
1188
  }
1523
1189
 
1524
1190
  const encKeyIdMatch2 =
1525
- layoutStr.match(
1526
- /IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i
1527
- ) ||
1528
- layoutStr.match(
1529
- /password.encryption.key.id[\\"\s:]+(\d+)/i
1530
- );
1191
+ layoutStr.match(/IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i) ||
1192
+ layoutStr.match(/password.encryption.key.id[\\"\s:]+(\d+)/i);
1531
1193
  if (encKeyIdMatch2) {
1532
- this.client.state.passwordEncryptionKeyId =
1533
- parseInt(encKeyIdMatch2[1]);
1194
+ this.client.state.passwordEncryptionKeyId = parseInt(encKeyIdMatch2[1]);
1534
1195
  }
1535
1196
 
1536
1197
  const encPubKeyMatch2 =
1537
- layoutStr.match(
1538
- /IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i
1539
- ) ||
1540
- layoutStr.match(
1541
- /password.encryption.pub.key[\\"\s:]+([A-Za-z0-9+\/=]+)/i
1542
- );
1198
+ layoutStr.match(/IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i) ||
1199
+ layoutStr.match(/password.encryption.pub.key[\\"\s:]+([A-Za-z0-9+\/=]+)/i);
1543
1200
  if (encPubKeyMatch2) {
1544
- this.client.state.passwordEncryptionPubKey =
1545
- encPubKeyMatch2[1];
1201
+ this.client.state.passwordEncryptionPubKey = encPubKeyMatch2[1];
1546
1202
  }
1547
1203
 
1548
1204
  const midMatch2 =
1549
- layoutStr.match(
1550
- /ig-set-x-mid[\\"\s:]+([A-Za-z0-9+\/=_-]+)/i
1551
- ) ||
1552
- layoutStr.match(
1553
- /\\"x-mid\\"[\\"\s:]+([A-Za-z0-9+\/=_-]+)/i
1554
- );
1555
- if (midMatch2) {
1556
- this.client.state.mid = midMatch2[1];
1557
- }
1205
+ layoutStr.match(/ig-set-x-mid[\\"\s:]+([A-Za-z0-9+\/=_-]+)/i) ||
1206
+ layoutStr.match(/\\"x-mid\\"[\\"\s:]+([A-Za-z0-9+\/=_-]+)/i);
1207
+ if (midMatch2) this.client.state.mid = midMatch2[1];
1558
1208
 
1559
1209
  const loginResponsePatterns = [
1560
- /\\"logged_in_user\\":\{[^}]*\\"pk\\":(\d+)[^}]*\\"username\\":\\"([^"\\]+)\\"/,
1210
+ /\\"logged_in_user\\":\{[^}]*\\"pk\\":(\\d+)[^}]*\\"username\\":\\"([^"\\]+)\\"/,
1561
1211
  /"logged_in_user":\{[^}]*"pk":(\d+)[^}]*"username":"([^"]+)"/,
1562
1212
  /\\"logged_in_user\\".*?\\"pk\\":\s*(\d+).*?\\"username\\":\s*\\"([^"\\]+)\\"/s,
1563
1213
  ];
1564
1214
  for (const pattern of loginResponsePatterns) {
1565
- const loginResponseMatch =
1566
- layoutStr.match(pattern);
1567
- if (loginResponseMatch) {
1568
- this.client.state.cookieUserId =
1569
- loginResponseMatch[1];
1570
- this.client.state._userId =
1571
- loginResponseMatch[1];
1572
- return {
1573
- pk: parseInt(loginResponseMatch[1]),
1574
- pk_id: loginResponseMatch[1],
1575
- username: loginResponseMatch[2],
1576
- };
1215
+ const m = layoutStr.match(pattern);
1216
+ if (m) {
1217
+ this.client.state.cookieUserId = m[1];
1218
+ this.client.state._userId = m[1];
1219
+ return { pk: parseInt(m[1]), pk_id: m[1], username: m[2] };
1577
1220
  }
1578
1221
  }
1579
1222
 
1580
- if (
1581
- extractedPk &&
1582
- this.client.state.authorization
1583
- ) {
1223
+ if (extractedPk && this.client.state.authorization) {
1584
1224
  try {
1585
1225
  const userInfo = await this.currentUser();
1586
1226
  const user = userInfo.user || userInfo;
1587
1227
  if (user && user.pk) {
1588
- this.client.state.cookieUserId =
1589
- String(user.pk);
1590
- this.client.state._userId =
1591
- String(user.pk);
1228
+ this.client.state.cookieUserId = String(user.pk);
1229
+ this.client.state._userId = String(user.pk);
1592
1230
  }
1593
1231
  return user;
1594
1232
  } catch (e) {
1595
- return {
1596
- pk: parseInt(extractedPk),
1597
- pk_id: extractedPk,
1598
- };
1233
+ return { pk: parseInt(extractedPk), pk_id: extractedPk };
1599
1234
  }
1600
1235
  }
1601
1236
  }
@@ -1615,8 +1250,7 @@ class AccountRepository extends Repository {
1615
1250
  const userInfo = await this.currentUser();
1616
1251
  const user = userInfo.user || userInfo;
1617
1252
  if (user && user.pk) {
1618
- this.client.state.cookieUserId =
1619
- String(user.pk);
1253
+ this.client.state.cookieUserId = String(user.pk);
1620
1254
  this.client.state._userId = String(user.pk);
1621
1255
  }
1622
1256
  return user;
@@ -1629,12 +1263,7 @@ class AccountRepository extends Repository {
1629
1263
  });
1630
1264
  }
1631
1265
 
1632
- async twoFactorLogin(
1633
- username,
1634
- verificationCode,
1635
- twoFactorIdentifier,
1636
- verificationMethod = '1'
1637
- ) {
1266
+ async twoFactorLogin(username, verificationCode, twoFactorIdentifier, verificationMethod = '1') {
1638
1267
  return this.requestWithRetry(async () => {
1639
1268
  const response = await this.client.request.send({
1640
1269
  method: 'POST',
@@ -1660,9 +1289,7 @@ class AccountRepository extends Repository {
1660
1289
  const response = await this.client.request.send({
1661
1290
  method: 'POST',
1662
1291
  url: '/api/v1/accounts/logout/',
1663
- form: this.client.request.sign({
1664
- _uuid: this.client.state.uuid,
1665
- }),
1292
+ form: this.client.request.sign({ _uuid: this.client.state.uuid }),
1666
1293
  });
1667
1294
  return response.body;
1668
1295
  });
@@ -1683,14 +1310,7 @@ class AccountRepository extends Repository {
1683
1310
  return this.currentUser();
1684
1311
  }
1685
1312
 
1686
- async editProfile({
1687
- externalUrl,
1688
- phoneNumber,
1689
- username,
1690
- fullName,
1691
- biography,
1692
- email,
1693
- } = {}) {
1313
+ async editProfile({ externalUrl, phoneNumber, username, fullName, biography, email } = {}) {
1694
1314
  return this.requestWithRetry(async () => {
1695
1315
  const current = await this.currentUser();
1696
1316
  const user = current.user || current;
@@ -1699,57 +1319,28 @@ class AccountRepository extends Repository {
1699
1319
  url: '/api/v1/accounts/edit_profile/',
1700
1320
  form: this.client.request.sign({
1701
1321
  _uuid: this.client.state.uuid,
1702
- external_url:
1703
- externalUrl !== undefined
1704
- ? externalUrl
1705
- : user.external_url || '',
1706
- phone_number:
1707
- phoneNumber !== undefined
1708
- ? phoneNumber
1709
- : user.phone_number || '',
1710
- username:
1711
- username !== undefined
1712
- ? username
1713
- : user.username,
1714
- full_name:
1715
- fullName !== undefined
1716
- ? fullName
1717
- : user.full_name || '',
1718
- biography:
1719
- biography !== undefined
1720
- ? biography
1721
- : user.biography || '',
1722
- email:
1723
- email !== undefined
1724
- ? email
1725
- : user.email || '',
1322
+ external_url: externalUrl !== undefined ? externalUrl : user.external_url || '',
1323
+ phone_number: phoneNumber !== undefined ? phoneNumber : user.phone_number || '',
1324
+ username: username !== undefined ? username : user.username,
1325
+ full_name: fullName !== undefined ? fullName : user.full_name || '',
1326
+ biography: biography !== undefined ? biography : user.biography || '',
1327
+ email: email !== undefined ? email : user.email || '',
1726
1328
  }),
1727
1329
  });
1728
1330
  return response.body;
1729
1331
  });
1730
1332
  }
1731
1333
 
1732
- async setBiography(biography) {
1733
- return this.editProfile({ biography });
1734
- }
1735
-
1736
- async setExternalUrl(url) {
1737
- return this.editProfile({ externalUrl: url });
1738
- }
1739
-
1740
- async removeBioLinks() {
1741
- return this.editProfile({ externalUrl: '' });
1742
- }
1334
+ async setBiography(biography) { return this.editProfile({ biography }); }
1335
+ async setExternalUrl(url) { return this.editProfile({ externalUrl: url }); }
1336
+ async removeBioLinks() { return this.editProfile({ externalUrl: '' }); }
1743
1337
 
1744
1338
  async setGender(gender) {
1745
1339
  return this.requestWithRetry(async () => {
1746
1340
  const response = await this.client.request.send({
1747
1341
  method: 'POST',
1748
1342
  url: '/api/v1/accounts/set_gender/',
1749
- form: this.client.request.sign({
1750
- _uuid: this.client.state.uuid,
1751
- gender,
1752
- }),
1343
+ form: this.client.request.sign({ _uuid: this.client.state.uuid, gender }),
1753
1344
  });
1754
1345
  return response.body;
1755
1346
  });
@@ -1760,9 +1351,7 @@ class AccountRepository extends Repository {
1760
1351
  const response = await this.client.request.send({
1761
1352
  method: 'POST',
1762
1353
  url: '/api/v1/accounts/set_private/',
1763
- form: this.client.request.sign({
1764
- _uuid: this.client.state.uuid,
1765
- }),
1354
+ form: this.client.request.sign({ _uuid: this.client.state.uuid }),
1766
1355
  });
1767
1356
  return response.body;
1768
1357
  });
@@ -1773,9 +1362,7 @@ class AccountRepository extends Repository {
1773
1362
  const response = await this.client.request.send({
1774
1363
  method: 'POST',
1775
1364
  url: '/api/v1/accounts/set_public/',
1776
- form: this.client.request.sign({
1777
- _uuid: this.client.state.uuid,
1778
- }),
1365
+ form: this.client.request.sign({ _uuid: this.client.state.uuid }),
1779
1366
  });
1780
1367
  return response.body;
1781
1368
  });
@@ -1804,10 +1391,7 @@ class AccountRepository extends Repository {
1804
1391
  const response = await this.client.request.send({
1805
1392
  method: 'POST',
1806
1393
  url: '/api/v1/accounts/send_confirm_email/',
1807
- form: this.client.request.sign({
1808
- _uuid: this.client.state.uuid,
1809
- send_source: 'edit_profile',
1810
- }),
1394
+ form: this.client.request.sign({ _uuid: this.client.state.uuid, send_source: 'edit_profile' }),
1811
1395
  });
1812
1396
  return response.body;
1813
1397
  });
@@ -1818,10 +1402,7 @@ class AccountRepository extends Repository {
1818
1402
  const response = await this.client.request.send({
1819
1403
  method: 'POST',
1820
1404
  url: '/api/v1/accounts/send_confirm_phone_number/',
1821
- form: this.client.request.sign({
1822
- _uuid: this.client.state.uuid,
1823
- phone_number: phoneNumber,
1824
- }),
1405
+ form: this.client.request.sign({ _uuid: this.client.state.uuid, phone_number: phoneNumber }),
1825
1406
  });
1826
1407
  return response.body;
1827
1408
  });
@@ -1847,9 +1428,7 @@ class AccountRepository extends Repository {
1847
1428
  const response = await this.client.request.send({
1848
1429
  method: 'POST',
1849
1430
  url: '/api/v1/accounts/remove_profile_picture/',
1850
- form: this.client.request.sign({
1851
- _uuid: this.client.state.uuid,
1852
- }),
1431
+ form: this.client.request.sign({ _uuid: this.client.state.uuid }),
1853
1432
  });
1854
1433
  return response.body;
1855
1434
  });
@@ -1873,9 +1452,7 @@ class AccountRepository extends Repository {
1873
1452
  form: this.client.request.sign({
1874
1453
  id: this.client.state.uuid,
1875
1454
  server_config_retrieval: '1',
1876
- experiments:
1877
- this.client.state.constants
1878
- .LOGIN_EXPERIMENTS,
1455
+ experiments: this.client.state.constants.LOGIN_EXPERIMENTS,
1879
1456
  }),
1880
1457
  });
1881
1458
  return response.body;
@@ -1884,12 +1461,7 @@ class AccountRepository extends Repository {
1884
1461
 
1885
1462
  async syncPostLoginExperiments() {
1886
1463
  let userId;
1887
- try {
1888
- userId = this.client.state.cookieUserId;
1889
- } catch {
1890
- userId = '0';
1891
- }
1892
-
1464
+ try { userId = this.client.state.cookieUserId; } catch { userId = '0'; }
1893
1465
  return this.requestWithRetry(async () => {
1894
1466
  const response = await this.client.request.send({
1895
1467
  method: 'POST',
@@ -1899,8 +1471,7 @@ class AccountRepository extends Repository {
1899
1471
  _uid: userId,
1900
1472
  _uuid: this.client.state.uuid,
1901
1473
  server_config_retrieval: '1',
1902
- experiments:
1903
- this.client.state.constants.EXPERIMENTS,
1474
+ experiments: this.client.state.constants.EXPERIMENTS,
1904
1475
  }),
1905
1476
  });
1906
1477
  return response.body;
@@ -1908,17 +1479,13 @@ class AccountRepository extends Repository {
1908
1479
  }
1909
1480
 
1910
1481
  async syncLauncher(preLogin = true) {
1911
- const data = {
1912
- id: this.client.state.uuid,
1913
- server_config_retrieval: '1',
1914
- };
1482
+ const data = { id: this.client.state.uuid, server_config_retrieval: '1' };
1915
1483
  if (!preLogin) {
1916
1484
  try {
1917
1485
  data._uid = this.client.state.cookieUserId;
1918
1486
  data._uuid = this.client.state.uuid;
1919
1487
  } catch {}
1920
1488
  }
1921
-
1922
1489
  return this.requestWithRetry(async () => {
1923
1490
  const response = await this.client.request.send({
1924
1491
  method: 'POST',
@@ -1934,10 +1501,7 @@ class AccountRepository extends Repository {
1934
1501
  const response = await this.client.request.send({
1935
1502
  method: 'POST',
1936
1503
  url: '/api/v1/devices/sync/',
1937
- form: this.client.request.sign({
1938
- id: this.client.state.uuid,
1939
- server_config_retrieval: '1',
1940
- }),
1504
+ form: this.client.request.sign({ id: this.client.state.uuid, server_config_retrieval: '1' }),
1941
1505
  });
1942
1506
  return response.body;
1943
1507
  });
@@ -1964,10 +1528,7 @@ class AccountRepository extends Repository {
1964
1528
  const response = await this.client.request.send({
1965
1529
  method: 'POST',
1966
1530
  url: '/api/v1/accounts/contact_point_prefill/',
1967
- form: this.client.request.sign({
1968
- phone_id: this.client.state.phoneId,
1969
- usage,
1970
- }),
1531
+ form: this.client.request.sign({ phone_id: this.client.state.phoneId, usage }),
1971
1532
  });
1972
1533
  return response.body;
1973
1534
  });
@@ -1995,10 +1556,7 @@ class AccountRepository extends Repository {
1995
1556
  const response = await this.client.request.send({
1996
1557
  method: 'GET',
1997
1558
  url: '/api/v1/consent/get_signup_config/',
1998
- qs: {
1999
- guid: this.client.state.uuid,
2000
- main_account_selected: false,
2001
- },
1559
+ qs: { guid: this.client.state.uuid, main_account_selected: false },
2002
1560
  });
2003
1561
  return response.body;
2004
1562
  });
@@ -2036,91 +1594,70 @@ class AccountRepository extends Repository {
2036
1594
  });
2037
1595
  }
2038
1596
 
1597
+ /**
1598
+ * FIX MAJOR — generateAttestParams: each certificate in the chain
1599
+ * now has its own EC key pair, correctly simulating the
1600
+ * Android Keystore hierarchy (leaf → intermediate1 → intermediate2 → root).
1601
+ */
2039
1602
  static generateAttestParams(state) {
2040
- const challengeNonce = crypto
2041
- .randomBytes(24)
2042
- .toString('base64url');
2043
- const { privateKey, publicKey } = crypto.generateKeyPairSync(
2044
- 'ec',
2045
- {
2046
- namedCurve: 'prime256v1',
2047
- }
1603
+ // Key for leaf (the actually attested key)
1604
+ const leafKey = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
1605
+
1606
+ const challengeNonce = crypto.randomBytes(24).toString('base64url');
1607
+ const signedData = crypto.sign(null, Buffer.from(challengeNonce), leafKey.privateKey);
1608
+ const signedNonce = signedData.toString('base64');
1609
+ const publicKeyDer = leafKey.publicKey.export({ type: 'spki', format: 'der' });
1610
+ const keyHash = crypto.createHash('sha256').update(publicKeyDer).digest('hex');
1611
+
1612
+ // Separate keys for each level in the chain
1613
+ const int1Key = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
1614
+ const int2Key = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
1615
+ const rootKey = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
1616
+
1617
+ const leafCertPem = AccountRepository._generateSignedCert(
1618
+ leafKey.privateKey, leafKey.publicKey, 'Android Keystore Key',
1619
+ int1Key.privateKey // signed by intermediate 1
2048
1620
  );
2049
- const signedData = crypto.sign(
2050
- null,
2051
- Buffer.from(challengeNonce),
2052
- privateKey
1621
+ const intermediate1Pem = AccountRepository._generateSignedCert(
1622
+ int1Key.privateKey, int1Key.publicKey, 'Android Keystore Key Attestation',
1623
+ int2Key.privateKey // signed by intermediate 2
1624
+ );
1625
+ const intermediate2Pem = AccountRepository._generateSignedCert(
1626
+ int2Key.privateKey, int2Key.publicKey, 'Android Hardware Keystore',
1627
+ rootKey.privateKey // self-signed root
1628
+ );
1629
+ const rootCertPem = AccountRepository._generateSignedCert(
1630
+ rootKey.privateKey, rootKey.publicKey, 'Android Keystore Root',
1631
+ rootKey.privateKey // self-signed root
2053
1632
  );
2054
- const signedNonce = signedData.toString('base64');
2055
- const publicKeyDer = publicKey.export({
2056
- type: 'spki',
2057
- format: 'der',
2058
- });
2059
- const keyHash = crypto
2060
- .createHash('sha256')
2061
- .update(publicKeyDer)
2062
- .digest('hex');
2063
-
2064
- const leafCertPem =
2065
- AccountRepository._generateSelfSignedCert(
2066
- privateKey,
2067
- publicKey,
2068
- 'Android Keystore Key'
2069
- );
2070
- const intermediate1Pem =
2071
- AccountRepository._generateSelfSignedCert(
2072
- privateKey,
2073
- publicKey,
2074
- 'Android Keystore Key Attestation'
2075
- );
2076
- const intermediate2Pem =
2077
- AccountRepository._generateSelfSignedCert(
2078
- privateKey,
2079
- publicKey,
2080
- 'Android Hardware Keystore'
2081
- );
2082
- const rootCertPem =
2083
- AccountRepository._generateSelfSignedCert(
2084
- privateKey,
2085
- publicKey,
2086
- 'Android Keystore Root'
2087
- );
2088
1633
 
2089
- const certificateChain = [
2090
- leafCertPem,
2091
- intermediate1Pem,
2092
- intermediate2Pem,
2093
- rootCertPem,
2094
- ].join('\n');
1634
+ const certificateChain = [leafCertPem, intermediate1Pem, intermediate2Pem, rootCertPem].join('\n');
2095
1635
 
2096
1636
  return {
2097
- attestation: [
2098
- {
2099
- version: 2,
2100
- type: 'keystore',
2101
- errors: [0],
2102
- challenge_nonce: challengeNonce,
2103
- signed_nonce: signedNonce,
2104
- key_hash: keyHash,
2105
- certificate_chain: certificateChain,
2106
- },
2107
- ],
1637
+ attestation: [{
1638
+ version: 2,
1639
+ type: 'keystore',
1640
+ errors: [0],
1641
+ challenge_nonce: challengeNonce,
1642
+ signed_nonce: signedNonce,
1643
+ key_hash: keyHash,
1644
+ certificate_chain: certificateChain,
1645
+ }],
2108
1646
  };
2109
1647
  }
2110
1648
 
2111
- static _generateSelfSignedCert(privateKey, publicKey, cn) {
2112
- const publicKeyDer = publicKey.export({
2113
- type: 'spki',
2114
- format: 'der',
2115
- });
1649
+ /**
1650
+ * FIX: _generateSignedCert now accepts a separate signing key
1651
+ * (signerPrivateKey), enabling the correct certificate hierarchy.
1652
+ */
1653
+ static _generateSignedCert(privateKey, publicKey, cn, signerPrivateKey) {
1654
+ if (!signerPrivateKey) signerPrivateKey = privateKey; // self-signed fallback
1655
+
1656
+ const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' });
2116
1657
  const serialNumber = crypto.randomBytes(8);
2117
1658
  const now = new Date();
2118
- const notBefore = new Date(
2119
- now.getTime() - 365 * 24 * 60 * 60 * 1000
2120
- );
2121
- const notAfter = new Date(
2122
- now.getTime() + 10 * 365 * 24 * 60 * 60 * 1000
2123
- );
1659
+ const notBefore = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
1660
+ const notAfter = new Date(now.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
2124
1661
  const cnBytes = Buffer.from(cn, 'utf8');
2125
1662
  const cnSeq = Buffer.concat([
2126
1663
  Buffer.from([0x30, cnBytes.length + 13]),
@@ -2132,122 +1669,83 @@ class AccountRepository extends Repository {
2132
1669
  ]);
2133
1670
 
2134
1671
  function encodeTime(date) {
2135
- const str =
2136
- date
2137
- .toISOString()
2138
- .replace(/[-:T]/g, '')
2139
- .slice(2, 14) + 'Z';
2140
- return Buffer.concat([
2141
- Buffer.from([0x17, str.length]),
2142
- Buffer.from(str),
2143
- ]);
1672
+ const str = date.toISOString().replace(/[-:T]/g, '').slice(2, 14) + 'Z';
1673
+ return Buffer.concat([Buffer.from([0x17, str.length]), Buffer.from(str)]);
2144
1674
  }
2145
1675
 
2146
- const validityBuf = Buffer.concat([
2147
- encodeTime(notBefore),
2148
- encodeTime(notAfter),
2149
- ]);
2150
- const validity = Buffer.concat([
2151
- Buffer.from([0x30, validityBuf.length]),
2152
- validityBuf,
2153
- ]);
1676
+ const validityBuf = Buffer.concat([encodeTime(notBefore), encodeTime(notAfter)]);
1677
+ const validity = Buffer.concat([Buffer.from([0x30, validityBuf.length]), validityBuf]);
2154
1678
 
2155
1679
  const tbs = Buffer.concat([
2156
1680
  Buffer.from([0xa0, 0x03, 0x02, 0x01, 0x02]),
2157
1681
  Buffer.from([0x02, serialNumber.length]),
2158
1682
  serialNumber,
2159
- Buffer.from([
2160
- 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d,
2161
- 0x04, 0x03, 0x02,
2162
- ]),
1683
+ Buffer.from([0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]),
2163
1684
  cnSeq,
2164
1685
  validity,
2165
1686
  cnSeq,
2166
1687
  publicKeyDer,
2167
1688
  ]);
2168
1689
 
2169
- const tbsSeq = Buffer.concat([
2170
- Buffer.from([0x30, 0x82]),
2171
- Buffer.alloc(2),
2172
- tbs,
2173
- ]);
1690
+ const tbsSeq = Buffer.concat([Buffer.from([0x30, 0x82]), Buffer.alloc(2), tbs]);
2174
1691
  tbsSeq.writeUInt16BE(tbs.length, 2);
2175
1692
 
2176
- const signature = crypto.sign(null, tbsSeq, privateKey);
1693
+ // FIX: signed with signerPrivateKey (can be an upper-level key)
1694
+ const signature = crypto.sign(null, tbsSeq, signerPrivateKey);
2177
1695
  const sigBitString = Buffer.concat([
2178
1696
  Buffer.from([0x03, signature.length + 1, 0x00]),
2179
1697
  signature,
2180
1698
  ]);
2181
- const algId = Buffer.from([
2182
- 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d,
2183
- 0x04, 0x03, 0x02,
2184
- ]);
1699
+ const algId = Buffer.from([0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]);
2185
1700
  const certBody = Buffer.concat([tbsSeq, algId, sigBitString]);
2186
- const cert = Buffer.concat([
2187
- Buffer.from([0x30, 0x82]),
2188
- Buffer.alloc(2),
2189
- certBody,
2190
- ]);
1701
+ const cert = Buffer.concat([Buffer.from([0x30, 0x82]), Buffer.alloc(2), certBody]);
2191
1702
  cert.writeUInt16BE(certBody.length, 2);
2192
1703
 
2193
1704
  const b64 = cert.toString('base64');
2194
1705
  const lines = b64.match(/.{1,76}/g) || [b64];
2195
- return (
2196
- '-----BEGIN CERTIFICATE-----\n' +
2197
- lines.join('\n') +
2198
- '\n-----END CERTIFICATE-----'
2199
- );
1706
+ return '-----BEGIN CERTIFICATE-----\n' + lines.join('\n') + '\n-----END CERTIFICATE-----';
1707
+ }
1708
+
1709
+ // Kept for compatibility — internally calls _generateSignedCert
1710
+ static _generateSelfSignedCert(privateKey, publicKey, cn) {
1711
+ return AccountRepository._generateSignedCert(privateKey, publicKey, cn, privateKey);
2200
1712
  }
2201
1713
 
2202
1714
  static createJazoest(input) {
2203
1715
  const buf = Buffer.from(input || '', 'ascii');
2204
1716
  let sum = 0;
2205
- for (let i = 0; i < buf.byteLength; i++) {
2206
- sum += buf.readUInt8(i);
2207
- }
1717
+ for (let i = 0; i < buf.byteLength; i++) sum += buf.readUInt8(i);
2208
1718
  return `2${sum}`;
2209
1719
  }
2210
1720
 
2211
1721
  encryptPassword(password) {
2212
1722
  if (!this.client.state.passwordEncryptionPubKey) {
2213
- return {
2214
- time: Math.floor(Date.now() / 1000).toString(),
2215
- encrypted: password,
2216
- };
1723
+ // FIX: no longer returning password in plaintext — throws a controlled error
1724
+ throw new Error(
1725
+ 'passwordEncryptionPubKey missing from state. ' +
1726
+ 'Cannot encrypt password safely.'
1727
+ );
2217
1728
  }
2218
1729
  const randKey = crypto.randomBytes(32);
2219
1730
  const iv = crypto.randomBytes(12);
2220
1731
  const rsaEncrypted = crypto.publicEncrypt(
2221
1732
  {
2222
- key: Buffer.from(
2223
- this.client.state.passwordEncryptionPubKey,
2224
- 'base64'
2225
- ).toString(),
1733
+ key: Buffer.from(this.client.state.passwordEncryptionPubKey, 'base64').toString(),
2226
1734
  padding: crypto.constants.RSA_PKCS1_PADDING,
2227
1735
  },
2228
1736
  randKey
2229
1737
  );
2230
- const cipher = crypto.createCipheriv(
2231
- 'aes-256-gcm',
2232
- randKey,
2233
- iv
2234
- );
1738
+ const cipher = crypto.createCipheriv('aes-256-gcm', randKey, iv);
2235
1739
  const time = Math.floor(Date.now() / 1000).toString();
2236
1740
  cipher.setAAD(Buffer.from(time));
2237
- const aesEncrypted = Buffer.concat([
2238
- cipher.update(password, 'utf8'),
2239
- cipher.final(),
2240
- ]);
1741
+ const aesEncrypted = Buffer.concat([cipher.update(password, 'utf8'), cipher.final()]);
2241
1742
  const sizeBuffer = Buffer.alloc(2, 0);
2242
1743
  sizeBuffer.writeInt16LE(rsaEncrypted.byteLength, 0);
2243
1744
  const authTag = cipher.getAuthTag();
2244
1745
  return {
2245
1746
  time,
2246
1747
  encrypted: Buffer.concat([
2247
- Buffer.from([
2248
- 1,
2249
- this.client.state.passwordEncryptionKeyId || 0,
2250
- ]),
1748
+ Buffer.from([1, this.client.state.passwordEncryptionKeyId || 0]),
2251
1749
  iv,
2252
1750
  sizeBuffer,
2253
1751
  rsaEncrypted,
@@ -2258,16 +1756,10 @@ class AccountRepository extends Repository {
2258
1756
  }
2259
1757
 
2260
1758
  async passwordPublicKeys() {
2261
- const response = await this.client.request.send({
2262
- method: 'GET',
2263
- url: '/api/v1/qe/sync/',
2264
- });
1759
+ const response = await this.client.request.send({ method: 'GET', url: '/api/v1/qe/sync/' });
2265
1760
  const headers = response.headers || {};
2266
- const keyId = parseInt(
2267
- headers['ig-set-password-encryption-key-id'] || '0'
2268
- );
2269
- const pubKey =
2270
- headers['ig-set-password-encryption-pub-key'] || '';
1761
+ const keyId = parseInt(headers['ig-set-password-encryption-key-id'] || '0');
1762
+ const pubKey = headers['ig-set-password-encryption-pub-key'] || '';
2271
1763
  return { keyId, pubKey };
2272
1764
  }
2273
1765
 
@@ -2309,6 +1801,7 @@ class AccountRepository extends Repository {
2309
1801
  });
2310
1802
  }
2311
1803
 
1804
+ // CRITICAL FIX: 'preferen1gces' → 'preferences' (typo → ReferenceError fix)
2312
1805
  async pushPreferences(preferences = 'default') {
2313
1806
  return this.requestWithRetry(async () => {
2314
1807
  const response = await this.client.request.send({
@@ -2321,7 +1814,7 @@ class AccountRepository extends Repository {
2321
1814
  phone_id: this.client.state.phoneId,
2322
1815
  device_token: '',
2323
1816
  guid: this.client.state.uuid,
2324
- users: preferen1gces,
1817
+ users: preferences, // FIX: was 'preferen1gces'
2325
1818
  }),
2326
1819
  });
2327
1820
  return response.body;