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

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,18 +291,22 @@ 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 / dynamically generated fallback.
299
+ * Priority:
300
+ * 1. state.userAgent / state.appUserAgent / request.userAgent / state.deviceString
301
+ * 2. Built dynamically from state device parameters (androidRelease, manufacturer, model, etc.)
302
+ * 3. Last-resort realistic static fallback
303
+ * This way the user-agent always reflects the emulated device set in state.
307
304
  */
308
305
  _resolveUserAgent() {
309
306
  const state = this.client.state || {};
310
307
  const req = this.client.request || {};
311
308
 
309
+ // 1) Explicit user-agent already set in state
312
310
  const candidates = [
313
311
  state.userAgent,
314
312
  state.appUserAgent,
@@ -316,30 +314,41 @@ class AccountRepository extends Repository {
316
314
  state.deviceString,
317
315
  ].filter(Boolean);
318
316
 
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)';
317
+ if (candidates.length > 0) {
318
+ this.client.state.userAgent = candidates[0];
319
+ return candidates[0];
325
320
  }
326
321
 
327
- // Sincronizăm în state ca fie disponibil peste tot
328
- this.client.state.userAgent = userAgent;
329
- return userAgent;
322
+ // 2) Build dynamically from device parameters stored in state
323
+ try {
324
+ const igVersion = state.appVersion || state.igVersion || '415.0.0.36.76';
325
+ const buildNumber = state.appVersionCode || state.buildNumber || '580610226';
326
+ const androidApi = state.androidVersion || state.androidApiLevel || '35';
327
+ const androidRel = state.androidRelease || state.androidOsVersion || '15';
328
+ const dpi = state.dpi || state.screenDpi || '480dpi';
329
+ const resolution = state.resolution || state.screenResolution || '1080x2400';
330
+ const manufacturer = state.manufacturer || state.deviceManufacturer || 'Google';
331
+ const brand = state.brand || state.deviceBrand || manufacturer;
332
+ const model = state.model || state.deviceModel || 'Pixel 7';
333
+ const device = state.device || state.deviceName || 'panther';
334
+ const cpu = state.cpu || state.deviceCpu || 'gs201';
335
+ const lang = (state.language || 'en_US').replace('-', '_');
336
+
337
+ const ua = `Instagram ${igVersion} Android (${androidApi}/${androidRel}; ${dpi}; ${resolution}; ${brand}/${manufacturer}; ${model}; ${device}; ${cpu}; ${lang}; ${buildNumber})`;
338
+ this.client.state.userAgent = ua;
339
+ return ua;
340
+ } catch (e) {}
341
+
342
+ // 3) Last-resort static fallback
343
+ const fallback = 'Instagram 415.0.0.36.76 Android (35/15; 480dpi; 1280x2856; Google; Pixel 9 Pro; caiman; google; en_US; 580610226)';
344
+ this.client.state.userAgent = fallback;
345
+ return fallback;
330
346
  }
331
347
 
332
- // Ensure we have a valid csrftoken cookie (and mid) before sensitive endpoints.
333
348
  async ensureCsrfToken() {
334
349
  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 {}
350
+ if (cookieToken && cookieToken !== 'missing' && cookieToken !== 'pending') {
351
+ try { this.client.state.csrfToken = cookieToken; } catch {}
343
352
  return cookieToken;
344
353
  }
345
354
 
@@ -351,24 +360,13 @@ class AccountRepository extends Repository {
351
360
  } catch (e) {}
352
361
 
353
362
  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 {}
363
+ if (token && token !== 'missing' && token !== 'pending') {
364
+ try { this.client.state.csrfToken = token; } catch {}
362
365
  return token;
363
366
  }
364
-
365
367
  return null;
366
368
  }
