nodejs-insta-private-api-mqtt 1.2.10 → 1.3.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.
@@ -26,7 +26,16 @@ const FILE_NAMES = {
26
26
  // Added utility files
27
27
  loginHistory: 'login-history.json',
28
28
  usageStats: 'usage-stats.json',
29
- accountsIndex: 'accounts.json'
29
+ accountsIndex: 'accounts.json',
30
+
31
+ // MQTT helpers files
32
+ mqttHealth: 'mqtt-health.json',
33
+ mqttReconnect: 'mqtt-reconnect.json',
34
+ mqttMessageCache: 'mqtt-message-cache.json',
35
+ mqttOutbox: 'mqtt-outbox.json',
36
+ mqttSubscriptionHealth: 'mqtt-subscription-health.json',
37
+ mqttTraffic: 'mqtt-traffic.json',
38
+ mqttRisk: 'mqtt-risk.json'
30
39
  };
31
40
 
32
41
  class MultiFileAuthState extends EventEmitter {
@@ -42,6 +51,11 @@ class MultiFileAuthState extends EventEmitter {
42
51
  this._autoRefreshIntervalMs = 5 * 60 * 1000; // default 5 minutes
43
52
  this._autoRefreshClient = null;
44
53
 
54
+ // Reconnect/backoff runtime
55
+ this._reconnectBackoff = [2000, 5000, 15000, 60000];
56
+ this._reconnectAttempts = 0;
57
+ this._reconnectTimer = null;
58
+
45
59
  this.data = {
46
60
  creds: null,
47
61
  device: null,
@@ -61,7 +75,16 @@ class MultiFileAuthState extends EventEmitter {
61
75
  // utility
62
76
  loginHistory: null,
63
77
  usageStats: null,
64
- accountsIndex: null
78
+ accountsIndex: null,
79
+
80
+ // mqtt helpers
81
+ mqttHealth: null,
82
+ mqttReconnect: null,
83
+ mqttMessageCache: null,
84
+ mqttOutbox: null,
85
+ mqttSubscriptionHealth: null,
86
+ mqttTraffic: null,
87
+ mqttRisk: null
65
88
  };
66
89
 
67
90
  // ensure folder structure
@@ -90,6 +113,80 @@ class MultiFileAuthState extends EventEmitter {
90
113
  }
91
114
  }
92
115
 
116
+ // sanitize state to avoid nulls for key fields (useful to ensure instant non-null)
117
+ _sanitizeForSave(key, data) {
118
+ try {
119
+ if (!data) return data;
120
+ if (key === 'creds') {
121
+ if (typeof data.userId === 'undefined' || data.userId === null) data.userId = data.userId || 'pending';
122
+ if (typeof data.username === 'undefined' || data.username === null) data.username = data.username || 'pending';
123
+ if (typeof data.sessionId === 'undefined' || data.sessionId === null) data.sessionId = data.sessionId || `pending-${Date.now()}`;
124
+ if (typeof data.csrfToken === 'undefined' || data.csrfToken === null) data.csrfToken = data.csrfToken || 'pending';
125
+ if (typeof data.isLoggedIn === 'undefined') data.isLoggedIn = !!data.sessionId || !!data.authorization;
126
+ if (!data.loginAt) data.loginAt = data.loginAt || new Date().toISOString();
127
+ }
128
+ if (key === 'mqttSession') {
129
+ if (!data.sessionId) data.sessionId = data.sessionId || `local-mqtt-${Date.now()}`;
130
+ if (!data.mqttSessionId) data.mqttSessionId = data.mqttSessionId || 'boot';
131
+ if (!data.lastConnected) data.lastConnected = new Date().toISOString();
132
+ }
133
+ if (key === 'seqIds') {
134
+ if (typeof data.seq_id === 'undefined' || data.seq_id === null) data.seq_id = data.seq_id || 0;
135
+ if (typeof data.snapshot_at_ms === 'undefined' || data.snapshot_at_ms === null) data.snapshot_at_ms = data.snapshot_at_ms || Date.now();
136
+ }
137
+ if (key === 'mqttTopics') {
138
+ if (!data.topics) data.topics = data.topics || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync', 'presence_subscribe'];
139
+ if (!data.updatedAt) data.updatedAt = new Date().toISOString();
140
+ }
141
+ if (key === 'mqttCapabilities') {
142
+ data.supportsTyping = Boolean(data.supportsTyping ?? true);
143
+ data.supportsReactions = Boolean(data.supportsReactions ?? true);
144
+ data.supportsVoice = Boolean(data.supportsVoice ?? false);
145
+ data.protocolVersion = data.protocolVersion || 3;
146
+ data.collectedAt = data.collectedAt || new Date().toISOString();
147
+ }
148
+ if (key === 'mqttHealth') {
149
+ data.lastPingAt = data.lastPingAt || null;
150
+ data.lastPongAt = data.lastPongAt || null;
151
+ data.latencyMs = data.latencyMs || 0;
152
+ data.missedPings = data.missedPings || 0;
153
+ data.status = data.status || 'unknown';
154
+ }
155
+ if (key === 'mqttMessageCache') {
156
+ if (!Array.isArray(data.lastIds)) data.lastIds = [];
157
+ if (!data.max) data.max = data.max || 500;
158
+ }
159
+ if (key === 'mqttOutbox') {
160
+ if (!Array.isArray(data)) return [];
161
+ }
162
+ if (key === 'mqttSubscriptionHealth') {
163
+ data.expected = data.expected || (this.getMqttTopics()?.topics || ['ig_sub_direct']);
164
+ data.active = data.active || data.expected.slice();
165
+ data.lastCheck = data.lastCheck || new Date().toISOString();
166
+ data.needsResubscribe = data.needsResubscribe || false;
167
+ }
168
+ if (key === 'mqttTraffic') {
169
+ data.messagesInPerMin = data.messagesInPerMin || 0;
170
+ data.messagesOutPerMin = data.messagesOutPerMin || 0;
171
+ data.lastReset = data.lastReset || new Date().toISOString();
172
+ }
173
+ if (key === 'mqttReconnect') {
174
+ data.attempts = data.attempts || 0;
175
+ data.lastReason = data.lastReason || null;
176
+ data.lastAttemptAt = data.lastAttemptAt || null;
177
+ data.nextRetryInMs = data.nextRetryInMs || 0;
178
+ }
179
+ if (key === 'mqttRisk') {
180
+ data.riskScore = typeof data.riskScore === 'number' ? data.riskScore : 0;
181
+ data.level = data.level || 'low';
182
+ data.updatedAt = data.updatedAt || new Date().toISOString();
183
+ }
184
+ } catch (e) {
185
+ // ignore sanitize errors
186
+ }
187
+ return data;
188
+ }
189
+
93
190
  // create a timestamped backup of existing file (if present)
94
191
  async _createBackup(key) {
95
192
  try {
@@ -133,9 +230,11 @@ class MultiFileAuthState extends EventEmitter {
133
230
  this._ensureFolder();
134
231
  const filePath = this._getFilePath(key);
135
232
  try {
233
+ // sanitize to avoid nulls for key fields
234
+ const sanitized = this._sanitizeForSave(key, JSON.parse(JSON.stringify(data)));
136
235
  // create backup before overwriting existing file
137
236
  await this._createBackup(key);
138
- await this._writeFileAtomic(filePath, data);
237
+ await this._writeFileAtomic(filePath, sanitized);
139
238
  return true;
140
239
  } catch (e) {
141
240
  console.error(`[MultiFileAuthState] Error writing ${key}:`, e.message);
@@ -189,6 +288,15 @@ class MultiFileAuthState extends EventEmitter {
189
288
  };
190
289
  if (!this.data.accountsIndex) this.data.accountsIndex = {};
191
290
 
291
+ // ensure mqtt helper objects exist
292
+ if (!this.data.mqttHealth) this.data.mqttHealth = { lastPingAt: null, lastPongAt: null, latencyMs: 0, missedPings: 0, status: 'unknown' };
293
+ if (!this.data.mqttReconnect) this.data.mqttReconnect = { attempts: 0, lastReason: null, lastAttemptAt: null, nextRetryInMs: 0 };
294
+ if (!this.data.mqttMessageCache) this.data.mqttMessageCache = { lastIds: [], max: 500 };
295
+ if (!this.data.mqttOutbox) this.data.mqttOutbox = [];
296
+ if (!this.data.mqttSubscriptionHealth) this.data.mqttSubscriptionHealth = { expected: (this.data.mqttTopics?.topics || ['ig_sub_direct']), active: (this.data.mqttTopics?.topics || ['ig_sub_direct']).slice(), lastCheck: new Date().toISOString(), needsResubscribe: false };
297
+ if (!this.data.mqttTraffic) this.data.mqttTraffic = { messagesInPerMin: 0, messagesOutPerMin: 0, lastReset: new Date().toISOString() };
298
+ if (!this.data.mqttRisk) this.data.mqttRisk = { riskScore: 0, level: 'low', updatedAt: new Date().toISOString() };
299
+
192
300
  return {
193
301
  creds: this.data.creds,
194
302
  device: this.data.device,
@@ -205,6 +313,13 @@ class MultiFileAuthState extends EventEmitter {
205
313
  loginHistory: this.data.loginHistory,
206
314
  usageStats: this.data.usageStats,
207
315
  accountsIndex: this.data.accountsIndex,
316
+ mqttHealth: this.data.mqttHealth,
317
+ mqttReconnect: this.data.mqttReconnect,
318
+ mqttMessageCache: this.data.mqttMessageCache,
319
+ mqttOutbox: this.data.mqttOutbox,
320
+ mqttSubscriptionHealth: this.data.mqttSubscriptionHealth,
321
+ mqttTraffic: this.data.mqttTraffic,
322
+ mqttRisk: this.data.mqttRisk,
208
323
  hasSession: !!(this.data.creds && this.data.cookies)
209
324
  };
210
325
  }
@@ -225,6 +340,15 @@ class MultiFileAuthState extends EventEmitter {
225
340
  if (this.data.accountsIndex) {
226
341
  try { await this._writeFile('accountsIndex', this.data.accountsIndex); } catch (e) {}
227
342
  }
343
+ // mqtt helpers
344
+ try { await this._writeFile('mqttHealth', this.data.mqttHealth); } catch (e) {}
345
+ try { await this._writeFile('mqttReconnect', this.data.mqttReconnect); } catch (e) {}
346
+ try { await this._writeFile('mqttMessageCache', this.data.mqttMessageCache); } catch (e) {}
347
+ try { await this._writeFile('mqttOutbox', this.data.mqttOutbox); } catch (e) {}
348
+ try { await this._writeFile('mqttSubscriptionHealth', this.data.mqttSubscriptionHealth); } catch (e) {}
349
+ try { await this._writeFile('mqttTraffic', this.data.mqttTraffic); } catch (e) {}
350
+ try { await this._writeFile('mqttRisk', this.data.mqttRisk); } catch (e) {}
351
+
228
352
  await Promise.all(savePromises);
229
353
  }
230
354
 
@@ -254,6 +378,15 @@ class MultiFileAuthState extends EventEmitter {
254
378
  if (this.data.mqttAuth) promises.push(this._writeFile('mqttAuth', this.data.mqttAuth));
255
379
  if (this.data.lastPublishIds) promises.push(this._writeFile('lastPublishIds', this.data.lastPublishIds));
256
380
 
381
+ // mqtt helpers
382
+ promises.push(this._writeFile('mqttHealth', this.data.mqttHealth));
383
+ promises.push(this._writeFile('mqttReconnect', this.data.mqttReconnect));
384
+ promises.push(this._writeFile('mqttMessageCache', this.data.mqttMessageCache));
385
+ promises.push(this._writeFile('mqttOutbox', this.data.mqttOutbox));
386
+ promises.push(this._writeFile('mqttSubscriptionHealth', this.data.mqttSubscriptionHealth));
387
+ promises.push(this._writeFile('mqttTraffic', this.data.mqttTraffic));
388
+ promises.push(this._writeFile('mqttRisk', this.data.mqttRisk));
389
+
257
390
  await Promise.all(promises);
258
391
  this.emit('mqtt-state-saved');
259
392
  }
@@ -382,6 +515,445 @@ class MultiFileAuthState extends EventEmitter {
382
515
  this._debouncedSave(['usageStats']);
383
516
  }
384
517
 
518
+ // --- MQTT helper methods ---
519
+
520
+ // update mqtt health and persist
521
+ _updateMqttHealth(partial) {
522
+ try {
523
+ if (!this.data.mqttHealth) this.data.mqttHealth = {};
524
+ Object.assign(this.data.mqttHealth, partial);
525
+ // recompute status
526
+ if (this.data.mqttHealth.missedPings > 3) {
527
+ this.data.mqttHealth.status = 'degraded';
528
+ } else if (this.data.mqttHealth.lastPongAt) {
529
+ this.data.mqttHealth.status = 'ok';
530
+ } else {
531
+ this.data.mqttHealth.status = this.data.mqttHealth.status || 'unknown';
532
+ }
533
+ this._debouncedSave(['mqttHealth']);
534
+ } catch (e) {}
535
+ }
536
+
537
+ // mark ping sent
538
+ markPing() {
539
+ this._updateMqttHealth({ lastPingAt: new Date().toISOString() });
540
+ }
541
+
542
+ // mark pong received and update latency
543
+ markPong() {
544
+ const now = Date.now();
545
+ const lastPing = this.data.mqttHealth?.lastPingAt ? new Date(this.data.mqttHealth.lastPingAt).getTime() : null;
546
+ const latency = lastPing ? (now - lastPing) : 0;
547
+ const missed = Math.max(0, (this.data.mqttHealth?.missedPings || 0) - 1);
548
+ this._updateMqttHealth({ lastPongAt: new Date().toISOString(), latencyMs: latency, missedPings: missed });
549
+ }
550
+
551
+ // increment missed ping
552
+ markMissedPing() {
553
+ const missed = (this.data.mqttHealth?.missedPings || 0) + 1;
554
+ this._updateMqttHealth({ missedPings: missed });
555
+ }
556
+
557
+ // message deduplication: return true if duplicate
558
+ isDuplicateMessage(messageId) {
559
+ if (!messageId) return false;
560
+ if (!this.data.mqttMessageCache) this.data.mqttMessageCache = { lastIds: [], max: 500 };
561
+ const idx = this.data.mqttMessageCache.lastIds.indexOf(messageId);
562
+ if (idx !== -1) return true;
563
+ // push and cap
564
+ this.data.mqttMessageCache.lastIds.push(messageId);
565
+ if (this.data.mqttMessageCache.lastIds.length > (this.data.mqttMessageCache.max || 500)) {
566
+ this.data.mqttMessageCache.lastIds.shift();
567
+ }
568
+ this._debouncedSave(['mqttMessageCache']);
569
+ return false;
570
+ }
571
+
572
+ // enqueue outgoing message to outbox
573
+ enqueueOutbox(item) {
574
+ if (!this.data.mqttOutbox) this.data.mqttOutbox = [];
575
+ // item: { topic, payload, qos, createdAt, tries }
576
+ const i = Object.assign({ qos: 1, createdAt: new Date().toISOString(), tries: 0 }, item);
577
+ this.data.mqttOutbox.push(i);
578
+ this._debouncedSave(['mqttOutbox']);
579
+ this.emit('outbox-enqueued', i);
580
+ return i;
581
+ }
582
+
583
+ // flush outbox (attempt send). Tries to use common publish/send methods on client
584
+ async flushOutbox(realtimeClient, maxPerRun = 50) {
585
+ if (!this.data.mqttOutbox || !this.data.mqttOutbox.length) return 0;
586
+ const sent = [];
587
+ const keep = [];
588
+ let processed = 0;
589
+ for (const item of this.data.mqttOutbox) {
590
+ if (processed >= maxPerRun) {
591
+ keep.push(item);
592
+ continue;
593
+ }
594
+ processed++;
595
+ try {
596
+ item.tries = (item.tries || 0) + 1;
597
+ // try publish methods in order of likelihood
598
+ let ok = false;
599
+ if (realtimeClient && typeof realtimeClient.publish === 'function') {
600
+ // mqtt.js style
601
+ try {
602
+ realtimeClient.publish(item.topic, typeof item.payload === 'string' ? item.payload : JSON.stringify(item.payload), { qos: item.qos });
603
+ ok = true;
604
+ } catch (e) {
605
+ ok = false;
606
+ }
607
+ } else if (realtimeClient && typeof realtimeClient.send === 'function') {
608
+ try {
609
+ realtimeClient.send(item.topic, item.payload);
610
+ ok = true;
611
+ } catch (e) { ok = false; }
612
+ } else if (realtimeClient && typeof realtimeClient.sendMessage === 'function') {
613
+ try {
614
+ realtimeClient.sendMessage(item.payload);
615
+ ok = true;
616
+ } catch (e) { ok = false; }
617
+ } else {
618
+ // cannot send: keep in outbox
619
+ ok = false;
620
+ }
621
+
622
+ if (ok) {
623
+ sent.push(item);
624
+ this.incrementStat('mqttMessages', 1);
625
+ } else {
626
+ // keep for retry, but if tries exceed threshold move to dead-letter (drop)
627
+ if (item.tries >= 10) {
628
+ this.emit('outbox-dropped', item);
629
+ } else {
630
+ keep.push(item);
631
+ }
632
+ }
633
+ } catch (e) {
634
+ keep.push(item);
635
+ }
636
+ }
637
+ // replace queue
638
+ this.data.mqttOutbox = keep.concat(this.data.mqttOutbox.slice(processed));
639
+ this._debouncedSave(['mqttOutbox']);
640
+ this.emit('outbox-flushed', { sentCount: sent.length, remaining: this.data.mqttOutbox.length });
641
+ return sent.length;
642
+ }
643
+
644
+ // subscription watchdog: check and attempt resubscribe
645
+ async checkSubscriptions(realtimeClient) {
646
+ try {
647
+ const expected = this.data.mqttSubscriptionHealth?.expected || (this.data.mqttTopics?.topics || []);
648
+ let active = [];
649
+ // try to read from client
650
+ if (realtimeClient?.connection?.subscribedTopics) active = realtimeClient.connection.subscribedTopics;
651
+ else if (realtimeClient?._subscribedTopics) active = realtimeClient._subscribedTopics;
652
+ else if (Array.isArray(this.data.mqttTopics?.topics)) active = this.data.mqttTopics.topics;
653
+
654
+ const needs = expected.filter(t => !active.includes(t));
655
+ this.data.mqttSubscriptionHealth = {
656
+ expected,
657
+ active,
658
+ lastCheck: new Date().toISOString(),
659
+ needsResubscribe: needs.length > 0
660
+ };
661
+ this._debouncedSave(['mqttSubscriptionHealth']);
662
+
663
+ if (needs.length && realtimeClient) {
664
+ for (const t of needs) {
665
+ try {
666
+ // call common subscribe methods
667
+ if (typeof realtimeClient.subscribe === 'function') {
668
+ realtimeClient.subscribe(t);
669
+ } else if (realtimeClient.connection && typeof realtimeClient.connection.subscribe === 'function') {
670
+ realtimeClient.connection.subscribe(t);
671
+ } else if (typeof realtimeClient.subscriptions === 'function') {
672
+ realtimeClient.subscriptions([t]);
673
+ }
674
+ // update active
675
+ if (!this.data.mqttSubscriptionHealth.active.includes(t)) this.data.mqttSubscriptionHealth.active.push(t);
676
+ this.emit('resubscribe-attempt', { topic: t });
677
+ } catch (e) {
678
+ this.emit('resubscribe-failed', { topic: t, error: e.message });
679
+ }
680
+ }
681
+ this._debouncedSave(['mqttSubscriptionHealth']);
682
+ }
683
+ return this.data.mqttSubscriptionHealth;
684
+ } catch (e) {
685
+ return null;
686
+ }
687
+ }
688
+
689
+ // traffic profiler: update incoming/outgoing counters
690
+ _updateTraffic(incoming = 0, outgoing = 0) {
691
+ try {
692
+ if (!this.data.mqttTraffic) this.data.mqttTraffic = { messagesInPerMin: 0, messagesOutPerMin: 0, lastReset: new Date().toISOString() };
693
+ const lastReset = new Date(this.data.mqttTraffic.lastReset).getTime();
694
+ if (Date.now() - lastReset > 60 * 1000) {
695
+ // reset every minute
696
+ this.data.mqttTraffic.messagesInPerMin = 0;
697
+ this.data.mqttTraffic.messagesOutPerMin = 0;
698
+ this.data.mqttTraffic.lastReset = new Date().toISOString();
699
+ }
700
+ this.data.mqttTraffic.messagesInPerMin += incoming;
701
+ this.data.mqttTraffic.messagesOutPerMin += outgoing;
702
+ this._debouncedSave(['mqttTraffic']);
703
+ } catch (e) {}
704
+ }
705
+
706
+ // compute simple risk score from reconnects, errors, traffic
707
+ _computeRiskScore() {
708
+ try {
709
+ const reconnects = this.data.usageStats?.reconnects || 0;
710
+ const errors = this.data.usageStats?.errors || 0;
711
+ const outPerMin = this.data.mqttTraffic?.messagesOutPerMin || 0;
712
+ // simple heuristic
713
+ let score = 0;
714
+ score += Math.min(1, reconnects / 10) * 0.4;
715
+ score += Math.min(1, errors / 20) * 0.3;
716
+ score += Math.min(1, outPerMin / 200) * 0.3;
717
+ const level = score > 0.7 ? 'high' : score > 0.35 ? 'medium' : 'low';
718
+ this.data.mqttRisk = { riskScore: Number(score.toFixed(3)), level, updatedAt: new Date().toISOString() };
719
+ this._debouncedSave(['mqttRisk']);
720
+ this.emit('risk-updated', this.data.mqttRisk);
721
+ return this.data.mqttRisk;
722
+ } catch (e) {
723
+ return null;
724
+ }
725
+ }
726
+
727
+ // schedule auto reconnect using backoff; will attempt to call client.reconnect() if available or emit event
728
+ _scheduleReconnect(realtimeClient, reason = 'unknown') {
729
+ try {
730
+ this._reconnectAttempts = (this._reconnectAttempts || 0) + 1;
731
+ const idx = Math.min(this._reconnectAttempts - 1, this._reconnectBackoff.length - 1);
732
+ const delay = this._reconnectBackoff[idx] || this._reconnectBackoff[this._reconnectBackoff.length - 1];
733
+ this.data.mqttReconnect = {
734
+ attempts: this._reconnectAttempts,
735
+ lastReason: reason,
736
+ lastAttemptAt: new Date().toISOString(),
737
+ nextRetryInMs: delay
738
+ };
739
+ this.incrementStat('reconnects', 1);
740
+ this._debouncedSave(['mqttReconnect']);
741
+ if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
742
+ this._reconnectTimer = setTimeout(async () => {
743
+ try {
744
+ if (realtimeClient) {
745
+ if (typeof realtimeClient.reconnect === 'function') {
746
+ realtimeClient.reconnect();
747
+ } else if (typeof realtimeClient.connect === 'function') {
748
+ // some libs have connect
749
+ realtimeClient.connect();
750
+ } else {
751
+ // can't auto-call: emit event so app can handle
752
+ this.emit('should-reconnect', { reason });
753
+ }
754
+ } else {
755
+ this.emit('should-reconnect', { reason });
756
+ }
757
+ } catch (e) {
758
+ this.emit('reconnect-failed', { reason, error: e.message });
759
+ }
760
+ }, delay);
761
+ } catch (e) {}
762
+ }
763
+
764
+ // reset reconnect attempts after successful connect
765
+ _resetReconnect() {
766
+ this._reconnectAttempts = 0;
767
+ if (this._reconnectTimer) {
768
+ clearTimeout(this._reconnectTimer);
769
+ this._reconnectTimer = null;
770
+ }
771
+ if (this.data.mqttReconnect) {
772
+ this.data.mqttReconnect.attempts = 0;
773
+ this.data.mqttReconnect.nextRetryInMs = 0;
774
+ this._debouncedSave(['mqttReconnect']);
775
+ }
776
+ }
777
+
778
+ // warm-restore: apply saved options into realtimeClient before connect to speed startup
779
+ warmRestoreClient(realtimeClient) {
780
+ try {
781
+ const opts = this.getMqttConnectOptions();
782
+ if (!opts) return false;
783
+ // Merge irisData, mqttAuthToken etc. into client init options if possible
784
+ if (realtimeClient.initOptions && typeof realtimeClient.initOptions === 'object') {
785
+ Object.assign(realtimeClient.initOptions, opts);
786
+ } else {
787
+ realtimeClient.initOptions = opts;
788
+ }
789
+ // also set connection clientInfo if supported
790
+ if (realtimeClient.connection && realtimeClient.connection.clientInfo && this.data.mqttSession?.mqttSessionId) {
791
+ realtimeClient.connection.clientInfo.clientMqttSessionId = this.data.mqttSession.mqttSessionId;
792
+ }
793
+ this.emit('warm-restore', { options: opts });
794
+ return true;
795
+ } catch (e) {
796
+ return false;
797
+ }
798
+ }
799
+
800
+ // attach runtime helpers to realtimeClient to manage instant improvements
801
+ attachRealtimeClient(realtimeClient, opts = {}) {
802
+ try {
803
+ if (!realtimeClient) return;
804
+
805
+ // warm restore immediately
806
+ try { this.warmRestoreClient(realtimeClient); } catch (e) {}
807
+
808
+ // ping/pong monitoring
809
+ const pingIntervalMs = opts.pingIntervalMs || 30 * 1000; // 30s
810
+ let pingTimer = null;
811
+ const startPing = () => {
812
+ if (pingTimer) clearInterval(pingTimer);
813
+ pingTimer = setInterval(() => {
814
+ try {
815
+ this.markPing();
816
+ if (typeof realtimeClient.ping === 'function') {
817
+ realtimeClient.ping();
818
+ } else if (typeof realtimeClient.pingReq === 'function') {
819
+ realtimeClient.pingReq();
820
+ } else if (typeof realtimeClient.pingServer === 'function') {
821
+ realtimeClient.pingServer();
822
+ } else {
823
+ // some libraries use publish to $SYS topic or nothing; we still mark ping
824
+ }
825
+ // after ping, schedule missed ping detection
826
+ setTimeout(() => {
827
+ // if lastPongAt older than lastPingAt, consider missed
828
+ const lastPing = new Date(this.data.mqttHealth?.lastPingAt || 0).getTime();
829
+ const lastPong = new Date(this.data.mqttHealth?.lastPongAt || 0).getTime();
830
+ if (lastPing && (!lastPong || lastPong < lastPing)) {
831
+ this.markMissedPing();
832
+ }
833
+ }, Math.min(5000, Math.floor(pingIntervalMs / 2)));
834
+ } catch (e) {}
835
+ }, pingIntervalMs);
836
+ };
837
+ const stopPing = () => { if (pingTimer) clearInterval(pingTimer); pingTimer = null; };
838
+
839
+ // event handlers (subscribe to multiple common event names)
840
+ const onConnect = async (...args) => {
841
+ this._resetReconnect();
842
+ this.data.mqttSession = this.data.mqttSession || {};
843
+ this.data.mqttSession.lastConnected = new Date().toISOString();
844
+ this._debouncedSave(['mqttSession']);
845
+ this._updateMqttHealth({ status: 'ok', lastPongAt: new Date().toISOString(), missedPings: 0 });
846
+ this.emit('realtime-connected', { args });
847
+ // flush outbox on connect
848
+ try { await this.flushOutbox(realtimeClient); } catch (e) {}
849
+ // check subscriptions
850
+ try { await this.checkSubscriptions(realtimeClient); } catch (e) {}
851
+ startPing();
852
+ };
853
+
854
+ const onClose = (...args) => {
855
+ stopPing();
856
+ this._updateMqttHealth({ status: 'dead' });
857
+ this._scheduleReconnect(realtimeClient, 'close');
858
+ this.emit('realtime-closed', { args });
859
+ };
860
+
861
+ const onReconnect = (...args) => {
862
+ this.emit('realtime-reconnect', { args });
863
+ };
864
+
865
+ const onOffline = (...args) => {
866
+ this._updateMqttHealth({ status: 'degraded' });
867
+ this._scheduleReconnect(realtimeClient, 'offline');
868
+ this.emit('realtime-offline', { args });
869
+ };
870
+
871
+ const onError = (err) => {
872
+ this.incrementStat('errors', 1);
873
+ this._scheduleReconnect(realtimeClient, err?.message || 'error');
874
+ this.emit('realtime-error', { error: err?.message || String(err) });
875
+ };
876
+
877
+ const onMessage = async (topic, payload, packet) => {
878
+ // generic message handler: try to extract id for dedupe
879
+ // Many IG messages contain an id field in JSON payload; try to parse
880
+ let messageId = null;
881
+ try {
882
+ const s = (typeof payload === 'string') ? payload : (payload && payload.toString ? payload.toString() : null);
883
+ if (s) {
884
+ const j = JSON.parse(s);
885
+ if (j && (j.message_id || j.id || j.msg_id)) messageId = j.message_id || j.id || j.msg_id;
886
+ }
887
+ } catch (e) {
888
+ // not json, ignore
889
+ }
890
+ if (messageId && this.isDuplicateMessage(messageId)) {
891
+ this.emit('message-duplicate', { topic, messageId });
892
+ return;
893
+ }
894
+ // update traffic counters
895
+ this._updateTraffic(1, 0);
896
+ this.incrementStat('mqttMessages', 1);
897
+ // mark pong if message looks like pong or heartbeat
898
+ if (topic && topic.includes('pong')) this.markPong();
899
+ this.emit('realtime-message', { topic, payload, packet });
900
+ };
901
+
902
+ // wire up events safely for common handlers
903
+ try {
904
+ // mqtt.js style
905
+ if (typeof realtimeClient.on === 'function') {
906
+ realtimeClient.on('connect', onConnect);
907
+ realtimeClient.on('reconnect', onReconnect);
908
+ realtimeClient.on('close', onClose);
909
+ realtimeClient.on('offline', onOffline);
910
+ realtimeClient.on('error', onError);
911
+ realtimeClient.on('message', onMessage);
912
+ }
913
+ // websockets or other: attempt to attach possible event names
914
+ if (realtimeClient.addEventListener && typeof realtimeClient.addEventListener === 'function') {
915
+ try { realtimeClient.addEventListener('open', onConnect); } catch (e) {}
916
+ try { realtimeClient.addEventListener('close', onClose); } catch (e) {}
917
+ try { realtimeClient.addEventListener('error', onError); } catch (e) {}
918
+ try { realtimeClient.addEventListener('message', (ev) => onMessage(ev.topic || ev.type, ev.data || ev.payload, ev)); } catch (e) {}
919
+ }
920
+ } catch (e) {}
921
+
922
+ // also attach one-time status: if the client supports events differently, let app listen to 'should-reconnect'
923
+ // attach a cleanup handle
924
+ const cleanup = () => {
925
+ stopPing();
926
+ try {
927
+ if (typeof realtimeClient.off === 'function') {
928
+ realtimeClient.off('connect', onConnect);
929
+ realtimeClient.off('reconnect', onReconnect);
930
+ realtimeClient.off('close', onClose);
931
+ realtimeClient.off('offline', onOffline);
932
+ realtimeClient.off('error', onError);
933
+ realtimeClient.off('message', onMessage);
934
+ }
935
+ } catch (e) {}
936
+ this.emit('realtime-detach');
937
+ };
938
+
939
+ // expose cleanup on client for convenience
940
+ try { realtimeClient._authStateCleanup = cleanup; } catch (e) {}
941
+
942
+ // return helper object to caller
943
+ return {
944
+ pingIntervalMs,
945
+ stop: cleanup,
946
+ flushOutbox: async (max) => { return await this.flushOutbox(realtimeClient, max); },
947
+ checkSubscriptions: async () => { return await this.checkSubscriptions(realtimeClient); },
948
+ warmRestore: () => { return this.warmRestoreClient(realtimeClient); }
949
+ };
950
+ } catch (e) {
951
+ // emit attach failure
952
+ this.emit('attach-failed', { error: e.message });
953
+ return null;
954
+ }
955
+ }
956
+
385
957
  // --- Getters ---
386
958
  getCreds() { return this.data.creds; }
387
959
  getDevice() { return this.data.device; }
@@ -398,6 +970,13 @@ class MultiFileAuthState extends EventEmitter {
398
970
  getLoginHistory() { return this.data.loginHistory || []; }
399
971
  getUsageStats() { return this.data.usageStats || {}; }
400
972
  getAccountsIndex() { return this.data.accountsIndex || {}; }
973
+ getMqttHealth() { return this.data.mqttHealth || {}; }
974
+ getMqttReconnect() { return this.data.mqttReconnect || {}; }
975
+ getMqttMessageCache() { return this.data.mqttMessageCache || {}; }
976
+ getMqttOutbox() { return this.data.mqttOutbox || []; }
977
+ getMqttSubscriptionHealth() { return this.data.mqttSubscriptionHealth || {}; }
978
+ getMqttTraffic() { return this.data.mqttTraffic || {}; }
979
+ getMqttRisk() { return this.data.mqttRisk || {}; }
401
980
 
402
981
  hasValidSession() {
403
982
  return !!(this.data.creds && this.data.cookies && this.data.creds.authorization);
@@ -517,7 +1096,9 @@ class MultiFileAuthState extends EventEmitter {
517
1096
  lastLogin: (this.data.loginHistory && this.data.loginHistory[0]?.date) || null,
518
1097
  usageStats: this.getUsageStats(),
519
1098
  accountStatus: this.data.creds?.accountStatus || 'ok',
520
- deviceLocked: !!this.data.device?.locked
1099
+ deviceLocked: !!this.data.device?.locked,
1100
+ mqttHealth: this.getMqttHealth(),
1101
+ mqttRisk: this.getMqttRisk()
521
1102
  };
522
1103
  }
523
1104
  }
@@ -947,7 +1528,15 @@ async function useMultiFileAuthState(folder) {
947
1528
 
948
1529
  const getMqttConnectOptions = () => {
949
1530
  if (!authState.hasMqttSession()) {
950
- return null;
1531
+ // still return a warm default to help warm-restore
1532
+ const defaultTopics = ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'];
1533
+ return {
1534
+ graphQlSubs: defaultTopics,
1535
+ skywalkerSubs: ['presence_subscribe', 'typing_subscribe'],
1536
+ irisData: { seq_id: authState.getSeqIds()?.seq_id || 0, snapshot_at_ms: authState.getSeqIds()?.snapshot_at_ms || Date.now() },
1537
+ mqttAuthToken: authState.getMqttAuth()?.jwt || null,
1538
+ mqttAuthExpiresAt: authState.getMqttAuth()?.expiresAt || null
1539
+ };
951
1540
  }
952
1541
 
953
1542
  const subs = authState.getSubscriptions() || {};
@@ -960,7 +1549,7 @@ async function useMultiFileAuthState(folder) {
960
1549
  irisData: seqIds.seq_id ? {
961
1550
  seq_id: seqIds.seq_id,
962
1551
  snapshot_at_ms: seqIds.snapshot_at_ms
963
- } : null,
1552
+ } : { seq_id: 0, snapshot_at_ms: Date.now() },
964
1553
  mqttAuthToken: mqttAuth?.jwt || null,
965
1554
  mqttAuthExpiresAt: mqttAuth?.expiresAt || null
966
1555
  };
@@ -1101,6 +1690,23 @@ async function useMultiFileAuthState(folder) {
1101
1690
  return authState.getHealth();
1102
1691
  };
1103
1692
 
1693
+ // --- MQTT helpers exposing internal features ---
1694
+ const attachRealtimeClient = (realtimeClient, opts = {}) => {
1695
+ return authState.attachRealtimeClient(realtimeClient, opts);
1696
+ };
1697
+
1698
+ const enqueueOutbox = (item) => authState.enqueueOutbox(item);
1699
+ const flushOutbox = (client, max) => authState.flushOutbox(client, max);
1700
+ const checkSubscriptions = (client) => authState.checkSubscriptions(client);
1701
+ const computeRisk = () => authState._computeRiskScore();
1702
+ const warmRestore = (client) => authState.warmRestoreClient(client);
1703
+ const getMqttHealth = () => authState.getMqttHealth();
1704
+ const getMqttTraffic = () => authState.getMqttTraffic();
1705
+ const getMqttRisk = () => authState.getMqttRisk();
1706
+ const isDuplicateMessage = (id) => authState.isDuplicateMessage(id);
1707
+ const markPing = () => authState.markPing();
1708
+ const markPong = () => authState.markPong();
1709
+
1104
1710
  // --- Misc helpers: loginHistory & usageStats getters ---
1105
1711
  const getLoginHistory = () => authState.getLoginHistory();
1106
1712
  const getUsageStats = () => authState.getUsageStats();
@@ -1151,7 +1757,21 @@ async function useMultiFileAuthState(folder) {
1151
1757
  verifyDeviceFingerprint,
1152
1758
  registerAccount,
1153
1759
  listAccounts,
1154
- getHealth
1760
+ getHealth,
1761
+
1762
+ // mqtt helpers
1763
+ attachRealtimeClient,
1764
+ enqueueOutbox,
1765
+ flushOutbox,
1766
+ checkSubscriptions,
1767
+ computeRisk,
1768
+ warmRestore,
1769
+ getMqttHealth,
1770
+ getMqttTraffic,
1771
+ getMqttRisk,
1772
+ isDuplicateMessage,
1773
+ markPing,
1774
+ markPong
1155
1775
  };
1156
1776
  }
1157
1777
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-insta-private-api-mqtt",
3
- "version": "1.2.10",
3
+ "version": "1.3.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": {