nodejs-insta-private-api-mqtt 1.1.10 → 1.2.10

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.
@@ -5,15 +5,28 @@ const path = require('path');
5
5
  const { CookieJar } = require('tough-cookie');
6
6
  const util = require('util');
7
7
  const EventEmitter = require('events');
8
+ const crypto = require('crypto');
8
9
 
9
10
  const FILE_NAMES = {
10
11
  creds: 'creds.json',
11
- device: 'device.json',
12
+ device: 'device.json',
12
13
  cookies: 'cookies.json',
13
14
  mqttSession: 'mqtt-session.json',
14
15
  subscriptions: 'subscriptions.json',
15
16
  seqIds: 'seq-ids.json',
16
- appState: 'app-state.json'
17
+ appState: 'app-state.json',
18
+
19
+ // Newly added MQTT / realtime persistence files
20
+ irisState: 'iris-state.json', // subscription / iris specific state
21
+ mqttTopics: 'mqtt-topics.json', // observed topics
22
+ mqttCapabilities: 'mqtt-capabilities.json', // realtime capabilities per device/version
23
+ mqttAuth: 'mqtt-auth.json', // mqtt auth token / jwt (if available)
24
+ lastPublishIds: 'last-publish-ids.json', // last published message ids / ack info
25
+
26
+ // Added utility files
27
+ loginHistory: 'login-history.json',
28
+ usageStats: 'usage-stats.json',
29
+ accountsIndex: 'accounts.json'
17
30
  };
18
31
 