367
369
 
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
370
  async _prefetchOauthTokenForLogin(username) {
373
371
  if (!username) return null;
374
372
 
@@ -383,16 +381,12 @@ class AccountRepository extends Repository {
383
381
  const familyDeviceId =
384
382
  this.client.state.phoneId ||
385
383
  this.client.state.familyDeviceId ||
386
- (crypto.randomUUID
387
- ? crypto.randomUUID()
388
- : require('uuid').v4());
384
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
389
385
 
390
386
  const qeDeviceId =
391
387
  this.client.state.deviceId ||
392
388
  this.client.state.qeDeviceId ||
393
- (crypto.randomUUID
394
- ? crypto.randomUUID()
395
- : require('uuid').v4());
389
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
396
390
 
397
391
  const waterfallId = crypto.randomUUID
398
392
  ? crypto.randomUUID()
@@ -403,23 +397,15 @@ class AccountRepository extends Repository {
403
397
  `${Date.now()}${Math.floor(Math.random() * 1000)}`
404
398
  );
405
399
 
406
- const aacInitTimestamp =
407
- nowSec - Math.floor(Math.random() * 50);
400
+ const aacInitTimestamp = nowSec - Math.floor(Math.random() * 50);
408
401
  const aacjid = crypto.randomUUID
409
402
  ? crypto.randomUUID()
410
403
  : require('uuid').v4();
411
- const aaccs = crypto
412
- .randomBytes(32)
413
- .toString('base64')
414
- .replace(/=/g, '');
404
+ const aaccs = crypto.randomBytes(32).toString('base64').replace(/=/g, '');
415
405
 
416
406
  const clientInputParams = {
417
407
  username_input: username,
418
- aac: JSON.stringify({
419
- aac_init_timestamp: aacInitTimestamp,
420
- aacjid,
421
- aaccs,
422
- }),
408
+ aac: JSON.stringify({ aac_init_timestamp: aacInitTimestamp, aacjid, aaccs }),
423
409
  lois_settings: { lois_token: '' },
424
410
  cloud_trust_token: null,
425
411
  zero_balance_state: '',
@@ -447,6 +433,7 @@ class AccountRepository extends Repository {
447
433
  server_params: serverParams,
448
434
  });
449
435
 
436
+ // FIX: bloksVersionId kept in sync with IG version from _resolveUserAgent
450
437
  const bloksVersionId =
451
438
  this.client.state.bloksVersionId ||
452
439
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
@@ -459,19 +446,19 @@ class AccountRepository extends Repository {
459
446
  const lang = this.client.state.language || 'ro_RO';
460
447
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
461
448
  const userAgent = this._resolveUserAgent();
462
-
463
449
  const timezoneOffset =
464
450
  typeof this.client.state.timezoneOffset === 'number'
465
451
  ? this.client.state.timezoneOffset
466
452
  : 7200;
467
453
 
454
+ // FIX: dynamic network properties
455
+ const networkProps = buildNetworkProperties();
456
+
468
457
  const bloksHeaders = {
469
458
  'accept-language': acceptLanguage,
470
- 'content-type':
471
- 'application/x-www-form-urlencoded; charset=UTF-8',
459
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
472
460
  'ig-intended-user-id': '0',
473
461
  priority: 'u=3',
474
-
475
462
  'x-bloks-is-layout-rtl': 'false',
476
463
  'x-bloks-prism-ax-base-colors-enabled': 'false',
477
464
  'x-bloks-prism-button-version': 'CONTROL',
@@ -479,53 +466,35 @@ class AccountRepository extends Repository {
479
466
  'x-bloks-prism-font-enabled': 'false',
480
467
  'x-bloks-prism-indigo-link-version': '0',
481
468
  'x-bloks-version-id': bloksVersionId,
482
-
483
469
  'x-ig-android-id': androidDeviceId,
484
470
  'x-ig-device-id': qeDeviceId,
485
471
  'x-ig-family-device-id': familyDeviceId,
486
472
  'x-ig-timezone-offset': String(timezoneOffset),
487
- 'x-ig-app-id': String(
488
- this.client.state.fbAnalyticsApplicationId ||
489
- '567067343352427'
490
- ),
473
+ 'x-ig-app-id': String(this.client.state.fbAnalyticsApplicationId || '567067343352427'),
491
474
  'x-ig-app-locale': lang,
492
475
  'x-ig-device-locale': lang,
493
476
  'x-ig-mapped-locale': lang,
494
-
495
- 'x-ig-client-endpoint':
496
- 'com.bloks.www.caa.login.login_homepage',
477
+ 'x-ig-client-endpoint': 'com.bloks.www.caa.login.login_homepage',
497
478
  'x-ig-nav-chain':
498
479
  'com.bloks.www.caa.login.login_homepage:com.bloks.www.caa.login.login_homepage:1:button:0:::0',
499
-
500
480
  'x-ig-connection-type': 'WIFI',
501
481
  'x-ig-capabilities': '3brTv10=',
502
482
  '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')}`,
483
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
509
484
  'x-pigeon-session-id':
510
485
  this.client.state.pigeonSessionId ||
511
486
  `UFS-${crypto.randomBytes(16).toString('hex')}-2`,
512
-
513
487
  'x-mid':
514
488
  this.client.state.mid ||
515
489
  `aZ${crypto.randomBytes(8).toString('hex')}`,
516
490
  'x-tigon-is-retry': 'False',
517
-
518
491
  'x-fb-client-ip': 'True',
519
492
  'x-fb-connection-type': 'WIFI',
520
493
  'x-fb-server-cluster': 'True',
521
- 'x-fb-network-properties':
522
- 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
494
+ 'x-fb-network-properties': networkProps, // FIX: dynamic
523
495
  'x-fb-request-analytics-tags': JSON.stringify({
524
496
  network_tags: {
525
- product: String(
526
- this.client.state.fbAnalyticsApplicationId ||
527
- '567067343352427'
528
- ),
497
+ product: String(this.client.state.fbAnalyticsApplicationId || '567067343352427'),
529
498
  purpose: 'fetch',
530
499
  surface: 'undefined',
531
500
  request_category: 'api',
@@ -536,7 +505,6 @@ class AccountRepository extends Repository {
536
505
  'IgApi: bloks/async_action/com.bloks.www.caa.login.oauth.token.fetch.async/',
537
506
  'x-fb-http-engine': 'MNS/TCP',
538
507
  'x-fb-rmd': 'state=URL_ELIGIBLE',
539
-
540
508
  'user-agent': userAgent,
541
509
  };
542
510
 
@@ -551,34 +519,12 @@ class AccountRepository extends Repository {
551
519
  headers: bloksHeaders,
552
520
  });
553
521
 
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) {}
522
+ debugWrite('oauth-token-debug.json', {
523
+ at: new Date().toISOString(),
524
+ statusCode: response.statusCode || response.status || null,
525
+ headers: response.headers || null,
526
+ body: response.body || null,
527
+ });
582
528
 
583
529
  return response.body;
584
530
  } catch (e) {
@@ -586,10 +532,6 @@ class AccountRepository extends Repository {
586
532
  }
587
533
  }
588
534
 
589
- /**
590
- * Step 1: launcher/mobileconfig to fetch mobile config + encryption keys.
591
- * Best effort – errors are ignored.
592
- */
593
535
  async _launcherMobileConfig(preLogin = true) {
594
536
  const nowSec = Math.floor(Date.now() / 1000);
595
537
  const state = this.client.state || {};
@@ -602,16 +544,12 @@ class AccountRepository extends Repository {
602
544
  const familyDeviceId =
603
545
  state.phoneId ||
604
546
  state.familyDeviceId ||
605
- (crypto.randomUUID
606
- ? crypto.randomUUID()
607
- : require('uuid').v4());
547
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
608
548
 
609
549
  const qeDeviceId =
610
550
  state.deviceId ||
611
551
  state.qeDeviceId ||
612
- (crypto.randomUUID
613
- ? crypto.randomUUID()
614
- : require('uuid').v4());
552
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
615
553
 
616
554
  const deviceUUID = state.uuid || qeDeviceId;
617
555
  const userId = state.cookieUserId || '0';
@@ -623,11 +561,12 @@ class AccountRepository extends Repository {
623
561
  const lang = state.language || 'ro_RO';
624
562
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
625
563
  const userAgent = this._resolveUserAgent();
626
-
627
564
  const timezoneOffset =
628
- typeof state.timezoneOffset === 'number'
629
- ? state.timezoneOffset
630
- : 7200;
565
+ typeof state.timezoneOffset === 'number' ? state.timezoneOffset : 7200;
566
+
567
+ // FIX: salt-ids from state or fallback
568
+ const saltIds = state.igSaltIds || '220140399,332020310,974466465,974460658';
569
+ const networkProps = buildNetworkProperties();
631
570
 
632
571
  const params = {
633
572
  bool_opt_policy: '0',
@@ -636,8 +575,7 @@ class AccountRepository extends Repository {
636
575
  client_context: '["opt,value_hash"]',
637
576
  unit_type: '2',
638
577
  use_case: 'STANDARD',
639
- query_hash:
640
- 'f00b9d0869db3969378d8d06bfccb24b5ef078012c8e199cba961cd5dfedaa88',
578
+ query_hash: 'f00b9d0869db3969378d8d06bfccb24b5ef078012c8e199cba961cd5dfedaa88',
641
579
  ts: String(nowSec),
642
580
  _uid: userId,
643
581
  device_id: deviceUUID,
@@ -648,10 +586,8 @@ class AccountRepository extends Repository {
648
586
 
649
587
  const headers = {
650
588
  'accept-language': acceptLanguage,
651
- 'content-type':
652
- 'application/x-www-form-urlencoded; charset=UTF-8',
589
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
653
590
  priority: 'u=3',
654
-
655
591
  'x-bloks-is-layout-rtl': 'false',
656
592
  'x-bloks-prism-ax-base-colors-enabled': 'false',
657
593
  'x-bloks-prism-button-version': 'CONTROL',
@@ -659,28 +595,19 @@ class AccountRepository extends Repository {
659
595
  'x-bloks-prism-font-enabled': 'false',
660
596
  'x-bloks-prism-indigo-link-version': '0',
661
597
  'x-bloks-version-id': bloksVersionId,
662
-
663
598
  'x-fb-client-ip': 'True',
664
599
  'x-fb-connection-type': 'WIFI',
665
600
  'x-fb-server-cluster': 'True',
666
- 'x-fb-network-properties':
667
- 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
601
+ 'x-fb-network-properties': networkProps, // FIX: dynamic
668
602
  'x-fb-http-engine': 'MNS/TCP',
669
603
  'x-fb-rmd': 'state=URL_ELIGIBLE',
670
-
671
604
  'x-ig-android-id': androidDeviceId,
672
- 'x-ig-app-id': String(
673
- state.fbAnalyticsApplicationId || '567067343352427'
674
- ),
605
+ 'x-ig-app-id': String(state.fbAnalyticsApplicationId || '567067343352427'),
675
606
  'x-ig-app-locale': lang,
676
- 'x-ig-bandwidth-speed-kbps': (
677
- Math.random() * 1500 +
678
- 800
679
- ).toFixed(3),
607
+ 'x-ig-bandwidth-speed-kbps': (Math.random() * 1500 + 800).toFixed(3),
680
608
  'x-ig-bandwidth-totalbytes-b': '0',
681
609
  'x-ig-bandwidth-totaltime-ms': '0',
682
- 'x-ig-client-endpoint':
683
- 'LockoutFragment:dogfooding_lockout',
610
+ 'x-ig-client-endpoint': 'LockoutFragment:dogfooding_lockout',
684
611
  'x-ig-capabilities': '3brTv10=',
685
612
  'x-ig-connection-type': 'WIFI',
686
613
  'x-ig-device-id': deviceUUID,
@@ -688,26 +615,19 @@ class AccountRepository extends Repository {
688
615
  'x-ig-device-locale': lang,
689
616
  'x-ig-family-device-id': familyDeviceId,
690
617
  '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',
618
+ 'x-ig-nav-chain': 'LockoutFragment:dogfooding_lockout:1:cold_start',
619
+ 'x-ig-salt-ids': saltIds, // FIX: from state or fallback
695
620
  'x-ig-timezone-offset': String(timezoneOffset),
696
621
  'x-ig-www-claim': state.igWWWClaim || '0',
697
622
  'x-mid':
698
623
  state.mid ||
699
624
  state.machineId ||
700
625
  `aZ${crypto.randomBytes(8).toString('hex')}`,
701
- 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(
702
- Math.random() * 1000
703
- )
704
- .toString()
705
- .padStart(3, '0')}`,
626
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
706
627
  'x-pigeon-session-id':
707
628
  state.pigeonSessionId ||
708
629
  `UFS-${crypto.randomBytes(16).toString('hex')}-M`,
709
630
  'x-tigon-is-retry': 'False',
710
-
711
631
  'accept-encoding': 'gzip, deflate, br',
712
632
  'user-agent': userAgent,
713
633
  };
@@ -716,12 +636,8 @@ class AccountRepository extends Repository {
716
636
  headers['ig-intended-user-id'] = String(state.cookieUserId);
717
637
  headers['ig-u-ds-user-id'] = String(state.cookieUserId);
718
638
  }
719
- if (state.igURur) {
720
- headers['ig-u-rur'] = state.igURur;
721
- }
722
- if (state.authorization) {
723
- headers['authorization'] = state.authorization;
724
- }
639
+ if (state.igURur) headers['ig-u-rur'] = state.igURur;
640
+ if (state.authorization) headers['authorization'] = state.authorization;
725
641
 
726
642
  try {
727
643
  const response = await this.client.request.send({
@@ -731,7 +647,6 @@ class AccountRepository extends Repository {
731
647
  headers,
732
648
  });
733
649
 
734
- // NEW: salvăm cheia de criptare a parolei direct din headers de la mobileconfig
735
650
  try {
736
651
  const mcHeaders = response.headers || {};
737
652
  const mcLower = {};
@@ -739,10 +654,8 @@ class AccountRepository extends Repository {
739
654
  mcLower[k.toLowerCase()] = mcHeaders[k];
740
655
  }
741
656
 
742
- const mcKeyIdStr =
743
- mcLower['ig-set-password-encryption-key-id'];
744
- const mcPubKey =
745
- mcLower['ig-set-password-encryption-pub-key'];
657
+ const mcKeyIdStr = mcLower['ig-set-password-encryption-key-id'];
658
+ const mcPubKey = mcLower['ig-set-password-encryption-pub-key'];
746
659
 
747
660
  if (mcKeyIdStr) {
748
661
  const parsedKeyId = parseInt(mcKeyIdStr, 10);
@@ -750,39 +663,15 @@ class AccountRepository extends Repository {
750
663
  state.passwordEncryptionKeyId = parsedKeyId;
751
664
  }
752
665
  }
753
-
754
- if (mcPubKey) {
755
- state.passwordEncryptionPubKey = mcPubKey;
756
- }
666
+ if (mcPubKey) state.passwordEncryptionPubKey = mcPubKey;
757
667
  } catch (e) {}
758
668
 
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) {}
669
+ debugWrite('launcher-mobileconfig-debug.json', {
670
+ at: new Date().toISOString(),
671
+ statusCode: response.statusCode || response.status || null,
672
+ headers: response.headers || null,
673
+ body: response.body || null,
674
+ });
786
675
 
787
676
  return response.body;
788
677
  } catch (e) {
@@ -790,10 +679,6 @@ class AccountRepository extends Repository {
790
679
  }
791
680
  }
792
681
 
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
682
  async _createAndroidKeystoreAttestation() {
798
683
  const state = this.client.state || {};
799
684
  const nowSec = Math.floor(Date.now() / 1000);
@@ -806,30 +691,25 @@ class AccountRepository extends Repository {
806
691
  const familyDeviceId =
807
692
  state.phoneId ||
808
693
  state.familyDeviceId ||
809
- (crypto.randomUUID
810
- ? crypto.randomUUID()
811
- : require('uuid').v4());
694
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
812
695
 
813
696
  const qeDeviceId =
814
697
  state.deviceId ||
815
698
  state.qeDeviceId ||
816
- (crypto.randomUUID
817
- ? crypto.randomUUID()
818
- : require('uuid').v4());
699
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
819
700
 
820
701
  const deviceUUID = state.uuid || qeDeviceId;
821
702
  const userId = state.cookieUserId || '0';
822
703
 
823
- const attestParams =
824
- AccountRepository.generateAttestParams(state);
704
+ // FIX: each cert in the chain has its own key pair
705
+ const attestParams = AccountRepository.generateAttestParams(state);
825
706
 
826
707
  const lang = state.language || 'ro_RO';
827
708
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
828
709
  const userAgent = this._resolveUserAgent();
829
710
  const timezoneOffset =
830
- typeof state.timezoneOffset === 'number'
831
- ? state.timezoneOffset
832
- : 7200;
711
+ typeof state.timezoneOffset === 'number' ? state.timezoneOffset : 7200;
712
+ const networkProps = buildNetworkProperties();
833
713
 
834
714
  const params = {
835
715
  _uid: userId,
@@ -842,29 +722,20 @@ class AccountRepository extends Repository {
842
722
 
843
723
  const headers = {
844
724
  'accept-language': acceptLanguage,
845
- 'content-type':
846
- 'application/x-www-form-urlencoded; charset=UTF-8',
725
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
847
726
  priority: 'u=3',
848
-
849
727
  'x-ig-android-id': androidDeviceId,
850
728
  'x-ig-device-id': qeDeviceId,
851
729
  'x-ig-family-device-id': familyDeviceId,
852
730
  'x-ig-timezone-offset': String(timezoneOffset),
853
- 'x-ig-app-id': String(
854
- state.fbAnalyticsApplicationId || '567067343352427'
855
- ),
731
+ 'x-ig-app-id': String(state.fbAnalyticsApplicationId || '567067343352427'),
856
732
  'x-ig-app-locale': lang,
857
733
  'x-ig-device-locale': lang,
858
734
  'x-ig-mapped-locale': lang,
859
735
  'x-ig-connection-type': 'WIFI',
860
736
  'x-ig-capabilities': '3brTv10=',
861
737
  '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')}`,
738
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
868
739
  'x-pigeon-session-id':
869
740
  state.pigeonSessionId ||
870
741
  `UFS-${crypto.randomBytes(16).toString('hex')}-A`,
@@ -872,28 +743,19 @@ class AccountRepository extends Repository {
872
743
  state.mid ||
873
744
  state.machineId ||
874
745
  `aZ${crypto.randomBytes(8).toString('hex')}`,
875
-
876
746
  'x-fb-client-ip': 'True',
877
747
  'x-fb-connection-type': 'WIFI',
878
748
  'x-fb-server-cluster': 'True',
879
749
  'x-fb-http-engine': 'MNS/TCP',
880
- 'x-fb-network-properties':
881
- 'VPN;Validated;LocalAddrs=/10.0.0.2,;',
750
+ 'x-fb-network-properties': networkProps, // FIX: dynamic
882
751
  'x-fb-rmd': 'state=URL_ELIGIBLE',
883
-
884
752
  'accept-encoding': 'gzip, deflate, br',
885
753
  'user-agent': userAgent,
886
754
  };
887
755
 
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
- }
756
+ if (state.cookieUserId) headers['ig-intended-user-id'] = String(state.cookieUserId);
757
+ if (state.authorization) headers['authorization'] = state.authorization;
758
+ if (state.igURur) headers['ig-u-rur'] = state.igURur;
897
759
 
898
760
  try {
899
761
  const response = await this.client.request.send({
@@ -903,33 +765,12 @@ class AccountRepository extends Repository {
903
765
  headers,
904
766
  });
905
767
 
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) {}
768
+ debugWrite('attestation-debug.json', {
769
+ at: new Date().toISOString(),
770
+ statusCode: response.statusCode || response.status || null,
771
+ headers: response.headers || null,
772
+ body: response.body || null,
773
+ });
933
774
 
934
775
  return response.body;
935
776
  } catch (e) {
@@ -937,11 +778,6 @@ class AccountRepository extends Repository {
937
778
  }
938
779
  }
939
780
 
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
781
  async _preloadTermsOfService() {
946
782
  const state = this.client.state || {};
947
783
  const lang = state.language || 'ro_RO';
@@ -952,29 +788,19 @@ class AccountRepository extends Repository {
952
788
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
953
789
 
954
790
  const paramsJson = JSON.stringify({});
955
- const bkClientContext = JSON.stringify({
956
- bloks_version: bloksVersionId,
957
- styles_id: 'instagram',
958
- });
959
-
791
+ const bkClientContext = JSON.stringify({ bloks_version: bloksVersionId, styles_id: 'instagram' });
960
792
  const headers = {
961
793
  'accept-language': acceptLanguage,
962
- 'content-type':
963
- 'application/x-www-form-urlencoded; charset=UTF-8',
794
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
964
795
  priority: 'u=3',
965
796
  'x-bloks-version-id': bloksVersionId,
966
797
  'user-agent': userAgent,
967
798
  };
968
-
969
799
  try {
970
800
  const response = await this.client.request.send({
971
801
  method: 'POST',
972
802
  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
- },
803
+ form: { params: paramsJson, bk_client_context: bkClientContext, bloks_versioning_id: bloksVersionId },
978
804
  headers,
979
805
  });
980
806
  return response.body;
@@ -992,32 +818,20 @@ class AccountRepository extends Repository {
992
818
  state.bloksVersionId ||
993
819
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
994
820
 
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
-
821
+ const paramsJson = JSON.stringify({ username_input: username || '' });
822
+ const bkClientContext = JSON.stringify({ bloks_version: bloksVersionId, styles_id: 'instagram' });
1003
823
  const headers = {
1004
824
  'accept-language': acceptLanguage,
1005
- 'content-type':
1006
- 'application/x-www-form-urlencoded; charset=UTF-8',
825
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
1007
826
  priority: 'u=3',
1008
827
  'x-bloks-version-id': bloksVersionId,
1009
828
  'user-agent': userAgent,
1010
829
  };
1011
-
1012
830
  try {
1013
831
  const response = await this.client.request.send({
1014
832
  method: 'POST',
1015
833
  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
- },
834
+ form: { params: paramsJson, bk_client_context: bkClientContext, bloks_versioning_id: bloksVersionId },
1021
835
  headers,
1022
836
  });
1023
837
  return response.body;
@@ -1036,29 +850,19 @@ class AccountRepository extends Repository {
1036
850
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
1037
851
 
1038
852
  const paramsJson = JSON.stringify({});
1039
- const bkClientContext = JSON.stringify({
1040
- bloks_version: bloksVersionId,
1041
- styles_id: 'instagram',
1042
- });
1043
-
853
+ const bkClientContext = JSON.stringify({ bloks_version: bloksVersionId, styles_id: 'instagram' });
1044
854
  const headers = {
1045
855
  'accept-language': acceptLanguage,
1046
- 'content-type':
1047
- 'application/x-www-form-urlencoded; charset=UTF-8',
856
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
1048
857
  priority: 'u=3',
1049
858
  'x-bloks-version-id': bloksVersionId,
1050
859
  'user-agent': userAgent,
1051
860
  };
1052
-
1053
861
  try {
1054
862
  const response = await this.client.request.send({
1055
863
  method: 'POST',
1056
864
  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
- },
865
+ form: { params: paramsJson, bk_client_context: bkClientContext, bloks_versioning_id: bloksVersionId },
1062
866
  headers,
1063
867
  });
1064
868
  return response.body;
@@ -1069,10 +873,7 @@ class AccountRepository extends Repository {
1069
873
 
1070
874
  async login(credentialsOrUsername, passwordArg) {
1071
875
  let username, password;
1072
- if (
1073
- typeof credentialsOrUsername === 'object' &&
1074
- credentialsOrUsername !== null
1075
- ) {
876
+ if (typeof credentialsOrUsername === 'object' && credentialsOrUsername !== null) {
1076
877
  username = credentialsOrUsername.username;
1077
878
  password = credentialsOrUsername.password;
1078
879
  } else {
@@ -1080,57 +881,37 @@ class AccountRepository extends Repository {
1080
881
  password = passwordArg;
1081
882
  }
1082
883
 
1083
- if (!username || !password) {
1084
- throw new Error('Username and password are required');
1085
- }
884
+ if (!username || !password) throw new Error('Username and password are required');
1086
885
 
1087
- // 0: ne asigurăm că avem csrf + cookies de bază
1088
886
  await this.ensureCsrfToken();
1089
887
 
1090
- // 1: încercăm luăm cheia de criptare a parolei din /launcher/mobileconfig/
1091
- try {
1092
- await this._launcherMobileConfig(true);
1093
- } catch (e) {}
888
+ try { await this._launcherMobileConfig(true); } catch (e) {}
1094
889
 
1095
- // Fallback: dacă mobileconfig nu a setat cheia, folosim mecanismul vechi de LOGIN_EXPERIMENTS
1096
890
  if (!this.client.state.passwordEncryptionPubKey) {
1097
891
  await this.syncLoginExperiments();
1098
892
  }
1099
893
 
1100
- // Acum parola este criptată CU cheia venită din mobileconfig (dacă a existat)
1101
- const { encrypted, time } = this.encryptPassword(password);
894
+ // FIX: if we still have no public key, throw error do not send password in plaintext
895
+ if (!this.client.state.passwordEncryptionPubKey) {
896
+ throw new Error(
897
+ 'Could not obtain the password encryption public key. ' +
898
+ 'Check that mobileconfig and syncLoginExperiments return valid data.'
899
+ );
900
+ }
1102
901
 
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) {}
902
+ const { encrypted, time } = this.encryptPassword(password);
1116
903
 
1117
- // Step 6: OAuth token fetch
1118
- try {
1119
- await this._prefetchOauthTokenForLogin(username);
1120
- } catch (e) {}
904
+ try { await this._createAndroidKeystoreAttestation(); } catch (e) {}
905
+ try { await this._preloadTermsOfService(); } catch (e) {}
906
+ try { await this._processClientDataAndRedirect(username); } catch (e) {}
907
+ try { await this._phoneNumberPrefill(); } catch (e) {}
908
+ try { await this._prefetchOauthTokenForLogin(username); } catch (e) {}
1121
909
 
1122
- // Step 7: real login
1123
- return this.requestWithRetry(async () => {
910
+ const loginResult = await this.requestWithRetry(async () => {
1124
911
  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, '');
912
+ const aacInitTimestamp = nowSec - Math.floor(Math.random() * 50);
913
+ const aacjid = crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4();
914
+ const aaccs = crypto.randomBytes(32).toString('base64').replace(/=/g, '');
1134
915
 
1135
916
  const androidDeviceId =
1136
917
  this.client.state.androidDeviceId ||
@@ -1143,18 +924,13 @@ class AccountRepository extends Repository {
1143
924
  const familyDeviceId =
1144
925
  this.client.state.phoneId ||
1145
926
  this.client.state.familyDeviceId ||
1146
- (crypto.randomUUID
1147
- ? crypto.randomUUID()
1148
- : require('uuid').v4());
927
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
1149
928
  const qeDeviceId =
1150
929
  this.client.state.deviceId ||
1151
930
  this.client.state.qeDeviceId ||
1152
- (crypto.randomUUID
1153
- ? crypto.randomUUID()
1154
- : require('uuid').v4());
931
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
1155
932
 
1156
933
  const accountsList = this.client.state.accountsList || [];
1157
-
1158
934
  const flashCallPermissionStatus = {
1159
935
  READ_PHONE_STATE: 'GRANTED',
1160
936
  READ_CALL_LOG: 'GRANTED',
@@ -1162,11 +938,7 @@ class AccountRepository extends Repository {
1162
938
  };
1163
939
 
1164
940
  const clientInputParams = {
1165
- aac: JSON.stringify({
1166
- aac_init_timestamp: aacInitTimestamp,
1167
- aacjid,
1168
- aaccs,
1169
- }),
941
+ aac: JSON.stringify({ aac_init_timestamp: aacInitTimestamp, aacjid, aaccs }),
1170
942
  sim_phones: [],
1171
943
  aymh_accounts: [],
1172
944
  network_bssid: null,
@@ -1194,8 +966,7 @@ class AccountRepository extends Repository {
1194
966
  machine_id: machineId,
1195
967
  flash_call_permission_status: flashCallPermissionStatus,
1196
968
  accounts_list: accountsList,
1197
- gms_incoming_call_retriever_eligibility:
1198
- 'client_not_supported',
969
+ gms_incoming_call_retriever_eligibility: 'client_not_supported',
1199
970
  family_device_id: familyDeviceId,
1200
971
  fb_ig_device_id: [],
1201
972
  device_emails: [],
@@ -1207,13 +978,9 @@ class AccountRepository extends Repository {
1207
978
  contact_point: username,
1208
979
  };
1209
980
 
1210
- const waterfallId = crypto.randomUUID
1211
- ? crypto.randomUUID()
1212
- : require('uuid').v4();
981
+ const waterfallId = crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4();
1213
982
  const latencyMarkerId = 36707139;
1214
- const latencyInstanceId = Number(
1215
- `${Date.now()}${Math.floor(Math.random() * 1000)}`
1216
- );
983
+ const latencyInstanceId = Number(`${Date.now()}${Math.floor(Math.random() * 1000)}`);
1217
984
 
1218
985
  const serverParams = {
1219
986
  should_trigger_override_login_2fa_action: 0,
@@ -1228,8 +995,7 @@ class AccountRepository extends Repository {
1228
995
  is_platform_login: 0,
1229
996
  INTERNAL__latency_qpl_marker_id: latencyMarkerId,
1230
997
  is_from_aymh: 0,
1231
- offline_experiment_group:
1232
- 'caa_iteration_v3_perf_ig_4',
998
+ offline_experiment_group: 'caa_iteration_v3_perf_ig_4',
1233
999
  is_from_landing_page: 0,
1234
1000
  left_nav_button_action: 'NONE',
1235
1001
  password_text_input_id: 'z0jejq:194',
@@ -1240,10 +1006,9 @@ class AccountRepository extends Repository {
1240
1006
  username_text_input_id: 'z0jejq:193',
1241
1007
  layered_homepage_experiment_group: null,
1242
1008
  device_id: androidDeviceId,
1243
- login_surface: 'switcher',
1009
+ login_surface: 'login_home',
1244
1010
  INTERNAL__latency_qpl_instance_id: latencyInstanceId,
1245
- reg_flow_source:
1246
- 'login_home_native_integration_point',
1011
+ reg_flow_source: 'aymh_multi_profiles_native_integration_point',
1247
1012
  is_caa_perf_enabled: 1,
1248
1013
  credential_type: 'password',
1249
1014
  is_from_password_entry_page: 0,
@@ -1263,22 +1028,17 @@ class AccountRepository extends Repository {
1263
1028
  this.client.state.bloksVersionId ||
1264
1029
  '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
1265
1030
 
1266
- const bkClientContext = JSON.stringify({
1267
- bloks_version: bloksVersionId,
1268
- styles_id: 'instagram',
1269
- });
1270
-
1031
+ const bkClientContext = JSON.stringify({ bloks_version: bloksVersionId, styles_id: 'instagram' });
1271
1032
  const lang = this.client.state.language || 'ro_RO';
1272
1033
  const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
1273
1034
  const userAgent = this._resolveUserAgent();
1035
+ const networkProps = buildNetworkProperties();
1274
1036
 
1275
1037
  const bloksHeaders = {
1276
1038
  'accept-language': acceptLanguage,
1277
- 'content-type':
1278
- 'application/x-www-form-urlencoded; charset=UTF-8',
1039
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
1279
1040
  'ig-intended-user-id': '0',
1280
1041
  priority: 'u=3',
1281
-
1282
1042
  'x-bloks-is-layout-rtl': 'false',
1283
1043
  'x-bloks-prism-ax-base-colors-enabled': 'false',
1284
1044
  'x-bloks-prism-button-version': 'CONTROL',
@@ -1286,19 +1046,14 @@ class AccountRepository extends Repository {
1286
1046
  'x-bloks-prism-font-enabled': 'false',
1287
1047
  'x-bloks-prism-indigo-link-version': '0',
1288
1048
  'x-bloks-version-id': bloksVersionId,
1289
-
1290
1049
  'x-fb-client-ip': 'True',
1291
- 'x-fb-connection-type': 'WIFI',
1050
+ 'x-fb-connection-type': 'MOBILE.UNKNOWN',
1292
1051
  'x-fb-friendly-name':
1293
1052
  '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,;',
1053
+ 'x-fb-network-properties': networkProps, // FIX: dynamic
1296
1054
  'x-fb-request-analytics-tags': JSON.stringify({
1297
1055
  network_tags: {
1298
- product: String(
1299
- this.client.state.fbAnalyticsApplicationId ||
1300
- '567067343352427'
1301
- ),
1056
+ product: String(this.client.state.fbAnalyticsApplicationId || '567067343352427'),
1302
1057
  purpose: 'fetch',
1303
1058
  surface: 'undefined',
1304
1059
  request_category: 'api',
@@ -1306,23 +1061,16 @@ class AccountRepository extends Repository {
1306
1061
  },
1307
1062
  }),
1308
1063
  'x-fb-server-cluster': 'True',
1309
-
1310
1064
  'x-ig-android-id': androidDeviceId,
1311
- 'x-ig-app-id': String(
1312
- this.client.state.fbAnalyticsApplicationId ||
1313
- '567067343352427'
1314
- ),
1065
+ 'x-ig-app-id': String(this.client.state.fbAnalyticsApplicationId || '567067343352427'),
1315
1066
  'x-ig-app-locale': lang,
1316
- 'x-ig-bandwidth-speed-kbps': (
1317
- Math.random() * 1500 +
1318
- 800
1319
- ).toFixed(3),
1067
+ 'x-ig-bandwidth-speed-kbps': (Math.random() * 1500 + 800).toFixed(3),
1320
1068
  'x-ig-bandwidth-totalbytes-b': '0',
1321
1069
  'x-ig-bandwidth-totaltime-ms': '0',
1322
1070
  'x-ig-client-endpoint':
1323
1071
  'com.bloks.www.caa.login.aymh_single_profile_screen_entry',
1324
1072
  'x-ig-capabilities': '3brTv10=',
1325
- 'x-ig-connection-type': 'WIFI',
1073
+ 'x-ig-connection-type': 'MOBILE(UNKNOWN)',
1326
1074
  'x-ig-device-id': qeDeviceId,
1327
1075
  'x-ig-device-locale': lang,
1328
1076
  'x-ig-family-device-id': familyDeviceId,
@@ -1330,29 +1078,20 @@ class AccountRepository extends Repository {
1330
1078
  'x-ig-nav-chain':
1331
1079
  '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
1080
  'x-ig-timezone-offset': String(
1333
- typeof this.client.state.timezoneOffset ===
1334
- 'number'
1081
+ typeof this.client.state.timezoneOffset === 'number'
1335
1082
  ? this.client.state.timezoneOffset
1336
1083
  : 7200
1337
1084
  ),
1338
1085
  'x-ig-www-claim': this.client.state.igWWWClaim || '0',
1339
1086
  'x-mid': machineId,
1340
- 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(
1341
- Math.random() * 1000
1342
- )
1343
- .toString()
1344
- .padStart(3, '0')}`,
1087
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
1345
1088
  'x-pigeon-session-id':
1346
1089
  this.client.state.pigeonSessionId ||
1347
- `UFS-${crypto.randomBytes(16).toString(
1348
- 'hex'
1349
- )}-1`,
1090
+ `UFS-${crypto.randomBytes(16).toString('hex')}-1`,
1350
1091
  'x-tigon-is-retry': 'False',
1351
-
1352
- 'accept-encoding': 'gzip, deflate, br',
1092
+ 'accept-encoding': 'gzip, deflate',
1353
1093
  'user-agent': userAgent,
1354
- 'x-fb-conn-uuid-client':
1355
- crypto.randomBytes(16).toString('hex'),
1094
+ 'x-fb-conn-uuid-client': crypto.randomBytes(16).toString('hex'),
1356
1095
  'x-fb-http-engine': 'MNS/TCP',
1357
1096
  'x-fb-rmd': 'state=URL_ELIGIBLE',
1358
1097
  };
@@ -1368,44 +1107,18 @@ class AccountRepository extends Repository {
1368
1107
  headers: bloksHeaders,
1369
1108
  });
1370
1109
 
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) {}
1110
+ debugWrite('login-debug.json', {
1111
+ at: new Date().toISOString(),
1112
+ statusCode: response.statusCode || response.status || null,
1113
+ headers: response.headers || null,
1114
+ body: response.body || null,
1115
+ });
1400
1116
 
1401
1117
  const body = response.body;
1402
-
1403
1118
  this._extractAndSaveAuthorization(response);
1404
1119
 
1405
1120
  if (body && body.two_factor_required) {
1406
- const err = new Error(
1407
- 'Two factor authentication required'
1408
- );
1121
+ const err = new Error('Two factor authentication required');
1409
1122
  err.name = 'IgLoginTwoFactorRequiredError';
1410
1123
  err.twoFactorInfo = body.two_factor_info;
1411
1124
  throw err;
@@ -1429,42 +1142,26 @@ class AccountRepository extends Repository {
1429
1142
 
1430
1143
  if (body && body.layout) {
1431
1144
  const layoutStr =
1432
- typeof body.layout === 'string'
1433
- ? body.layout
1434
- : JSON.stringify(body.layout);
1145
+ typeof body.layout === 'string' ? body.layout : JSON.stringify(body.layout);
1435
1146
 
1436
- if (
1437
- layoutStr.includes('two_factor_required') ||
1438
- layoutStr.includes('"two_factor_info"')
1439
- ) {
1147
+ if (layoutStr.includes('two_factor_required') || layoutStr.includes('"two_factor_info"')) {
1440
1148
  let twoFactorInfo = null;
1441
1149
  try {
1442
- const match = layoutStr.match(
1443
- /"two_factor_info"\s*:\s*(\{[^}]+\})/
1444
- );
1445
- if (match)
1446
- twoFactorInfo = JSON.parse(match[1]);
1150
+ const match = layoutStr.match(/"two_factor_info"\s*:\s*(\{[^}]+\})/);
1151
+ if (match) twoFactorInfo = JSON.parse(match[1]);
1447
1152
  } catch (e) {}
1448
- const err = new Error(
1449
- 'Two factor authentication required'
1450
- );
1153
+ const err = new Error('Two factor authentication required');
1451
1154
  err.name = 'IgLoginTwoFactorRequiredError';
1452
1155
  err.twoFactorInfo = twoFactorInfo;
1453
1156
  throw err;
1454
1157
  }
1455
1158
 
1456
- if (
1457
- layoutStr.includes('bad_password') ||
1458
- layoutStr.includes('incorrect_password')
1459
- ) {
1159
+ if (layoutStr.includes('bad_password') || layoutStr.includes('incorrect_password')) {
1460
1160
  const err = new Error('Bad password');
1461
1161
  err.name = 'IgLoginBadPasswordError';
1462
1162
  throw err;
1463
1163
  }
1464
- if (
1465
- layoutStr.includes('invalid_user') ||
1466
- layoutStr.includes('user_not_found')
1467
- ) {
1164
+ if (layoutStr.includes('invalid_user') || layoutStr.includes('user_not_found')) {
1468
1165
  const err = new Error('Invalid user');
1469
1166
  err.name = 'IgLoginInvalidUserError';
1470
1167
  throw err;
@@ -1476,14 +1173,10 @@ class AccountRepository extends Repository {
1476
1173
  throw err;
1477
1174
  }
1478
1175
 
1479
- const tokenFromLayout =
1480
- this._findBearerInString(layoutStr);
1176
+ const tokenFromLayout = this._findBearerInString(layoutStr);
1481
1177
  if (tokenFromLayout) {
1482
- this.client.state.authorization =
1483
- tokenFromLayout;
1484
- try {
1485
- this.client.state.updateAuthorization();
1486
- } catch (e) {}
1178
+ this.client.state.authorization = tokenFromLayout;
1179
+ try { this.client.state.updateAuthorization(); } catch (e) {}
1487
1180
  }
1488
1181
 
1489
1182
  const pkPatterns = [
@@ -1499,8 +1192,7 @@ class AccountRepository extends Repository {
1499
1192
  const pkMatch = layoutStr.match(pattern);
1500
1193
  if (pkMatch) {
1501
1194
  extractedPk = pkMatch[1];
1502
- this.client.state.cookieUserId =
1503
- extractedPk;
1195
+ this.client.state.cookieUserId = extractedPk;
1504
1196
  this.client.state._userId = extractedPk;
1505
1197
  break;
1506
1198
  }
@@ -1513,89 +1205,54 @@ class AccountRepository extends Repository {
1513
1205
  /x-ig-set-www-claim[\\"\s:]+([a-f0-9]+)/i,
1514
1206
  ];
1515
1207
  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
- }
1208
+ const m = layoutStr.match(pattern);
1209
+ if (m) { this.client.state.igWWWClaim = m[1]; break; }
1522
1210
  }
1523
1211
 
1524
1212
  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
- );
1213
+ layoutStr.match(/IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i) ||
1214
+ layoutStr.match(/password.encryption.key.id[\\"\s:]+(\d+)/i);
1531
1215
  if (encKeyIdMatch2) {
1532
- this.client.state.passwordEncryptionKeyId =
1533
- parseInt(encKeyIdMatch2[1]);
1216
+ this.client.state.passwordEncryptionKeyId = parseInt(encKeyIdMatch2[1]);
1534
1217
  }
1535
1218
 
1536
1219
  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
- );
1220
+ layoutStr.match(/IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i) ||
1221
+ layoutStr.match(/password.encryption.pub.key[\\"\s:]+([A-Za-z0-9+\/=]+)/i);
1543
1222
  if (encPubKeyMatch2) {
1544
- this.client.state.passwordEncryptionPubKey =
1545
- encPubKeyMatch2[1];
1223
+ this.client.state.passwordEncryptionPubKey = encPubKeyMatch2[1];
1546
1224
  }
1547
1225
 
1548
1226
  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
- }
1227
+ layoutStr.match(/ig-set-x-mid[\\"\s:]+([A-Za-z0-9+\/=_-]+)/i) ||
1228
+ layoutStr.match(/\\"x-mid\\"[\\"\s:]+([A-Za-z0-9+\/=_-]+)/i);
1229
+ if (midMatch2) this.client.state.mid = midMatch2[1];
1558
1230
 
1559
1231
  const loginResponsePatterns = [
1560
- /\\"logged_in_user\\":\{[^}]*\\"pk\\":(\d+)[^}]*\\"username\\":\\"([^"\\]+)\\"/,
1232
+ /\\"logged_in_user\\":\{[^}]*\\"pk\\":(\\d+)[^}]*\\"username\\":\\"([^"\\]+)\\"/,
1561
1233
  /"logged_in_user":\{[^}]*"pk":(\d+)[^}]*"username":"([^"]+)"/,
1562
1234
  /\\"logged_in_user\\".*?\\"pk\\":\s*(\d+).*?\\"username\\":\s*\\"([^"\\]+)\\"/s,
1563
1235
  ];
1564
1236
  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
- };
1237
+ const m = layoutStr.match(pattern);
1238
+ if (m) {
1239
+ this.client.state.cookieUserId = m[1];
1240
+ this.client.state._userId = m[1];
1241
+ return { pk: parseInt(m[1]), pk_id: m[1], username: m[2] };
1577
1242
  }
1578
1243
  }
1579
1244
 
1580
- if (
1581
- extractedPk &&
1582
- this.client.state.authorization
1583
- ) {
1245
+ if (extractedPk && this.client.state.authorization) {
1584
1246
  try {
1585
1247
  const userInfo = await this.currentUser();
1586
1248
  const user = userInfo.user || userInfo;
1587
1249
  if (user && user.pk) {
1588
- this.client.state.cookieUserId =
1589
- String(user.pk);
1590
- this.client.state._userId =
1591
- String(user.pk);
1250
+ this.client.state.cookieUserId = String(user.pk);
1251
+ this.client.state._userId = String(user.pk);
1592
1252
  }
1593
1253
  return user;
1594
1254
  } catch (e) {
1595
- return {
1596
- pk: parseInt(extractedPk),
1597
- pk_id: extractedPk,
1598
- };
1255
+ return { pk: parseInt(extractedPk), pk_id: extractedPk };
1599
1256
  }
1600
1257
  }
1601
1258
  }
@@ -1615,8 +1272,7 @@ class AccountRepository extends Repository {
1615
1272
  const userInfo = await this.currentUser();
1616
1273
  const user = userInfo.user || userInfo;
1617
1274
  if (user && user.pk) {
1618
- this.client.state.cookieUserId =
1619
- String(user.pk);
1275
+ this.client.state.cookieUserId = String(user.pk);
1620
1276
  this.client.state._userId = String(user.pk);
1621
1277
  }
1622
1278
  return user;
@@ -1627,14 +1283,129 @@ class AccountRepository extends Repository {
1627
1283
 
1628
1284
  return body;
1629
1285
  });
1286
+
1287
+ // Post-login: set E2EE eligibility (as real app does after login)
1288
+ try { await this._setE2eeEligibility(); } catch (e) {}
1289
+
1290
+ return loginResult;
1630
1291
  }
1631
1292
 
1632
- async twoFactorLogin(
1633
- username,
1634
- verificationCode,
1635
- twoFactorIdentifier,
1636
- verificationMethod = '1'
1637
- ) {
1293
+ async _setE2eeEligibility(eligibility = 4) {
1294
+ const state = this.client.state || {};
1295
+ const lang = state.language || 'ro_RO';
1296
+ const acceptLanguage = `${lang.replace('_', '-')}, en-US`;
1297
+ const userAgent = this._resolveUserAgent();
1298
+ const bloksVersionId =
1299
+ state.bloksVersionId ||
1300
+ '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b';
1301
+ const networkProps = buildNetworkProperties();
1302
+ const nowSec = Math.floor(Date.now() / 1000);
1303
+
1304
+ const androidDeviceId =
1305
+ state.androidDeviceId ||
1306
+ state.deviceId ||
1307
+ `android-${crypto.randomBytes(8).toString('hex')}`;
1308
+ const familyDeviceId =
1309
+ state.phoneId ||
1310
+ state.familyDeviceId ||
1311
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
1312
+ const qeDeviceId =
1313
+ state.deviceId ||
1314
+ state.qeDeviceId ||
1315
+ (crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4());
1316
+ const machineId =
1317
+ state.mid ||
1318
+ state.machineId ||
1319
+ `aZ${crypto.randomBytes(8).toString('hex')}`;
1320
+
1321
+ const headers = {
1322
+ 'accept-language': acceptLanguage,
1323
+ 'authorization': state.authorization || '',
1324
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
1325
+ 'ig-intended-user-id': String(state.cookieUserId || '0'),
1326
+ 'ig-u-ds-user-id': String(state.cookieUserId || '0'),
1327
+ 'priority': 'u=3',
1328
+ 'x-bloks-is-layout-rtl': 'false',
1329
+ 'x-bloks-prism-ax-base-colors-enabled': 'false',
1330
+ 'x-bloks-prism-button-version': 'CONTROL',
1331
+ 'x-bloks-prism-colors-enabled': 'true',
1332
+ 'x-bloks-prism-font-enabled': 'false',
1333
+ 'x-bloks-prism-indigo-link-version': '0',
1334
+ 'x-bloks-version-id': bloksVersionId,
1335
+ 'x-fb-client-ip': 'True',
1336
+ 'x-fb-connection-type': 'MOBILE.UNKNOWN',
1337
+ 'x-fb-friendly-name': 'IgApi: direct_v2/set_e2ee_eligibility/',
1338
+ 'x-fb-network-properties': networkProps,
1339
+ 'x-fb-request-analytics-tags': JSON.stringify({
1340
+ network_tags: {
1341
+ product: String(state.fbAnalyticsApplicationId || '567067343352427'),
1342
+ purpose: 'fetch',
1343
+ surface: 'undefined',
1344
+ request_category: 'api',
1345
+ retry_attempt: '0',
1346
+ },
1347
+ }),
1348
+ 'x-fb-server-cluster': 'True',
1349
+ 'x-ig-android-id': androidDeviceId,
1350
+ 'x-ig-app-id': String(state.fbAnalyticsApplicationId || '567067343352427'),
1351
+ 'x-ig-app-locale': lang,
1352
+ 'x-ig-bandwidth-speed-kbps': (Math.random() * 1500 + 800).toFixed(3),
1353
+ 'x-ig-bandwidth-totalbytes-b': '0',
1354
+ 'x-ig-bandwidth-totaltime-ms': '0',
1355
+ 'x-ig-capabilities': '3brTv10=',
1356
+ 'x-ig-client-endpoint': 'empty',
1357
+ 'x-ig-connection-type': 'MOBILE(UNKNOWN)',
1358
+ 'x-ig-device-id': qeDeviceId,
1359
+ 'x-ig-device-languages': `{"system_languages":"${lang}"}`,
1360
+ 'x-ig-device-locale': lang,
1361
+ 'x-ig-family-device-id': familyDeviceId,
1362
+ 'x-ig-mapped-locale': lang,
1363
+ 'x-ig-nav-chain': 'LockoutFragment:dogfooding_lockout:1:cold_start',
1364
+ 'x-ig-salt-ids': state.igSaltIds || '220140399,332020310,974466465,974460658',
1365
+ 'x-ig-timezone-offset': String(
1366
+ typeof state.timezoneOffset === 'number' ? state.timezoneOffset : 7200
1367
+ ),
1368
+ 'x-ig-www-claim': state.igWWWClaim || '0',
1369
+ 'x-mid': machineId,
1370
+ 'x-pigeon-rawclienttime': `${nowSec}.${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
1371
+ 'x-pigeon-session-id':
1372
+ state.pigeonSessionId ||
1373
+ `UFS-${crypto.randomBytes(16).toString('hex')}-3`,
1374
+ 'x-tigon-is-retry': 'False',
1375
+ 'accept-encoding': 'gzip, deflate',
1376
+ 'user-agent': userAgent,
1377
+ 'x-fb-conn-uuid-client': crypto.randomBytes(16).toString('hex'),
1378
+ 'x-fb-http-engine': 'MNS/TCP',
1379
+ 'x-fb-rmd': 'state=URL_ELIGIBLE',
1380
+ };
1381
+
1382
+ if (state.igURur) headers['ig-u-rur'] = state.igURur;
1383
+
1384
+ try {
1385
+ const response = await this.client.request.send({
1386
+ method: 'POST',
1387
+ url: '/api/v1/direct_v2/set_e2ee_eligibility/',
1388
+ form: {
1389
+ _uuid: qeDeviceId,
1390
+ e2ee_eligibility: String(eligibility),
1391
+ },
1392
+ headers,
1393
+ });
1394
+
1395
+ debugWrite('e2ee-eligibility-debug.json', {
1396
+ at: new Date().toISOString(),
1397
+ statusCode: response.statusCode || response.status || null,
1398
+ headers: response.headers || null,
1399
+ body: response.body || null,
1400
+ });
1401
+
1402
+ return response.body;
1403
+ } catch (e) {
1404
+ return null;
1405
+ }
1406
+ }
1407
+
1408
+ async twoFactorLogin(username, verificationCode, twoFactorIdentifier, verificationMethod = '1') {
1638
1409
  return this.requestWithRetry(async () => {
1639
1410
  const response = await this.client.request.send({
1640
1411
  method: 'POST',
@@ -1660,9 +1431,7 @@ class AccountRepository extends Repository {
1660
1431
  const response = await this.client.request.send({
1661
1432
  method: 'POST',
1662
1433
  url: '/api/v1/accounts/logout/',
1663
- form: this.client.request.sign({
1664
- _uuid: this.client.state.uuid,
1665
- }),
1434
+ form: this.client.request.sign({ _uuid: this.client.state.uuid }),
1666
1435
  });
1667
1436
  return response.body;
1668
1437
  });
@@ -1683,14 +1452,7 @@ class AccountRepository extends Repository {
1683
1452
  return this.currentUser();
1684
1453
  }
1685
1454
 
1686
- async editProfile({
1687
- externalUrl,
1688
- phoneNumber,
1689
- username,
1690
- fullName,
1691
- biography,
1692
- email,
1693
- } = {}) {
1455
+ async editProfile({ externalUrl, phoneNumber, username, fullName, biography, email } = {}) {
1694
1456
  return this.requestWithRetry(async () => {
1695
1457
  const current = await this.currentUser();
1696
1458
  const user = current.user || current;
@@ -1699,57 +1461,28 @@ class AccountRepository extends Repository {
1699
1461
  url: '/api/v1/accounts/edit_profile/',
1700
1462
  form: this.client.request.sign({
1701
1463
  _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 || '',
1464
+ external_url: externalUrl !== undefined ? externalUrl : user.external_url || '',
1465
+ phone_number: phoneNumber !== undefined ? phoneNumber : user.phone_number || '',
1466
+ username: username !== undefined ? username : user.username,
1467
+ full_name: fullName !== undefined ? fullName : user.full_name || '',
1468
+ biography: biography !== undefined ? biography : user.biography || '',
1469
+ email: email !== undefined ? email : user.email || '',
1726
1470
  }),
1727
1471
  });
1728
1472
  return response.body;
1729
1473
  });
1730
1474
  }
1731
1475
 
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
- }
1476
+ async setBiography(biography) { return this.editProfile({ biography }); }
1477
+ async setExternalUrl(url) { return this.editProfile({ externalUrl: url }); }
1478
+ async removeBioLinks() { return this.editProfile({ externalUrl: '' }); }
1743
1479
 
1744
1480
  async setGender(gender) {
1745
1481
  return this.requestWithRetry(async () => {
1746
1482
  const response = await this.client.request.send({
1747
1483
  method: 'POST',
1748
1484
  url: '/api/v1/accounts/set_gender/',
1749
- form: this.client.request.sign({
1750
- _uuid: this.client.state.uuid,
1751
- gender,
1752
- }),
1485
+ form: this.client.request.sign({ _uuid: this.client.state.uuid, gender }),
1753
1486
  });
1754
1487
  return response.body;
1755
1488
  });
@@ -1760,9 +1493,7 @@ class AccountRepository extends Repository {
1760
1493
  const response = await this.client.request.send({
1761
1494
  method: 'POST',
1762
1495
  url: '/api/v1/accounts/set_private/',
1763
- form: this.client.request.sign({
1764
- _uuid: this.client.state.uuid,
1765
- }),
1496
+ form: this.client.request.sign({ _uuid: this.client.state.uuid }),
1766
1497
  });
1767
1498
  return response.body;
1768
1499
  });
@@ -1773,9 +1504,7 @@ class AccountRepository extends Repository {
1773
1504
  const response = await this.client.request.send({
1774
1505
  method: 'POST',
1775
1506
  url: '/api/v1/accounts/set_public/',
1776
- form: this.client.request.sign({
1777
- _uuid: this.client.state.uuid,
1778
- }),
1507
+ form: this.client.request.sign({ _uuid: this.client.state.uuid }),
1779
1508
  });
1780
1509
  return response.body;
1781
1510
  });
@@ -1804,10 +1533,7 @@ class AccountRepository extends Repository {
1804
1533
  const response = await this.client.request.send({
1805
1534
  method: 'POST',
1806
1535
  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
- }),
1536
+ form: this.client.request.sign({ _uuid: this.client.state.uuid, send_source: 'edit_profile' }),
1811
1537
  });
1812
1538
  return response.body;
1813
1539
  });
@@ -1818,10 +1544,7 @@ class AccountRepository extends Repository {
1818
1544
  const response = await this.client.request.send({
1819
1545
  method: 'POST',
1820
1546
  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
- }),
1547
+ form: this.client.request.sign({ _uuid: this.client.state.uuid, phone_number: phoneNumber }),
1825
1548
  });
1826
1549
  return response.body;
1827
1550
  });
@@ -1847,9 +1570,7 @@ class AccountRepository extends Repository {
1847
1570
  const response = await this.client.request.send({
1848
1571
  method: 'POST',
1849
1572
  url: '/api/v1/accounts/remove_profile_picture/',
1850
- form: this.client.request.sign({
1851
- _uuid: this.client.state.uuid,
1852
- }),
1573
+ form: this.client.request.sign({ _uuid: this.client.state.uuid }),
1853
1574
  });
1854
1575
  return response.body;
1855
1576
  });
@@ -1873,9 +1594,7 @@ class AccountRepository extends Repository {
1873
1594
  form: this.client.request.sign({
1874
1595
  id: this.client.state.uuid,
1875
1596
  server_config_retrieval: '1',
1876
- experiments:
1877
- this.client.state.constants
1878
- .LOGIN_EXPERIMENTS,
1597
+ experiments: this.client.state.constants.LOGIN_EXPERIMENTS,
1879
1598
  }),
1880
1599
  });
1881
1600
  return response.body;
@@ -1884,12 +1603,7 @@ class AccountRepository extends Repository {
1884
1603
 
1885
1604
  async syncPostLoginExperiments() {
1886
1605
  let userId;
1887
- try {
1888
- userId = this.client.state.cookieUserId;
1889
- } catch {
1890
- userId = '0';
1891
- }
1892
-
1606
+ try { userId = this.client.state.cookieUserId; } catch { userId = '0'; }
1893
1607
  return this.requestWithRetry(async () => {
1894
1608
  const response = await this.client.request.send({
1895
1609
  method: 'POST',
@@ -1899,8 +1613,7 @@ class AccountRepository extends Repository {
1899
1613
  _uid: userId,
1900
1614
  _uuid: this.client.state.uuid,
1901
1615
  server_config_retrieval: '1',
1902
- experiments:
1903
- this.client.state.constants.EXPERIMENTS,
1616
+ experiments: this.client.state.constants.EXPERIMENTS,
1904
1617
  }),
1905
1618
  });
1906
1619
  return response.body;
@@ -1908,17 +1621,13 @@ class AccountRepository extends Repository {
1908
1621
  }
1909
1622
 
1910
1623
  async syncLauncher(preLogin = true) {
1911
- const data = {
1912
- id: this.client.state.uuid,
1913
- server_config_retrieval: '1',
1914
- };
1624
+ const data = { id: this.client.state.uuid, server_config_retrieval: '1' };
1915
1625
  if (!preLogin) {
1916
1626
  try {
1917
1627
  data._uid = this.client.state.cookieUserId;
1918
1628
  data._uuid = this.client.state.uuid;
1919
1629
  } catch {}
1920
1630
  }
1921
-
1922
1631
  return this.requestWithRetry(async () => {
1923
1632
  const response = await this.client.request.send({
1924
1633
  method: 'POST',
@@ -1934,10 +1643,7 @@ class AccountRepository extends Repository {
1934
1643
  const response = await this.client.request.send({
1935
1644
  method: 'POST',
1936
1645
  url: '/api/v1/devices/sync/',
1937
- form: this.client.request.sign({
1938
- id: this.client.state.uuid,
1939
- server_config_retrieval: '1',
1940
- }),
1646
+ form: this.client.request.sign({ id: this.client.state.uuid, server_config_retrieval: '1' }),
1941
1647
  });
1942
1648
  return response.body;
1943
1649
  });
@@ -1964,10 +1670,7 @@ class AccountRepository extends Repository {
1964
1670
  const response = await this.client.request.send({
1965
1671
  method: 'POST',
1966
1672
  url: '/api/v1/accounts/contact_point_prefill/',
1967
- form: this.client.request.sign({
1968
- phone_id: this.client.state.phoneId,
1969
- usage,
1970
- }),
1673
+ form: this.client.request.sign({ phone_id: this.client.state.phoneId, usage }),
1971
1674
  });
1972
1675
  return response.body;
1973
1676
  });
@@ -1995,10 +1698,7 @@ class AccountRepository extends Repository {
1995
1698
  const response = await this.client.request.send({
1996
1699
  method: 'GET',
1997
1700
  url: '/api/v1/consent/get_signup_config/',
1998
- qs: {
1999
- guid: this.client.state.uuid,
2000
- main_account_selected: false,
2001
- },
1701
+ qs: { guid: this.client.state.uuid, main_account_selected: false },
2002
1702
  });
2003
1703
  return response.body;
2004
1704
  });
@@ -2036,91 +1736,70 @@ class AccountRepository extends Repository {
2036
1736
  });
2037
1737
  }
2038
1738
 
1739
+ /**
1740
+ * FIX MAJOR — generateAttestParams: each certificate in the chain
1741
+ * now has its own EC key pair, correctly simulating the
1742
+ * Android Keystore hierarchy (leaf → intermediate1 → intermediate2 → root).
1743
+ */
2039
1744
  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
- }
1745
+ // Key for leaf (the actually attested key)
1746
+ const leafKey = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
1747
+
1748
+ const challengeNonce = crypto.randomBytes(24).toString('base64url');
1749
+ const signedData = crypto.sign(null, Buffer.from(challengeNonce), leafKey.privateKey);
1750
+ const signedNonce = signedData.toString('base64');
1751
+ const publicKeyDer = leafKey.publicKey.export({ type: 'spki', format: 'der' });
1752
+ const keyHash = crypto.createHash('sha256').update(publicKeyDer).digest('hex');
1753
+
1754
+ // Separate keys for each level in the chain
1755
+ const int1Key = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
1756
+ const int2Key = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
1757
+ const rootKey = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
1758
+
1759
+ const leafCertPem = AccountRepository._generateSignedCert(
1760
+ leafKey.privateKey, leafKey.publicKey, 'Android Keystore Key',
1761
+ int1Key.privateKey // signed by intermediate 1
2048
1762
  );
2049
- const signedData = crypto.sign(
2050
- null,
2051
- Buffer.from(challengeNonce),
2052
- privateKey
1763
+ const intermediate1Pem = AccountRepository._generateSignedCert(
1764
+ int1Key.privateKey, int1Key.publicKey, 'Android Keystore Key Attestation',
1765
+ int2Key.privateKey // signed by intermediate 2
1766
+ );
1767
+ const intermediate2Pem = AccountRepository._generateSignedCert(
1768
+ int2Key.privateKey, int2Key.publicKey, 'Android Hardware Keystore',
1769
+ rootKey.privateKey // self-signed root
1770
+ );
1771
+ const rootCertPem = AccountRepository._generateSignedCert(
1772
+ rootKey.privateKey, rootKey.publicKey, 'Android Keystore Root',
1773
+ rootKey.privateKey // self-signed root
2053
1774
  );
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
1775
 
2089
- const certificateChain = [
2090
- leafCertPem,
2091
- intermediate1Pem,
2092
- intermediate2Pem,
2093
- rootCertPem,
2094
- ].join('\n');
1776
+ const certificateChain = [leafCertPem, intermediate1Pem, intermediate2Pem, rootCertPem].join('\n');
2095
1777
 
2096
1778
  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
- ],
1779
+ attestation: [{
1780
+ version: 2,
1781
+ type: 'keystore',
1782
+ errors: [0],
1783
+ challenge_nonce: challengeNonce,
1784
+ signed_nonce: signedNonce,
1785
+ key_hash: keyHash,
1786
+ certificate_chain: certificateChain,
1787
+ }],
2108
1788
  };
2109
1789
  }
2110
1790
 
2111
- static _generateSelfSignedCert(privateKey, publicKey, cn) {
2112
- const publicKeyDer = publicKey.export({
2113
- type: 'spki',
2114
- format: 'der',
2115
- });
1791
+ /**
1792
+ * FIX: _generateSignedCert now accepts a separate signing key
1793
+ * (signerPrivateKey), enabling the correct certificate hierarchy.
1794
+ */
1795
+ static _generateSignedCert(privateKey, publicKey, cn, signerPrivateKey) {
1796
+ if (!signerPrivateKey) signerPrivateKey = privateKey; // self-signed fallback
1797
+
1798
+ const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' });
2116
1799
  const serialNumber = crypto.randomBytes(8);
2117
1800
  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
- );
1801
+ const notBefore = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
1802
+ const notAfter = new Date(now.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
2124
1803
  const cnBytes = Buffer.from(cn, 'utf8');
2125
1804
  const cnSeq = Buffer.concat([
2126
1805
  Buffer.from([0x30, cnBytes.length + 13]),
@@ -2132,122 +1811,83 @@ class AccountRepository extends Repository {
2132
1811
  ]);
2133
1812
 
2134
1813
  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
- ]);
1814
+ const str = date.toISOString().replace(/[-:T]/g, '').slice(2, 14) + 'Z';
1815
+ return Buffer.concat([Buffer.from([0x17, str.length]), Buffer.from(str)]);
2144
1816
  }
2145
1817
 
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
- ]);
1818
+ const validityBuf = Buffer.concat([encodeTime(notBefore), encodeTime(notAfter)]);
1819
+ const validity = Buffer.concat([Buffer.from([0x30, validityBuf.length]), validityBuf]);
2154
1820
 
2155
1821
  const tbs = Buffer.concat([
2156
1822
  Buffer.from([0xa0, 0x03, 0x02, 0x01, 0x02]),
2157
1823
  Buffer.from([0x02, serialNumber.length]),
2158
1824
  serialNumber,
2159
- Buffer.from([
2160
- 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d,
2161
- 0x04, 0x03, 0x02,
2162
- ]),
1825
+ Buffer.from([0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]),
2163
1826
  cnSeq,
2164
1827
  validity,
2165
1828
  cnSeq,
2166
1829
  publicKeyDer,
2167
1830
  ]);
2168
1831
 
2169
- const tbsSeq = Buffer.concat([
2170
- Buffer.from([0x30, 0x82]),
2171
- Buffer.alloc(2),
2172
- tbs,
2173
- ]);
1832
+ const tbsSeq = Buffer.concat([Buffer.from([0x30, 0x82]), Buffer.alloc(2), tbs]);
2174
1833
  tbsSeq.writeUInt16BE(tbs.length, 2);
2175
1834
 
2176
- const signature = crypto.sign(null, tbsSeq, privateKey);
1835
+ // FIX: signed with signerPrivateKey (can be an upper-level key)
1836
+ const signature = crypto.sign(null, tbsSeq, signerPrivateKey);
2177
1837
  const sigBitString = Buffer.concat([
2178
1838
  Buffer.from([0x03, signature.length + 1, 0x00]),
2179
1839
  signature,
2180
1840
  ]);
2181
- const algId = Buffer.from([
2182
- 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d,
2183
- 0x04, 0x03, 0x02,
2184
- ]);
1841
+ const algId = Buffer.from([0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]);
2185
1842
  const certBody = Buffer.concat([tbsSeq, algId, sigBitString]);
2186
- const cert = Buffer.concat([
2187
- Buffer.from([0x30, 0x82]),
2188
- Buffer.alloc(2),
2189
- certBody,
2190
- ]);
1843
+ const cert = Buffer.concat([Buffer.from([0x30, 0x82]), Buffer.alloc(2), certBody]);
2191
1844
  cert.writeUInt16BE(certBody.length, 2);
2192
1845
 
2193
1846
  const b64 = cert.toString('base64');
2194
1847
  const lines = b64.match(/.{1,76}/g) || [b64];
2195
- return (
2196
- '-----BEGIN CERTIFICATE-----\n' +
2197
- lines.join('\n') +
2198
- '\n-----END CERTIFICATE-----'
2199
- );
1848
+ return '-----BEGIN CERTIFICATE-----\n' + lines.join('\n') + '\n-----END CERTIFICATE-----';
1849
+ }
1850
+
1851
+ // Kept for compatibility — internally calls _generateSignedCert
1852
+ static _generateSelfSignedCert(privateKey, publicKey, cn) {
1853
+ return AccountRepository._generateSignedCert(privateKey, publicKey, cn, privateKey);
2200
1854
  }
2201
1855
 
2202
1856
  static createJazoest(input) {
2203
1857
  const buf = Buffer.from(input || '', 'ascii');
2204
1858
  let sum = 0;
2205
- for (let i = 0; i < buf.byteLength; i++) {
2206
- sum += buf.readUInt8(i);
2207
- }
1859
+ for (let i = 0; i < buf.byteLength; i++) sum += buf.readUInt8(i);
2208
1860
  return `2${sum}`;
2209
1861
  }
2210
1862
 
2211
1863
  encryptPassword(password) {
2212
1864
  if (!this.client.state.passwordEncryptionPubKey) {
2213
- return {
2214
- time: Math.floor(Date.now() / 1000).toString(),
2215
- encrypted: password,
2216
- };
1865
+ // FIX: no longer returning password in plaintext — throws a controlled error
1866
+ throw new Error(
1867
+ 'passwordEncryptionPubKey missing from state. ' +
1868
+ 'Cannot encrypt password safely.'
1869
+ );
2217
1870
  }
2218
1871
  const randKey = crypto.randomBytes(32);
2219
1872
  const iv = crypto.randomBytes(12);
2220
1873
  const rsaEncrypted = crypto.publicEncrypt(
2221
1874
  {
2222
- key: Buffer.from(
2223
- this.client.state.passwordEncryptionPubKey,
2224
- 'base64'
2225
- ).toString(),
1875
+ key: Buffer.from(this.client.state.passwordEncryptionPubKey, 'base64').toString(),
2226
1876
  padding: crypto.constants.RSA_PKCS1_PADDING,
2227
1877
  },
2228
1878
  randKey
2229
1879
  );
2230
- const cipher = crypto.createCipheriv(
2231
- 'aes-256-gcm',
2232
- randKey,
2233
- iv
2234
- );
1880
+ const cipher = crypto.createCipheriv('aes-256-gcm', randKey, iv);
2235
1881
  const time = Math.floor(Date.now() / 1000).toString();
2236
1882
  cipher.setAAD(Buffer.from(time));
2237
- const aesEncrypted = Buffer.concat([
2238
- cipher.update(password, 'utf8'),
2239
- cipher.final(),
2240
- ]);
1883
+ const aesEncrypted = Buffer.concat([cipher.update(password, 'utf8'), cipher.final()]);
2241
1884
  const sizeBuffer = Buffer.alloc(2, 0);
2242
1885
  sizeBuffer.writeInt16LE(rsaEncrypted.byteLength, 0);
2243
1886
  const authTag = cipher.getAuthTag();
2244
1887
  return {
2245
1888
  time,
2246
1889
  encrypted: Buffer.concat([
2247
- Buffer.from([
2248
- 1,
2249
- this.client.state.passwordEncryptionKeyId || 0,
2250
- ]),
1890
+ Buffer.from([1, this.client.state.passwordEncryptionKeyId || 0]),
2251
1891
  iv,
2252
1892
  sizeBuffer,
2253
1893
  rsaEncrypted,
@@ -2258,16 +1898,10 @@ class AccountRepository extends Repository {
2258
1898
  }
2259
1899
 
2260
1900
  async passwordPublicKeys() {
2261
- const response = await this.client.request.send({
2262
- method: 'GET',
2263
- url: '/api/v1/qe/sync/',
2264
- });
1901
+ const response = await this.client.request.send({ method: 'GET', url: '/api/v1/qe/sync/' });
2265
1902
  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'] || '';
1903
+ const keyId = parseInt(headers['ig-set-password-encryption-key-id'] || '0');
1904
+ const pubKey = headers['ig-set-password-encryption-pub-key'] || '';
2271
1905
  return { keyId, pubKey };
2272
1906
  }
2273
1907
 
@@ -2309,6 +1943,7 @@ class AccountRepository extends Repository {
2309
1943
  });
2310
1944
  }
2311
1945
 
1946
+ // CRITICAL FIX: 'preferen1gces' → 'preferences' (typo → ReferenceError fix)
2312
1947
  async pushPreferences(preferences = 'default') {
2313
1948
  return this.requestWithRetry(async () => {
2314
1949
  const response = await this.client.request.send({
@@ -2321,7 +1956,7 @@ class AccountRepository extends Repository {
2321
1956
  phone_id: this.client.state.phoneId,
2322
1957
  device_token: '',
2323
1958
  guid: this.client.state.uuid,
2324
- users: preferen1gces,
1959
+ users: preferences, // FIX: was 'preferen1gces'
2325
1960
  }),
2326
1961
  });
2327
1962
  return response.body;