19
32
  class MultiFileAuthState extends EventEmitter {
@@ -23,7 +36,12 @@ class MultiFileAuthState extends EventEmitter {
23
36
  this._saveDebounceTimer = null;
24
37
  this._saveDebounceMs = 500;
25
38
  this._dirty = new Set();
26
-
39
+
40
+ // Auto-refresh fields
41
+ this._autoRefreshTimer = null;
42
+ this._autoRefreshIntervalMs = 5 * 60 * 1000; // default 5 minutes
43
+ this._autoRefreshClient = null;
44
+
27
45
  this.data = {
28
46
  creds: null,
29
47
  device: null,
@@ -31,20 +49,66 @@ class MultiFileAuthState extends EventEmitter {
31
49
  mqttSession: null,
32
50
  subscriptions: null,
33
51
  seqIds: null,
34
- appState: null
52
+ appState: null,
53
+
54
+ // new
55
+ irisState: null,
56
+ mqttTopics: null,
57
+ mqttCapabilities: null,
58
+ mqttAuth: null,
59
+ lastPublishIds: null,
60
+
61
+ // utility
62
+ loginHistory: null,
63
+ usageStats: null,
64
+ accountsIndex: null
35
65
  };
66
+
67
+ // ensure folder structure
68
+ this._ensureFolder();
69
+ this._ensureBackupFolder();
36
70
  }
37
71
 
38
72
  _getFilePath(key) {
39
73
  return path.join(this.folder, FILE_NAMES[key]);
40
74
  }
41
75
 
76
+ _getBackupFolder() {
77
+ return path.join(this.folder, 'backup');
78
+ }
79
+
42
80
  _ensureFolder() {
43
81
  if (!fs.existsSync(this.folder)) {
44
82
  fs.mkdirSync(this.folder, { recursive: true, mode: 0o700 });
45
83
  }
46
84
  }
47
85
 
86
+ _ensureBackupFolder() {
87
+ const b = this._getBackupFolder();
88
+ if (!fs.existsSync(b)) {
89
+ fs.mkdirSync(b, { recursive: true, mode: 0o700 });
90
+ }
91
+ }
92
+
93
+ // create a timestamped backup of existing file (if present)
94
+ async _createBackup(key) {
95
+ try {
96
+ const filePath = this._getFilePath(key);
97
+ if (fs.existsSync(filePath)) {
98
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
99
+ const base = path.basename(filePath);
100
+ const dest = path.join(this._getBackupFolder(), `${base}.${ts}.bak`);
101
+ await fs.promises.copyFile(filePath, dest);
102
+ this.emit('backup-created', { key, file: dest });
103
+ return dest;
104
+ }
105
+ } catch (e) {
106
+ // ignore errors in backup process but log
107
+ console.warn('[MultiFileAuthState] Backup creation failed for', key, e.message);
108
+ }
109
+ return null;
110
+ }
111
+
48
112
  async _writeFileAtomic(filePath, data) {
49
113
  const tempPath = filePath + '.tmp';
50
114
  const jsonData = JSON.stringify(data, null, 2);
@@ -69,6 +133,8 @@ class MultiFileAuthState extends EventEmitter {
69
133
  this._ensureFolder();
70
134
  const filePath = this._getFilePath(key);
71
135
  try {
136
+ // create backup before overwriting existing file
137
+ await this._createBackup(key);
72
138
  await this._writeFileAtomic(filePath, data);
73
139
  return true;
74
140
  } catch (e) {
@@ -77,15 +143,52 @@ class MultiFileAuthState extends EventEmitter {
77
143
  }
78
144
  }
79
145
 
146
+ // restore the latest backup for a key (returns path of restored file or null)
147
+ async restoreLatestBackup(key) {
148
+ try {
149
+ const base = FILE_NAMES[key];
150
+ const bfolder = this._getBackupFolder();
151
+ if (!fs.existsSync(bfolder)) return null;
152
+ const files = await fs.promises.readdir(bfolder);
153
+ const matches = files.filter(f => f.startsWith(base + '.'));
154
+ if (!matches.length) return null;
155
+ // sort descending by timestamp embedded in filename
156
+ matches.sort().reverse();
157
+ const latest = matches[0];
158
+ const src = path.join(bfolder, latest);
159
+ const dest = this._getFilePath(key);
160
+ await fs.promises.copyFile(src, dest);
161
+ this.emit('backup-restored', { key, file: src });
162
+ // reload into memory
163
+ this.data[key] = await this._readFile(key);
164
+ return src;
165
+ } catch (e) {
166
+ console.warn('[MultiFileAuthState] restoreLatestBackup error:', e.message);
167
+ return null;
168
+ }
169
+ }
170
+
80
171
  async loadAll() {
81
172
  this._ensureFolder();
82
-
173
+ this._ensureBackupFolder();
174
+
83
175
  const loadPromises = Object.keys(FILE_NAMES).map(async (key) => {
84
176
  this.data[key] = await this._readFile(key);
85
177
  });
86
-
178
+
87
179
  await Promise.all(loadPromises);
88
-
180
+
181
+ // ensure usageStats and loginHistory objects exist
182
+ if (!this.data.loginHistory) this.data.loginHistory = [];
183
+ if (!this.data.usageStats) this.data.usageStats = {
184
+ apiRequests: 0,
185
+ mqttMessages: 0,
186
+ errors: 0,
187
+ reconnects: 0,
188
+ lastReset: new Date().toISOString()
189
+ };
190
+ if (!this.data.accountsIndex) this.data.accountsIndex = {};
191
+
89
192
  return {
90
193
  creds: this.data.creds,
91
194
  device: this.data.device,
@@ -94,6 +197,14 @@ class MultiFileAuthState extends EventEmitter {
94
197
  subscriptions: this.data.subscriptions,
95
198
  seqIds: this.data.seqIds,
96
199
  appState: this.data.appState,
200
+ irisState: this.data.irisState,
201
+ mqttTopics: this.data.mqttTopics,
202
+ mqttCapabilities: this.data.mqttCapabilities,
203
+ mqttAuth: this.data.mqttAuth,
204
+ lastPublishIds: this.data.lastPublishIds,
205
+ loginHistory: this.data.loginHistory,
206
+ usageStats: this.data.usageStats,
207
+ accountsIndex: this.data.accountsIndex,
97
208
  hasSession: !!(this.data.creds && this.data.cookies)
98
209
  };
99
210
  }
@@ -104,6 +215,16 @@ class MultiFileAuthState extends EventEmitter {
104
215
  await this._writeFile(key, this.data[key]);
105
216
  }
106
217
  });
218
+ // also save usageStats and loginHistory explicitly
219
+ if (this.data.loginHistory) {
220
+ try { await this._writeFile('loginHistory', this.data.loginHistory); } catch (e) {}
221
+ }
222
+ if (this.data.usageStats) {
223
+ try { await this._writeFile('usageStats', this.data.usageStats); } catch (e) {}
224
+ }
225
+ if (this.data.accountsIndex) {
226
+ try { await this._writeFile('accountsIndex', this.data.accountsIndex); } catch (e) {}
227
+ }
107
228
  await Promise.all(savePromises);
108
229
  }
109
230
 
@@ -112,15 +233,27 @@ class MultiFileAuthState extends EventEmitter {
112
233
  if (this.data.creds) promises.push(this._writeFile('creds', this.data.creds));
113
234
  if (this.data.device) promises.push(this._writeFile('device', this.data.device));
114
235
  if (this.data.cookies) promises.push(this._writeFile('cookies', this.data.cookies));
236
+ // also persist loginHistory and usageStats after creds save
237
+ if (this.data.loginHistory) promises.push(this._writeFile('loginHistory', this.data.loginHistory));
238
+ if (this.data.usageStats) promises.push(this._writeFile('usageStats', this.data.usageStats));
115
239
  await Promise.all(promises);
116
240
  this.emit('creds-saved');
117
241
  }
118
242
 
119
243
  async saveMqttState() {
244
+ // Save base mqtt data
120
245
  const promises = [];
121
246
  if (this.data.mqttSession) promises.push(this._writeFile('mqttSession', this.data.mqttSession));
122
247
  if (this.data.subscriptions) promises.push(this._writeFile('subscriptions', this.data.subscriptions));
123
248
  if (this.data.seqIds) promises.push(this._writeFile('seqIds', this.data.seqIds));
249
+
250
+ // Save extended mqtt/realtime data (new files)
251
+ if (this.data.irisState) promises.push(this._writeFile('irisState', this.data.irisState));
252
+ if (this.data.mqttTopics) promises.push(this._writeFile('mqttTopics', this.data.mqttTopics));
253
+ if (this.data.mqttCapabilities) promises.push(this._writeFile('mqttCapabilities', this.data.mqttCapabilities));
254
+ if (this.data.mqttAuth) promises.push(this._writeFile('mqttAuth', this.data.mqttAuth));
255
+ if (this.data.lastPublishIds) promises.push(this._writeFile('lastPublishIds', this.data.lastPublishIds));
256
+
124
257
  await Promise.all(promises);
125
258
  this.emit('mqtt-state-saved');
126
259
  }
@@ -133,23 +266,35 @@ class MultiFileAuthState extends EventEmitter {
133
266
 
134
267
  _debouncedSave(keys) {
135
268
  keys.forEach(k => this._dirty.add(k));
136
-
269
+
137
270
  if (this._saveDebounceTimer) {
138
271
  clearTimeout(this._saveDebounceTimer);
139
272
  }
140
-
273
+
141
274
  this._saveDebounceTimer = setTimeout(async () => {
142
275
  const toSave = Array.from(this._dirty);
143
276
  this._dirty.clear();
144
-
277
+
145
278
  for (const key of toSave) {
146
279
  if (this.data[key] !== null && this.data[key] !== undefined) {
147
- await this._writeFile(key, this.data[key]);
280
+ try {
281
+ // ensure backup for critical files
282
+ if (['creds', 'cookies', 'device'].includes(key)) {
283
+ await this._createBackup(key);
284
+ }
285
+ await this._writeFile(key, this.data[key]);
286
+ } catch (e) {
287
+ console.error(`[MultiFileAuthState] Debounced write error for ${key}:`, e.message);
288
+ // increment usageStats.errors
289
+ try { this.incrementStat('errors'); } catch (e2) {}
290
+ }
148
291
  }
149
292
  }
293
+ this.emit('debounced-save-complete', toSave);
150
294
  }, this._saveDebounceMs);
151
295
  }
152
296
 
297
+ // --- Setters that schedule debounced saves ---
153
298
  setCreds(creds) {
154
299
  this.data.creds = creds;
155
300
  this._debouncedSave(['creds']);
@@ -185,6 +330,59 @@ class MultiFileAuthState extends EventEmitter {
185
330
  this._debouncedSave(['appState']);
186
331
  }
187
332
 
333
+ // --- New setters for additional MQTT files ---
334
+ setIrisState(irisState) {
335
+ this.data.irisState = irisState;
336
+ this._debouncedSave(['irisState']);
337
+ }
338
+
339
+ setMqttTopics(topics) {
340
+ this.data.mqttTopics = topics;
341
+ this._debouncedSave(['mqttTopics']);
342
+ }
343
+
344
+ setMqttCapabilities(capabilities) {
345
+ this.data.mqttCapabilities = capabilities;
346
+ this._debouncedSave(['mqttCapabilities']);
347
+ }
348
+
349
+ setMqttAuth(auth) {
350
+ this.data.mqttAuth = auth;
351
+ this._debouncedSave(['mqttAuth']);
352
+ }
353
+
354
+ setLastPublishIds(lastPublishIds) {
355
+ this.data.lastPublishIds = lastPublishIds;
356
+ this._debouncedSave(['lastPublishIds']);
357
+ }
358
+
359
+ // --- Utility setters ---
360
+ addLoginHistory(entry) {
361
+ if (!this.data.loginHistory) this.data.loginHistory = [];
362
+ this.data.loginHistory.unshift(entry);
363
+ // keep bounded history length
364
+ if (this.data.loginHistory.length > 200) this.data.loginHistory.length = 200;
365
+ this._debouncedSave(['loginHistory']);
366
+ this.emit('login-attempt', entry);
367
+ }
368
+
369
+ incrementStat(statName, by = 1) {
370
+ if (!this.data.usageStats) this.data.usageStats = {
371
+ apiRequests: 0,
372
+ mqttMessages: 0,
373
+ errors: 0,
374
+ reconnects: 0,
375
+ lastReset: new Date().toISOString()
376
+ };
377
+ if (typeof this.data.usageStats[statName] === 'number') {
378
+ this.data.usageStats[statName] += by;
379
+ } else {
380
+ this.data.usageStats[statName] = (this.data.usageStats[statName] || 0) + by;
381
+ }
382
+ this._debouncedSave(['usageStats']);
383
+ }
384
+
385
+ // --- Getters ---
188
386
  getCreds() { return this.data.creds; }
189
387
  getDevice() { return this.data.device; }
190
388
  getCookies() { return this.data.cookies; }
@@ -192,13 +390,24 @@ class MultiFileAuthState extends EventEmitter {
192
390
  getSubscriptions() { return this.data.subscriptions; }
193
391
  getSeqIds() { return this.data.seqIds; }
194
392
  getAppState() { return this.data.appState; }
393
+ getIrisState() { return this.data.irisState; }
394
+ getMqttTopics() { return this.data.mqttTopics; }
395
+ getMqttCapabilities() { return this.data.mqttCapabilities; }
396
+ getMqttAuth() { return this.data.mqttAuth; }
397
+ getLastPublishIds() { return this.data.lastPublishIds; }
398
+ getLoginHistory() { return this.data.loginHistory || []; }
399
+ getUsageStats() { return this.data.usageStats || {}; }
400
+ getAccountsIndex() { return this.data.accountsIndex || {}; }
195
401
 
196
402
  hasValidSession() {
197
403
  return !!(this.data.creds && this.data.cookies && this.data.creds.authorization);
198
404
  }
199
405
 
200
406
  hasMqttSession() {
201
- return !!(this.data.mqttSession && this.data.mqttSession.sessionId);
407
+ return !!(
408
+ (this.data.mqttSession && this.data.mqttSession.sessionId) ||
409
+ (this.data.mqttAuth && this.data.mqttAuth.jwt)
410
+ );
202
411
  }
203
412
 
204
413
  async clearAll() {
@@ -211,10 +420,213 @@ class MultiFileAuthState extends EventEmitter {
211
420
  } catch (e) {}
212
421
  this.data[key] = null;
213
422
  }
423
+ // clear backups too? leave backups intact but emit event
424
+ this.emit('cleared-all');
425
+ }
426
+
427
+ // --- Backup utilities exposed ---
428
+ async listBackups() {
429
+ try {
430
+ const b = this._getBackupFolder();
431
+ if (!fs.existsSync(b)) return [];
432
+ const files = await fs.promises.readdir(b);
433
+ return files.map(f => path.join(b, f));
434
+ } catch (e) {
435
+ return [];
436
+ }
437
+ }
438
+
439
+ // --- Device fingerprint helpers ---
440
+ computeDeviceFingerprint(deviceObj) {
441
+ try {
442
+ const s = JSON.stringify(deviceObj || this.data.device || {});
443
+ return crypto.createHash('sha256').update(s).digest('hex');
444
+ } catch (e) {
445
+ return null;
446
+ }
447
+ }
448
+
449
+ // lock device fingerprint: saves hash into device.json (device.locked = true)
450
+ async lockDeviceFingerprint() {
451
+ try {
452
+ if (!this.data.device) this.data.device = {};
453
+ const hash = this.computeDeviceFingerprint(this.data.device);
454
+ this.data.device.fingerprintHash = hash;
455
+ this.data.device.locked = true;
456
+ await this._writeFile('device', this.data.device);
457
+ this.emit('device-locked', { fingerprintHash: hash });
458
+ return hash;
459
+ } catch (e) {
460
+ console.warn('[MultiFileAuthState] lockDeviceFingerprint failed:', e.message);
461
+ return null;
462
+ }
463
+ }
464
+
465
+ verifyDeviceFingerprint(deviceObj) {
466
+ try {
467
+ const stored = this.data.device?.fingerprintHash || null;
468
+ if (!stored) return true; // no lock present
469
+ const hash = this.computeDeviceFingerprint(deviceObj || this.data.device);
470
+ return stored === hash;
471
+ } catch (e) {
472
+ return false;
473
+ }
474
+ }
475
+
476
+ // --- Login history helpers ---
477
+ recordLoginAttempt({ success, ip, userAgent, method = 'password', reason = null }) {
478
+ const entry = {
479
+ date: new Date().toISOString(),
480
+ success: Boolean(success),
481
+ ip: ip || null,
482
+ userAgent: userAgent || null,
483
+ method,
484
+ reason
485
+ };
486
+ this.addLoginHistory(entry);
487
+ return entry;
488
+ }
489
+
490
+ // mark account restricted / checkpoint
491
+ markAccountRestricted({ status = 'checkpoint', reason = null }) {
492
+ if (!this.data.creds) this.data.creds = {};
493
+ this.data.creds.accountStatus = status;
494
+ this.data.creds.restrictedAt = new Date().toISOString();
495
+ this.data.creds.lastError = reason;
496
+ this._debouncedSave(['creds']);
497
+ this.emit('account-restricted', { status, reason });
498
+ }
499
+
500
+ // clear restriction
501
+ clearAccountRestriction() {
502
+ if (this.data.creds) {
503
+ delete this.data.creds.accountStatus;
504
+ delete this.data.creds.restrictedAt;
505
+ delete this.data.creds.lastError;
506
+ this._debouncedSave(['creds']);
507
+ this.emit('account-unrestricted');
508
+ }
509
+ }
510
+
511
+ // --- Health check ---
512
+ getHealth() {
513
+ return {
514
+ validSession: this.hasValidSession(),
515
+ hasCookies: !!this.data.cookies,
516
+ hasMqtt: this.hasMqttSession(),
517
+ lastLogin: (this.data.loginHistory && this.data.loginHistory[0]?.date) || null,
518
+ usageStats: this.getUsageStats(),
519
+ accountStatus: this.data.creds?.accountStatus || 'ok',
520
+ deviceLocked: !!this.data.device?.locked
521
+ };
522
+ }
523
+ }
524
+
525
+ // Helper: try to safely extract cookieJar serialization if exists
526
+ async function trySerializeCookieJar(igState) {
527
+ let cookies = null;
528
+ try {
529
+ if (igState.cookieJar && typeof igState.serializeCookieJar === 'function') {
530
+ cookies = await igState.serializeCookieJar();
531
+ } else if (igState.cookieJar && typeof igState.cookieJar.serialize === 'function') {
532
+ // fallback: tough-cookie jar serialize
533
+ const ser = await util.promisify(igState.cookieJar.serialize).bind(igState.cookieJar)();
534
+ cookies = ser;
535
+ }
536
+ } catch (e) {
537
+ console.warn('[MultiFileAuthState] Could not serialize cookies:', e.message);
538
+ }
539
+ return cookies;
540
+ }
541
+
542
+ // Helper: try to read a cookie value from a tough-cookie Jar or equivalent
543
+ async function getCookieValueFromJar(cookieJar, name) {
544
+ if (!cookieJar) return null;
545
+
546
+ // try getCookieString (tough-cookie)
547
+ try {
548
+ if (typeof cookieJar.getCookieString === 'function') {
549
+ const getCookieString = util.promisify(cookieJar.getCookieString).bind(cookieJar);
550
+ const urls = ['https://www.instagram.com', 'https://instagram.com', 'https://i.instagram.com'];
551
+ for (const url of urls) {
552
+ try {
553
+ const cookieString = await getCookieString(url);
554
+ if (cookieString && typeof cookieString === 'string') {
555
+ const pairs = cookieString.split(';').map(s => s.trim());
556
+ for (const p of pairs) {
557
+ const [k, ...rest] = p.split('=');
558
+ if (k === name) return rest.join('=');
559
+ }
560
+ }
561
+ } catch (e) {
562
+ // ignore and try next url
563
+ }
564
+ }
565
+ }
566
+ } catch (e) {
567
+ // ignore
568
+ }
569
+
570
+ // try getCookies which returns Cookie objects
571
+ try {
572
+ if (typeof cookieJar.getCookies === 'function') {
573
+ const getCookies = util.promisify(cookieJar.getCookies).bind(cookieJar);
574
+ const urls = ['https://www.instagram.com', 'https://instagram.com', 'https://i.instagram.com'];
575
+ for (const url of urls) {
576
+ try {
577
+ const cookies = await getCookies(url);
578
+ if (Array.isArray(cookies)) {
579
+ for (const c of cookies) {
580
+ // cookie object shapes vary: check common keys
581
+ const key = c.key ?? c.name ?? c.name;
582
+ const val = c.value ?? c.value ?? c.value;
583
+ if (key === name) return val;
584
+ }
585
+ }
586
+ } catch (e) {
587
+ // ignore and try next url
588
+ }
589
+ }
590
+ }
591
+ } catch (e) {
592
+ // ignore
593
+ }
594
+
595
+ // try serialized cookie jar structure if present
596
+ try {
597
+ if (cookieJar && typeof cookieJar === 'object' && cookieJar.cookies && Array.isArray(cookieJar.cookies)) {
598
+ const found = cookieJar.cookies.find(c => (c.key === name || c.name === name || c.name === name));
599
+ if (found) return found.value ?? found.value;
600
+ }
601
+ } catch (e) {
602
+ // ignore
214
603
  }
604
+
605
+ return null;
606
+ }
607
+
608
+ // Extract cookie-derived fields (sessionid, csrftoken, ds_user_id, mid)
609
+ async function extractCookieFields(igState) {
610
+ const out = {
611
+ sessionIdCookie: null,
612
+ csrfTokenCookie: null,
613
+ dsUserIdCookie: null,
614
+ midCookie: null
615
+ };
616
+ try {
617
+ const cookieJar = igState.cookieJar;
618
+ out.sessionIdCookie = await getCookieValueFromJar(cookieJar, 'sessionid');
619
+ out.csrfTokenCookie = await getCookieValueFromJar(cookieJar, 'csrftoken');
620
+ out.dsUserIdCookie = await getCookieValueFromJar(cookieJar, 'ds_user_id') || await getCookieValueFromJar(cookieJar, 'ds_user');
621
+ out.midCookie = await getCookieValueFromJar(cookieJar, 'mid');
622
+ } catch (e) {
623
+ // ignore
624
+ }
625
+ return out;
215
626
  }
216
627
 
217
628
  async function extractStateData(igState) {
629
+ // Basic creds (existing)
218
630
  const creds = {
219
631
  authorization: igState.authorization || null,
220
632
  igWWWClaim: igState.igWWWClaim || null,
@@ -222,6 +634,7 @@ async function extractStateData(igState) {
222
634
  passwordEncryptionPubKey: igState.passwordEncryptionPubKey || null
223
635
  };
224
636
 
637
+ // Device remains same
225
638
  const device = {
226
639
  deviceString: igState.deviceString || null,
227
640
  deviceId: igState.deviceId || null,
@@ -231,15 +644,10 @@ async function extractStateData(igState) {
231
644
  build: igState.build || null
232
645
  };
233
646
 
234
- let cookies = null;
235
- try {
236
- if (igState.cookieJar && typeof igState.serializeCookieJar === 'function') {
237
- cookies = await igState.serializeCookieJar();
238
- }
239
- } catch (e) {
240
- console.warn('[MultiFileAuthState] Could not serialize cookies:', e.message);
241
- }
647
+ // Try to serialize cookies (kept separate)
648
+ const cookies = await trySerializeCookieJar(igState);
242
649
 
650
+ // App state remains same
243
651
  const appState = {
244
652
  language: igState.language || 'en_US',
245
653
  timezoneOffset: igState.timezoneOffset || null,
@@ -249,6 +657,62 @@ async function extractStateData(igState) {
249
657
  challenge: igState.challenge || null
250
658
  };
251
659
 
660
+ // --- NEW: richer creds fields derived from igState + cookies ---
661
+ // try to obtain cookie fields
662
+ const cookieFields = await extractCookieFields(igState);
663
+
664
+ // Primary identifiers
665
+ const userIdFromState = igState.cookieUserId || igState.userId || igState.user_id || null;
666
+ const dsUserIdFromCookies = cookieFields.dsUserIdCookie || igState.dsUserId || igState.ds_user_id || null;
667
+ const sessionIdFromCookies = cookieFields.sessionIdCookie || null;
668
+ const csrfFromCookies = cookieFields.csrfTokenCookie || null;
669
+ const midFromCookies = cookieFields.midCookie || igState.mid || null;
670
+
671
+ // username if exposed on state
672
+ const usernameFromState = igState.username || igState.userName || igState.user?.username || null;
673
+
674
+ // rankToken: if userId + uuid available, form `${userId}_${uuid}`
675
+ let rankToken = null;
676
+ try {
677
+ if (userIdFromState && (igState.uuid || device.uuid)) {
678
+ rankToken = `${userIdFromState}_${igState.uuid || device.uuid}`;
679
+ } else if (igState.rankToken) {
680
+ rankToken = igState.rankToken;
681
+ }
682
+ } catch (e) {
683
+ rankToken = igState.rankToken || null;
684
+ }
685
+
686
+ // sessionId and csrfToken - prefer cookies, fallback to state fields if present
687
+ const sessionId = sessionIdFromCookies || igState.sessionId || igState.sessionid || null;
688
+ const csrfToken = csrfFromCookies || igState.csrfToken || igState.csrftoken || null;
689
+
690
+ // mid fallback
691
+ const mid = midFromCookies || igState.mid || null;
692
+
693
+ // isLoggedIn heuristic: presence of sessionid cookie or an 'isLoggedIn' flag or authorization header
694
+ const isLoggedIn = Boolean(
695
+ sessionId ||
696
+ igState.isLoggedIn ||
697
+ (creds.authorization && creds.authorization.length)
698
+ );
699
+
700
+ // loginAt / lastValidatedAt: try to take from igState if present, otherwise null
701
+ const loginAt = igState.loginAt || igState.loggedInAt || null;
702
+ const lastValidatedAt = igState.lastValidatedAt || igState.lastValidated || null;
703
+
704
+ // Attach all new fields into creds object (non-destructive)
705
+ creds.userId = userIdFromState || dsUserIdFromCookies || creds.userId || null;
706
+ creds.dsUserId = dsUserIdFromCookies || null;
707
+ creds.username = usernameFromState || null;
708
+ creds.rankToken = rankToken;
709
+ creds.sessionId = sessionId;
710
+ creds.csrfToken = csrfToken;
711
+ creds.mid = mid;
712
+ creds.isLoggedIn = isLoggedIn;
713
+ creds.loginAt = loginAt;
714
+ creds.lastValidatedAt = lastValidatedAt;
715
+
252
716
  return { creds, device, cookies, appState };
253
717
  }
254
718
 
@@ -260,7 +724,25 @@ async function applyStateData(igState, authState) {
260
724
  if (creds.igWWWClaim) igState.igWWWClaim = creds.igWWWClaim;
261
725
  if (creds.passwordEncryptionKeyId) igState.passwordEncryptionKeyId = creds.passwordEncryptionKeyId;
262
726
  if (creds.passwordEncryptionPubKey) igState.passwordEncryptionPubKey = creds.passwordEncryptionPubKey;
263
- igState.updateAuthorization();
727
+ // if igState provides a helper to update authorization, call it
728
+ if (typeof igState.updateAuthorization === 'function') {
729
+ try { igState.updateAuthorization(); } catch (e) {}
730
+ }
731
+
732
+ // apply new creds-derived fields if state supports them (non-intrusive)
733
+ try {
734
+ if (creds.userId && !igState.cookieUserId) igState.cookieUserId = creds.userId;
735
+ if (creds.username && !igState.username) igState.username = creds.username;
736
+ if (creds.rankToken && !igState.rankToken) igState.rankToken = creds.rankToken;
737
+ if (creds.sessionId && !igState.sessionId) igState.sessionId = creds.sessionId;
738
+ if (creds.csrfToken && !igState.csrfToken) igState.csrfToken = creds.csrfToken;
739
+ if (creds.mid && !igState.mid) igState.mid = creds.mid;
740
+ if (typeof creds.isLoggedIn !== 'undefined' && !igState.isLoggedIn) igState.isLoggedIn = creds.isLoggedIn;
741
+ if (creds.loginAt && !igState.loginAt) igState.loginAt = creds.loginAt;
742
+ if (creds.lastValidatedAt && !igState.lastValidatedAt) igState.lastValidatedAt = creds.lastValidatedAt;
743
+ } catch (e) {
744
+ // ignore if igState shape different
745
+ }
264
746
  }
265
747
 
266
748
  if (device) {
@@ -274,7 +756,15 @@ async function applyStateData(igState, authState) {
274
756
 
275
757
  if (cookies) {
276
758
  try {
277
- await igState.deserializeCookieJar(cookies);
759
+ if (typeof igState.deserializeCookieJar === 'function') {
760
+ await igState.deserializeCookieJar(cookies);
761
+ } else if (igState.cookieJar && typeof igState.cookieJar.restore === 'function') {
762
+ // fallback restore
763
+ await util.promisify(igState.cookieJar.restore).bind(igState.cookieJar)(cookies);
764
+ } else if (igState.cookieJar && typeof igState.cookieJar._importCookies === 'function') {
765
+ // last-resort, not likely
766
+ try { igState.cookieJar._importCookies(cookies); } catch (e) {}
767
+ }
278
768
  } catch (e) {
279
769
  console.warn('[MultiFileAuthState] Could not deserialize cookies:', e.message);
280
770
  }
@@ -290,26 +780,46 @@ async function applyStateData(igState, authState) {
290
780
  }
291
781
  }
292
782
 
783
+ // Main exported helper that wires MultiFileAuthState to your clients
293
784
  async function useMultiFileAuthState(folder) {
294
785
  const authState = new MultiFileAuthState(folder);
295
786
  await authState.loadAll();
296
787
 
788
+ // Helper: persist additional derived creds fields when saving creds
789
+ const enrichAndSaveCreds = async (igClientState) => {
790
+ const { creds, device, cookies, appState } = await extractStateData(igClientState);
791
+ // merge with existing creds non-destructive
792
+ authState.data.creds = Object.assign({}, authState.data.creds || {}, creds);
793
+ authState.data.device = Object.assign({}, authState.data.device || {}, device);
794
+ authState.data.cookies = cookies || authState.data.cookies;
795
+ authState.data.appState = Object.assign({}, authState.data.appState || {}, appState);
796
+ await authState.saveCreds();
797
+ await authState.saveAppState();
798
+ };
799
+
297
800
  const saveCreds = async (igClient) => {
298
801
  if (!igClient || !igClient.state) {
299
802
  console.warn('[useMultiFileAuthState] No igClient provided to saveCreds');
300
803
  return;
301
804
  }
302
805
 
303
- const { creds, device, cookies, appState } = await extractStateData(igClient.state);
304
-
305
- authState.data.creds = creds;
306
- authState.data.device = device;
307
- authState.data.cookies = cookies;
308
- authState.data.appState = appState;
309
-
310
- await authState.saveCreds();
311
- await authState.saveAppState();
312
-
806
+ // Use the enriched saver
807
+ await enrichAndSaveCreds(igClient.state);
808
+
809
+ // Also attempt to extract some cookie-derived fields for convenience
810
+ try {
811
+ const cookieFields = await extractCookieFields(igClient.state);
812
+ if (cookieFields.sessionIdCookie) {
813
+ if (!authState.data.creds) authState.data.creds = {};
814
+ authState.data.creds.sessionId = cookieFields.sessionIdCookie;
815
+ }
816
+ if (cookieFields.dsUserIdCookie) {
817
+ if (!authState.data.creds) authState.data.creds = {};
818
+ authState.data.creds.dsUserId = cookieFields.dsUserIdCookie;
819
+ }
820
+ authState._debouncedSave(['creds']);
821
+ } catch (e) {}
822
+
313
823
  console.log('[useMultiFileAuthState] Credentials saved to', folder);
314
824
  };
315
825
 
@@ -319,6 +829,7 @@ async function useMultiFileAuthState(folder) {
319
829
  return;
320
830
  }
321
831
 
832
+ // base mqtt session
322
833
  const mqttSession = {
323
834
  sessionId: null,
324
835
  mqttSessionId: null,
@@ -328,30 +839,92 @@ async function useMultiFileAuthState(folder) {
328
839
 
329
840
  try {
330
841
  if (realtimeClient.ig && realtimeClient.ig.state) {
331
- mqttSession.userId = realtimeClient.ig.state.cookieUserId;
332
- mqttSession.sessionId = realtimeClient.extractSessionIdFromJWT?.() || null;
842
+ mqttSession.userId = realtimeClient.ig.state.cookieUserId || realtimeClient.ig.state.userId || null;
843
+ // attempt to extract sessionId from JWT helper if available
844
+ mqttSession.sessionId = (typeof realtimeClient.extractSessionIdFromJWT === 'function') ? realtimeClient.extractSessionIdFromJWT() : null;
333
845
  }
334
846
  if (realtimeClient.connection) {
335
847
  mqttSession.mqttSessionId = realtimeClient.connection?.clientInfo?.clientMqttSessionId?.toString() || null;
336
848
  }
337
- } catch (e) {}
849
+ } catch (e) {
850
+ // ignore
851
+ }
338
852
 
853
+ // subscriptions
339
854
  const subscriptions = {
340
855
  graphQlSubs: realtimeClient.initOptions?.graphQlSubs || [],
341
856
  skywalkerSubs: realtimeClient.initOptions?.skywalkerSubs || [],
342
857
  subscribedAt: new Date().toISOString()
343
858
  };
344
859
 
860
+ // seq ids (iris)
345
861
  const seqIds = {};
346
862
  if (realtimeClient.initOptions?.irisData) {
347
863
  seqIds.seq_id = realtimeClient.initOptions.irisData.seq_id || null;
348
864
  seqIds.snapshot_at_ms = realtimeClient.initOptions.irisData.snapshot_at_ms || null;
349
865
  }
350
866
 
867
+ // --- Extended MQTT / realtime info (new) ---
868
+ // irisState: things like subscription_id / device id / other iris metadata
869
+ const irisState = {};
870
+ try {
871
+ // try to glean from various possible places on realtimeClient
872
+ irisState.subscription_id = realtimeClient.initOptions?.irisData?.subscription_id || realtimeClient.iris?.subscriptionId || realtimeClient.irisState?.subscription_id || null;
873
+ irisState.user_id = realtimeClient.ig?.state?.cookieUserId || realtimeClient.ig?.state?.userId || irisState.user_id || null;
874
+ irisState.device_id = realtimeClient.connection?.clientInfo?.deviceId || realtimeClient.initOptions?.deviceId || null;
875
+ irisState.created_at = new Date().toISOString();
876
+ } catch (e) {}
877
+
878
+ // mqttTopics: topics observed/subscribed
879
+ const mqttTopics = {};
880
+ try {
881
+ mqttTopics.topics = realtimeClient.connection?.subscribedTopics || realtimeClient._subscribedTopics || realtimeClient.subscribedTopics || [];
882
+ mqttTopics.updatedAt = new Date().toISOString();
883
+ } catch (e) {
884
+ mqttTopics.topics = [];
885
+ mqttTopics.updatedAt = new Date().toISOString();
886
+ }
887
+
888
+ // mqttCapabilities: features supported / protocol version
889
+ const mqttCapabilities = {};
890
+ try {
891
+ mqttCapabilities.supportsTyping = Boolean(realtimeClient.initOptions?.supportsTyping ?? realtimeClient.capabilities?.supportsTyping);
892
+ mqttCapabilities.supportsReactions = Boolean(realtimeClient.initOptions?.supportsReactions ?? realtimeClient.capabilities?.supportsReactions);
893
+ mqttCapabilities.supportsVoice = Boolean(realtimeClient.initOptions?.supportsVoice ?? realtimeClient.capabilities?.supportsVoice);
894
+ mqttCapabilities.protocolVersion = realtimeClient.connection?.protocolVersion || realtimeClient.initOptions?.protocolVersion || null;
895
+ mqttCapabilities.collectedAt = new Date().toISOString();
896
+ } catch (e) {}
897
+
898
+ // mqttAuth: if realtimeClient keeps an auth token / jwt for mqtt, store (but be careful: short lived)
899
+ const mqttAuth = {};
900
+ try {
901
+ mqttAuth.jwt = realtimeClient.connection?.authToken || realtimeClient.mqttAuth?.jwt || realtimeClient.initOptions?.mqttJwt || null;
902
+ mqttAuth.expiresAt = realtimeClient.connection?.authExpiresAt || realtimeClient.mqttAuth?.expiresAt || null;
903
+ mqttAuth.collectedAt = new Date().toISOString();
904
+ } catch (e) {}
905
+
906
+ // lastPublishIds: tracking last message ids for reliable publish/acks
907
+ const lastPublishIds = {};
908
+ try {
909
+ lastPublishIds.lastMessageId = realtimeClient._lastMessageId || realtimeClient.lastMessageId || null;
910
+ lastPublishIds.lastAckedAt = realtimeClient._lastAckedAt || null;
911
+ lastPublishIds.collectedAt = new Date().toISOString();
912
+ } catch (e) {}
913
+
914
+ // Assign all into authState and persist
351
915
  authState.data.mqttSession = mqttSession;
352
916
  authState.data.subscriptions = subscriptions;
353
917
  authState.data.seqIds = seqIds;
354
918
 
919
+ authState.data.irisState = irisState;
920
+ authState.data.mqttTopics = mqttTopics;
921
+ authState.data.mqttCapabilities = mqttCapabilities;
922
+ authState.data.mqttAuth = mqttAuth;
923
+ authState.data.lastPublishIds = lastPublishIds;
924
+
925
+ // Increment stats
926
+ authState.incrementStat('mqttMessages');
927
+
355
928
  await authState.saveMqttState();
356
929
  console.log('[useMultiFileAuthState] MQTT session saved to', folder);
357
930
  };
@@ -379,6 +952,7 @@ async function useMultiFileAuthState(folder) {
379
952
 
380
953
  const subs = authState.getSubscriptions() || {};
381
954
  const seqIds = authState.getSeqIds() || {};
955
+ const mqttAuth = authState.getMqttAuth() || null;
382
956
 
383
957
  return {
384
958
  graphQlSubs: subs.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
@@ -386,7 +960,9 @@ async function useMultiFileAuthState(folder) {
386
960
  irisData: seqIds.seq_id ? {
387
961
  seq_id: seqIds.seq_id,
388
962
  snapshot_at_ms: seqIds.snapshot_at_ms
389
- } : null
963
+ } : null,
964
+ mqttAuthToken: mqttAuth?.jwt || null,
965
+ mqttAuthExpiresAt: mqttAuth?.expiresAt || null
390
966
  };
391
967
  };
392
968
 
@@ -400,32 +976,182 @@ async function useMultiFileAuthState(folder) {
400
976
  if (!igClient) return true;
401
977
 
402
978
  try {
403
- await igClient.account.currentUser();
979
+ // quick check by calling account/currentUser or equivalent
980
+ if (igClient.account && typeof igClient.account.currentUser === 'function') {
981
+ await igClient.account.currentUser();
982
+ return true;
983
+ }
984
+ // fallback assume valid if we have creds - up to caller to verify
404
985
  return true;
405
986
  } catch (e) {
406
987
  console.warn('[useMultiFileAuthState] Session validation failed:', e.message);
988
+ // emit session-expired if credentials appear invalid
989
+ authState.emit('session-expired', { reason: e.message });
407
990
  return false;
408
991
  }
409
992
  };
410
993
 
994
+ // --- Auto-refresh / revalidation helpers ---
995
+ const _autoRefreshCheck = async () => {
996
+ try {
997
+ if (!authState.data.creds) return;
998
+ // If creds indicate expiry timestamp, check it
999
+ const expiresAt = authState.data.creds.sessionExpiresAt ? new Date(authState.data.creds.sessionExpiresAt).getTime() : null;
1000
+ if (expiresAt && Date.now() > expiresAt) {
1001
+ authState.data.creds.isExpired = true;
1002
+ authState._debouncedSave(['creds']);
1003
+ authState.emit('session-expired', { reason: 'sessionExpiresAt' });
1004
+ } else {
1005
+ // if igClient provided, optionally validate remote
1006
+ if (authState._autoRefreshClient) {
1007
+ try {
1008
+ const valid = await isSessionValid(authState._autoRefreshClient);
1009
+ if (!valid) {
1010
+ authState.emit('session-expired', { reason: 'remote-check' });
1011
+ } else {
1012
+ // update lastValidatedAt
1013
+ if (!authState.data.creds) authState.data.creds = {};
1014
+ authState.data.creds.lastValidatedAt = new Date().toISOString();
1015
+ authState._debouncedSave(['creds']);
1016
+ }
1017
+ } catch (e) {}
1018
+ }
1019
+ }
1020
+ } catch (e) {
1021
+ // ignore
1022
+ }
1023
+ };
1024
+
1025
+ const enableAutoRefresh = (igClient, intervalMs = 5 * 60 * 1000) => {
1026
+ // igClient optional: if provided, will be used to do remote validation
1027
+ try {
1028
+ disableAutoRefresh();
1029
+ authState._autoRefreshClient = igClient || null;
1030
+ authState._autoRefreshIntervalMs = intervalMs;
1031
+ authState._autoRefreshTimer = setInterval(_autoRefreshCheck, intervalMs);
1032
+ // run immediately once
1033
+ _autoRefreshCheck();
1034
+ authState.emit('auto-refresh-enabled', { intervalMs });
1035
+ } catch (e) {
1036
+ console.warn('[useMultiFileAuthState] enableAutoRefresh error:', e.message);
1037
+ }
1038
+ };
1039
+
1040
+ const disableAutoRefresh = () => {
1041
+ try {
1042
+ if (authState._autoRefreshTimer) {
1043
+ clearInterval(authState._autoRefreshTimer);
1044
+ authState._autoRefreshTimer = null;
1045
+ authState._autoRefreshClient = null;
1046
+ authState.emit('auto-refresh-disabled');
1047
+ }
1048
+ } catch (e) {}
1049
+ };
1050
+
1051
+ // --- Login history / audit helpers ---
1052
+ const recordLoginAttempt = async ({ success, ip = null, userAgent = null, method = 'password', reason = null }) => {
1053
+ const entry = authState.recordLoginAttempt({ success, ip, userAgent, method, reason });
1054
+ // return entry for caller
1055
+ return entry;
1056
+ };
1057
+
1058
+ // --- Account restriction helpers ---
1059
+ const markAccountRestricted = ({ status = 'checkpoint', reason = null }) => {
1060
+ authState.markAccountRestricted({ status, reason });
1061
+ };
1062
+
1063
+ const clearAccountRestriction = () => {
1064
+ authState.clearAccountRestriction();
1065
+ };
1066
+
1067
+ // --- Usage stats helpers ---
1068
+ const incrementStat = (statName, by = 1) => {
1069
+ authState.incrementStat(statName, by);
1070
+ };
1071
+
1072
+ // --- Backup / restore helpers exposed ---
1073
+ const restoreLatestBackup = async (key) => {
1074
+ return await authState.restoreLatestBackup(key);
1075
+ };
1076
+
1077
+ // --- Device fingerprinting ---
1078
+ const lockDeviceFingerprint = async () => {
1079
+ return await authState.lockDeviceFingerprint();
1080
+ };
1081
+
1082
+ const verifyDeviceFingerprint = (deviceObj) => {
1083
+ return authState.verifyDeviceFingerprint(deviceObj);
1084
+ };
1085
+
1086
+ // --- Accounts manager ---
1087
+ const registerAccount = async (name, folderPath) => {
1088
+ if (!authState.data.accountsIndex) authState.data.accountsIndex = {};
1089
+ authState.data.accountsIndex[name] = folderPath;
1090
+ await authState._writeFile('accountsIndex', authState.data.accountsIndex);
1091
+ authState.emit('account-registered', { name, folderPath });
1092
+ return authState.data.accountsIndex;
1093
+ };
1094
+
1095
+ const listAccounts = () => {
1096
+ return authState.getAccountsIndex();
1097
+ };
1098
+
1099
+ // --- Health check ---
1100
+ const getHealth = () => {
1101
+ return authState.getHealth();
1102
+ };
1103
+
1104
+ // --- Misc helpers: loginHistory & usageStats getters ---
1105
+ const getLoginHistory = () => authState.getLoginHistory();
1106
+ const getUsageStats = () => authState.getUsageStats();
1107
+
411
1108
  return {
412
1109
  state: authState,
413
-
1110
+
414
1111
  saveCreds,
415
1112
  loadCreds,
416
-
1113
+
417
1114
  saveMqttSession,
418
1115
  getMqttConnectOptions,
419
-
1116
+
420
1117
  clearSession,
421
1118
  isSessionValid,
422
1119
 
423
1120
  hasSession: () => authState.hasValidSession(),
424
1121
  hasMqttSession: () => authState.hasMqttSession(),
425
-
1122
+
426
1123
  folder,
427
-
428
- getData: () => authState.data
1124
+
1125
+ // pass-through for convenience
1126
+ getData: () => authState.data,
1127
+
1128
+ // convenience setters/getters for the new data
1129
+ setIrisState: (s) => authState.setIrisState(s),
1130
+ setMqttTopics: (t) => authState.setMqttTopics(t),
1131
+ setMqttCapabilities: (c) => authState.setMqttCapabilities(c),
1132
+ setMqttAuth: (a) => authState.setMqttAuth(a),
1133
+ setLastPublishIds: (p) => authState.setLastPublishIds(p),
1134
+ getIrisState: () => authState.getIrisState(),
1135
+ getMqttTopics: () => authState.getMqttTopics(),
1136
+ getMqttCapabilities: () => authState.getMqttCapabilities(),
1137
+ getMqttAuth: () => authState.getMqttAuth(),
1138
+ getLastPublishIds: () => authState.getLastPublishIds(),
1139
+
1140
+ // new utilities
1141
+ enableAutoRefresh,
1142
+ disableAutoRefresh,
1143
+ recordLoginAttempt,
1144
+ getLoginHistory,
1145
+ markAccountRestricted,
1146
+ clearAccountRestriction,
1147
+ incrementStat,
1148
+ getUsageStats,
1149
+ restoreLatestBackup,
1150
+ lockDeviceFingerprint,
1151
+ verifyDeviceFingerprint,
1152
+ registerAccount,
1153
+ listAccounts,
1154
+ getHealth
429
1155
  };
430
1156
  }
431
1157
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-insta-private-api-mqtt",
3
- "version": "1.1.10",
3
+ "version": "1.2.10",
4
4
  "description": "Complete Instagram MQTT protocol with FULL iOS + Android support. 33 device presets (21 iOS + 12 Android). iPhone 16/15/14/13/12, iPad Pro, Samsung, Pixel, Huawei. Real-time DM messaging, view-once media extraction, sub-500ms latency.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {