nodejs-insta-private-api-mqtt 1.1.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.
@@ -5,15 +5,37 @@ 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',
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'
17
39
  };
18
40
 
19
41
  class MultiFileAuthState extends EventEmitter {
@@ -23,7 +45,17 @@ class MultiFileAuthState extends EventEmitter {
23
45
  this._saveDebounceTimer = null;
24
46
  this._saveDebounceMs = 500;
25
47
  this._dirty = new Set();
26
-
48
+
49
+ // Auto-refresh fields
50
+ this._autoRefreshTimer = null;
51
+ this._autoRefreshIntervalMs = 5 * 60 * 1000; // default 5 minutes
52
+ this._autoRefreshClient = null;
53
+
54
+ // Reconnect/backoff runtime
55
+ this._reconnectBackoff = [2000, 5000, 15000, 60000];
56
+ this._reconnectAttempts = 0;
57
+ this._reconnectTimer = null;
58
+
27
59
  this.data = {
28
60
  creds: null,
29
61
  device: null,
@@ -31,20 +63,149 @@ class MultiFileAuthState extends EventEmitter {
31
63
  mqttSession: null,
32
64
  subscriptions: null,
33
65
  seqIds: null,
34
- appState: null
66
+ appState: null,
67
+
68
+ // new
69
+ irisState: null,
70
+ mqttTopics: null,
71
+ mqttCapabilities: null,
72
+ mqttAuth: null,
73
+ lastPublishIds: null,
74
+
75
+ // utility
76
+ loginHistory: null,
77
+ usageStats: 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
35
88
  };
89
+
90
+ // ensure folder structure
91
+ this._ensureFolder();
92
+ this._ensureBackupFolder();
36
93
  }
37
94
 
38
95
  _getFilePath(key) {
39
96
  return path.join(this.folder, FILE_NAMES[key]);
40
97
  }
41
98
 
99
+ _getBackupFolder() {
100
+ return path.join(this.folder, 'backup');
101
+ }
102
+
42
103
  _ensureFolder() {
43
104
  if (!fs.existsSync(this.folder)) {
44
105
  fs.mkdirSync(this.folder, { recursive: true, mode: 0o700 });
45
106
  }
46
107
  }
47
108
 
109
+ _ensureBackupFolder() {
110
+ const b = this._getBackupFolder();
111
+ if (!fs.existsSync(b)) {
112
+ fs.mkdirSync(b, { recursive: true, mode: 0o700 });
113
+ }
114
+ }
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
+
190
+ // create a timestamped backup of existing file (if present)
191
+ async _createBackup(key) {
192
+ try {
193
+ const filePath = this._getFilePath(key);
194
+ if (fs.existsSync(filePath)) {
195
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
196
+ const base = path.basename(filePath);
197
+ const dest = path.join(this._getBackupFolder(), `${base}.${ts}.bak`);
198
+ await fs.promises.copyFile(filePath, dest);
199
+ this.emit('backup-created', { key, file: dest });
200
+ return dest;
201
+ }
202
+ } catch (e) {
203
+ // ignore errors in backup process but log
204
+ console.warn('[MultiFileAuthState] Backup creation failed for', key, e.message);
205
+ }
206
+ return null;
207
+ }
208
+
48
209
  async _writeFileAtomic(filePath, data) {
49
210
  const tempPath = filePath + '.tmp';
50
211
  const jsonData = JSON.stringify(data, null, 2);
@@ -69,7 +230,11 @@ class MultiFileAuthState extends EventEmitter {
69
230
  this._ensureFolder();
70
231
  const filePath = this._getFilePath(key);
71
232
  try {
72
- await this._writeFileAtomic(filePath, data);
233
+ // sanitize to avoid nulls for key fields
234
+ const sanitized = this._sanitizeForSave(key, JSON.parse(JSON.stringify(data)));
235
+ // create backup before overwriting existing file
236
+ await this._createBackup(key);
237
+ await this._writeFileAtomic(filePath, sanitized);
73
238
  return true;
74
239
  } catch (e) {
75
240
  console.error(`[MultiFileAuthState] Error writing ${key}:`, e.message);
@@ -77,15 +242,61 @@ class MultiFileAuthState extends EventEmitter {
77
242
  }
78
243
  }
79
244
 
245
+ // restore the latest backup for a key (returns path of restored file or null)
246
+ async restoreLatestBackup(key) {
247
+ try {
248
+ const base = FILE_NAMES[key];
249
+ const bfolder = this._getBackupFolder();
250
+ if (!fs.existsSync(bfolder)) return null;
251
+ const files = await fs.promises.readdir(bfolder);
252
+ const matches = files.filter(f => f.startsWith(base + '.'));
253
+ if (!matches.length) return null;
254
+ // sort descending by timestamp embedded in filename
255
+ matches.sort().reverse();
256
+ const latest = matches[0];
257
+ const src = path.join(bfolder, latest);
258
+ const dest = this._getFilePath(key);
259
+ await fs.promises.copyFile(src, dest);
260
+ this.emit('backup-restored', { key, file: src });
261
+ // reload into memory
262
+ this.data[key] = await this._readFile(key);
263
+ return src;
264
+ } catch (e) {
265
+ console.warn('[MultiFileAuthState] restoreLatestBackup error:', e.message);
266
+ return null;
267
+ }
268
+ }
269
+
80
270
  async loadAll() {
81
271
  this._ensureFolder();
82
-
272
+ this._ensureBackupFolder();
273
+
83
274
  const loadPromises = Object.keys(FILE_NAMES).map(async (key) => {
84
275
  this.data[key] = await this._readFile(key);
85
276
  });
86
-
277
+
87
278
  await Promise.all(loadPromises);
88
-
279
+
280
+ // ensure usageStats and loginHistory objects exist
281
+ if (!this.data.loginHistory) this.data.loginHistory = [];
282
+ if (!this.data.usageStats) this.data.usageStats = {
283
+ apiRequests: 0,
284
+ mqttMessages: 0,
285
+ errors: 0,
286
+ reconnects: 0,
287
+ lastReset: new Date().toISOString()
288
+ };
289
+ if (!this.data.accountsIndex) this.data.accountsIndex = {};
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
+
89
300
  return {
90
301
  creds: this.data.creds,
91
302
  device: this.data.device,
@@ -94,6 +305,21 @@ class MultiFileAuthState extends EventEmitter {
94
305
  subscriptions: this.data.subscriptions,
95
306
  seqIds: this.data.seqIds,
96
307
  appState: this.data.appState,
308
+ irisState: this.data.irisState,
309
+ mqttTopics: this.data.mqttTopics,
310
+ mqttCapabilities: this.data.mqttCapabilities,
311
+ mqttAuth: this.data.mqttAuth,
312
+ lastPublishIds: this.data.lastPublishIds,
313
+ loginHistory: this.data.loginHistory,
314
+ usageStats: this.data.usageStats,
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,
97
323
  hasSession: !!(this.data.creds && this.data.cookies)
98
324
  };
99
325
  }
@@ -104,6 +330,25 @@ class MultiFileAuthState extends EventEmitter {
104
330
  await this._writeFile(key, this.data[key]);
105
331
  }
106
332
  });
333
+ // also save usageStats and loginHistory explicitly
334
+ if (this.data.loginHistory) {
335
+ try { await this._writeFile('loginHistory', this.data.loginHistory); } catch (e) {}
336
+ }
337
+ if (this.data.usageStats) {
338
+ try { await this._writeFile('usageStats', this.data.usageStats); } catch (e) {}
339
+ }
340
+ if (this.data.accountsIndex) {
341
+ try { await this._writeFile('accountsIndex', this.data.accountsIndex); } catch (e) {}
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
+
107
352
  await Promise.all(savePromises);
108
353
  }
109
354
 
@@ -112,15 +357,36 @@ class MultiFileAuthState extends EventEmitter {
112
357
  if (this.data.creds) promises.push(this._writeFile('creds', this.data.creds));
113
358
  if (this.data.device) promises.push(this._writeFile('device', this.data.device));
114
359
  if (this.data.cookies) promises.push(this._writeFile('cookies', this.data.cookies));
360
+ // also persist loginHistory and usageStats after creds save
361
+ if (this.data.loginHistory) promises.push(this._writeFile('loginHistory', this.data.loginHistory));
362
+ if (this.data.usageStats) promises.push(this._writeFile('usageStats', this.data.usageStats));
115
363
  await Promise.all(promises);
116
364
  this.emit('creds-saved');
117
365
  }
118
366
 
119
367
  async saveMqttState() {
368
+ // Save base mqtt data
120
369
  const promises = [];
121
370
  if (this.data.mqttSession) promises.push(this._writeFile('mqttSession', this.data.mqttSession));
122
371
  if (this.data.subscriptions) promises.push(this._writeFile('subscriptions', this.data.subscriptions));
123
372
  if (this.data.seqIds) promises.push(this._writeFile('seqIds', this.data.seqIds));
373
+
374
+ // Save extended mqtt/realtime data (new files)
375
+ if (this.data.irisState) promises.push(this._writeFile('irisState', this.data.irisState));
376
+ if (this.data.mqttTopics) promises.push(this._writeFile('mqttTopics', this.data.mqttTopics));
377
+ if (this.data.mqttCapabilities) promises.push(this._writeFile('mqttCapabilities', this.data.mqttCapabilities));
378
+ if (this.data.mqttAuth) promises.push(this._writeFile('mqttAuth', this.data.mqttAuth));
379
+ if (this.data.lastPublishIds) promises.push(this._writeFile('lastPublishIds', this.data.lastPublishIds));
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
+
124
390
  await Promise.all(promises);
125
391
  this.emit('mqtt-state-saved');
126
392
  }
@@ -133,23 +399,35 @@ class MultiFileAuthState extends EventEmitter {
133
399
 
134
400
  _debouncedSave(keys) {
135
401
  keys.forEach(k => this._dirty.add(k));
136
-
402
+
137
403
  if (this._saveDebounceTimer) {
138
404
  clearTimeout(this._saveDebounceTimer);
139
405
  }
140
-
406
+
141
407
  this._saveDebounceTimer = setTimeout(async () => {
142
408
  const toSave = Array.from(this._dirty);
143
409
  this._dirty.clear();
144
-
410
+
145
411
  for (const key of toSave) {
146
412
  if (this.data[key] !== null && this.data[key] !== undefined) {
147
- await this._writeFile(key, this.data[key]);
413
+ try {
414
+ // ensure backup for critical files
415
+ if (['creds', 'cookies', 'device'].includes(key)) {
416
+ await this._createBackup(key);
417
+ }
418
+ await this._writeFile(key, this.data[key]);
419
+ } catch (e) {
420
+ console.error(`[MultiFileAuthState] Debounced write error for ${key}:`, e.message);
421
+ // increment usageStats.errors
422
+ try { this.incrementStat('errors'); } catch (e2) {}
423
+ }
148
424
  }
149
425
  }
426
+ this.emit('debounced-save-complete', toSave);
150
427
  }, this._saveDebounceMs);
151
428
  }
152
429
 
430
+ // --- Setters that schedule debounced saves ---
153
431
  setCreds(creds) {
154
432
  this.data.creds = creds;
155
433
  this._debouncedSave(['creds']);
@@ -185,6 +463,498 @@ class MultiFileAuthState extends EventEmitter {
185
463
  this._debouncedSave(['appState']);
186
464
  }
187
465
 
466
+ // --- New setters for additional MQTT files ---
467
+ setIrisState(irisState) {
468
+ this.data.irisState = irisState;
469
+ this._debouncedSave(['irisState']);
470
+ }
471
+
472
+ setMqttTopics(topics) {
473
+ this.data.mqttTopics = topics;
474
+ this._debouncedSave(['mqttTopics']);
475
+ }
476
+
477
+ setMqttCapabilities(capabilities) {
478
+ this.data.mqttCapabilities = capabilities;
479
+ this._debouncedSave(['mqttCapabilities']);
480
+ }
481
+
482
+ setMqttAuth(auth) {
483
+ this.data.mqttAuth = auth;
484
+ this._debouncedSave(['mqttAuth']);
485
+ }
486
+
487
+ setLastPublishIds(lastPublishIds) {
488
+ this.data.lastPublishIds = lastPublishIds;
489
+ this._debouncedSave(['lastPublishIds']);
490
+ }
491
+
492
+ // --- Utility setters ---
493
+ addLoginHistory(entry) {
494
+ if (!this.data.loginHistory) this.data.loginHistory = [];
495
+ this.data.loginHistory.unshift(entry);
496
+ // keep bounded history length
497
+ if (this.data.loginHistory.length > 200) this.data.loginHistory.length = 200;
498
+ this._debouncedSave(['loginHistory']);
499
+ this.emit('login-attempt', entry);
500
+ }
501
+
502
+ incrementStat(statName, by = 1) {
503
+ if (!this.data.usageStats) this.data.usageStats = {
504
+ apiRequests: 0,
505
+ mqttMessages: 0,
506
+ errors: 0,
507
+ reconnects: 0,
508
+ lastReset: new Date().toISOString()
509
+ };
510
+ if (typeof this.data.usageStats[statName] === 'number') {
511
+ this.data.usageStats[statName] += by;
512
+ } else {
513
+ this.data.usageStats[statName] = (this.data.usageStats[statName] || 0) + by;
514
+ }
515
+ this._debouncedSave(['usageStats']);
516
+ }
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
+
957
+ // --- Getters ---
188
958
  getCreds() { return this.data.creds; }
189
959
  getDevice() { return this.data.device; }
190
960
  getCookies() { return this.data.cookies; }
@@ -192,13 +962,31 @@ class MultiFileAuthState extends EventEmitter {
192
962
  getSubscriptions() { return this.data.subscriptions; }
193
963
  getSeqIds() { return this.data.seqIds; }
194
964
  getAppState() { return this.data.appState; }
965
+ getIrisState() { return this.data.irisState; }
966
+ getMqttTopics() { return this.data.mqttTopics; }
967
+ getMqttCapabilities() { return this.data.mqttCapabilities; }
968
+ getMqttAuth() { return this.data.mqttAuth; }
969
+ getLastPublishIds() { return this.data.lastPublishIds; }
970
+ getLoginHistory() { return this.data.loginHistory || []; }
971
+ getUsageStats() { return this.data.usageStats || {}; }
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 || {}; }
195
980
 
196
981
  hasValidSession() {
197
982
  return !!(this.data.creds && this.data.cookies && this.data.creds.authorization);
198
983
  }
199
984
 
200
985
  hasMqttSession() {
201
- return !!(this.data.mqttSession && this.data.mqttSession.sessionId);
986
+ return !!(
987
+ (this.data.mqttSession && this.data.mqttSession.sessionId) ||
988
+ (this.data.mqttAuth && this.data.mqttAuth.jwt)
989
+ );
202
990
  }
203
991
 
204
992
  async clearAll() {
@@ -211,10 +999,215 @@ class MultiFileAuthState extends EventEmitter {
211
999
  } catch (e) {}
212
1000
  this.data[key] = null;
213
1001
  }
1002
+ // clear backups too? leave backups intact but emit event
1003
+ this.emit('cleared-all');
214
1004
  }
1005
+
1006
+ // --- Backup utilities exposed ---
1007
+ async listBackups() {
1008
+ try {
1009
+ const b = this._getBackupFolder();
1010
+ if (!fs.existsSync(b)) return [];
1011
+ const files = await fs.promises.readdir(b);
1012
+ return files.map(f => path.join(b, f));
1013
+ } catch (e) {
1014
+ return [];
1015
+ }
1016
+ }
1017
+
1018
+ // --- Device fingerprint helpers ---
1019
+ computeDeviceFingerprint(deviceObj) {
1020
+ try {
1021
+ const s = JSON.stringify(deviceObj || this.data.device || {});
1022
+ return crypto.createHash('sha256').update(s).digest('hex');
1023
+ } catch (e) {
1024
+ return null;
1025
+ }
1026
+ }
1027
+
1028
+ // lock device fingerprint: saves hash into device.json (device.locked = true)
1029
+ async lockDeviceFingerprint() {
1030
+ try {
1031
+ if (!this.data.device) this.data.device = {};
1032
+ const hash = this.computeDeviceFingerprint(this.data.device);
1033
+ this.data.device.fingerprintHash = hash;
1034
+ this.data.device.locked = true;
1035
+ await this._writeFile('device', this.data.device);
1036
+ this.emit('device-locked', { fingerprintHash: hash });
1037
+ return hash;
1038
+ } catch (e) {
1039
+ console.warn('[MultiFileAuthState] lockDeviceFingerprint failed:', e.message);
1040
+ return null;
1041
+ }
1042
+ }
1043
+
1044
+ verifyDeviceFingerprint(deviceObj) {
1045
+ try {
1046
+ const stored = this.data.device?.fingerprintHash || null;
1047
+ if (!stored) return true; // no lock present
1048
+ const hash = this.computeDeviceFingerprint(deviceObj || this.data.device);
1049
+ return stored === hash;
1050
+ } catch (e) {
1051
+ return false;
1052
+ }
1053
+ }
1054
+
1055
+ // --- Login history helpers ---
1056
+ recordLoginAttempt({ success, ip, userAgent, method = 'password', reason = null }) {
1057
+ const entry = {
1058
+ date: new Date().toISOString(),
1059
+ success: Boolean(success),
1060
+ ip: ip || null,
1061
+ userAgent: userAgent || null,
1062
+ method,
1063
+ reason
1064
+ };
1065
+ this.addLoginHistory(entry);
1066
+ return entry;
1067
+ }
1068
+
1069
+ // mark account restricted / checkpoint
1070
+ markAccountRestricted({ status = 'checkpoint', reason = null }) {
1071
+ if (!this.data.creds) this.data.creds = {};
1072
+ this.data.creds.accountStatus = status;
1073
+ this.data.creds.restrictedAt = new Date().toISOString();
1074
+ this.data.creds.lastError = reason;
1075
+ this._debouncedSave(['creds']);
1076
+ this.emit('account-restricted', { status, reason });
1077
+ }
1078
+
1079
+ // clear restriction
1080
+ clearAccountRestriction() {
1081
+ if (this.data.creds) {
1082
+ delete this.data.creds.accountStatus;
1083
+ delete this.data.creds.restrictedAt;
1084
+ delete this.data.creds.lastError;
1085
+ this._debouncedSave(['creds']);
1086
+ this.emit('account-unrestricted');
1087
+ }
1088
+ }
1089
+
1090
+ // --- Health check ---
1091
+ getHealth() {
1092
+ return {
1093
+ validSession: this.hasValidSession(),
1094
+ hasCookies: !!this.data.cookies,
1095
+ hasMqtt: this.hasMqttSession(),
1096
+ lastLogin: (this.data.loginHistory && this.data.loginHistory[0]?.date) || null,
1097
+ usageStats: this.getUsageStats(),
1098
+ accountStatus: this.data.creds?.accountStatus || 'ok',
1099
+ deviceLocked: !!this.data.device?.locked,
1100
+ mqttHealth: this.getMqttHealth(),
1101
+ mqttRisk: this.getMqttRisk()
1102
+ };
1103
+ }
1104
+ }
1105
+
1106
+ // Helper: try to safely extract cookieJar serialization if exists
1107
+ async function trySerializeCookieJar(igState) {
1108
+ let cookies = null;
1109
+ try {
1110
+ if (igState.cookieJar && typeof igState.serializeCookieJar === 'function') {
1111
+ cookies = await igState.serializeCookieJar();
1112
+ } else if (igState.cookieJar && typeof igState.cookieJar.serialize === 'function') {
1113
+ // fallback: tough-cookie jar serialize
1114
+ const ser = await util.promisify(igState.cookieJar.serialize).bind(igState.cookieJar)();
1115
+ cookies = ser;
1116
+ }
1117
+ } catch (e) {
1118
+ console.warn('[MultiFileAuthState] Could not serialize cookies:', e.message);
1119
+ }
1120
+ return cookies;
1121
+ }
1122
+
1123
+ // Helper: try to read a cookie value from a tough-cookie Jar or equivalent
1124
+ async function getCookieValueFromJar(cookieJar, name) {
1125
+ if (!cookieJar) return null;
1126
+
1127
+ // try getCookieString (tough-cookie)
1128
+ try {
1129
+ if (typeof cookieJar.getCookieString === 'function') {
1130
+ const getCookieString = util.promisify(cookieJar.getCookieString).bind(cookieJar);
1131
+ const urls = ['https://www.instagram.com', 'https://instagram.com', 'https://i.instagram.com'];
1132
+ for (const url of urls) {
1133
+ try {
1134
+ const cookieString = await getCookieString(url);
1135
+ if (cookieString && typeof cookieString === 'string') {
1136
+ const pairs = cookieString.split(';').map(s => s.trim());
1137
+ for (const p of pairs) {
1138
+ const [k, ...rest] = p.split('=');
1139
+ if (k === name) return rest.join('=');
1140
+ }
1141
+ }
1142
+ } catch (e) {
1143
+ // ignore and try next url
1144
+ }
1145
+ }
1146
+ }
1147
+ } catch (e) {
1148
+ // ignore
1149
+ }
1150
+
1151
+ // try getCookies which returns Cookie objects
1152
+ try {
1153
+ if (typeof cookieJar.getCookies === 'function') {
1154
+ const getCookies = util.promisify(cookieJar.getCookies).bind(cookieJar);
1155
+ const urls = ['https://www.instagram.com', 'https://instagram.com', 'https://i.instagram.com'];
1156
+ for (const url of urls) {
1157
+ try {
1158
+ const cookies = await getCookies(url);
1159
+ if (Array.isArray(cookies)) {
1160
+ for (const c of cookies) {
1161
+ // cookie object shapes vary: check common keys
1162
+ const key = c.key ?? c.name ?? c.name;
1163
+ const val = c.value ?? c.value ?? c.value;
1164
+ if (key === name) return val;
1165
+ }
1166
+ }
1167
+ } catch (e) {
1168
+ // ignore and try next url
1169
+ }
1170
+ }
1171
+ }
1172
+ } catch (e) {
1173
+ // ignore
1174
+ }
1175
+
1176
+ // try serialized cookie jar structure if present
1177
+ try {
1178
+ if (cookieJar && typeof cookieJar === 'object' && cookieJar.cookies && Array.isArray(cookieJar.cookies)) {
1179
+ const found = cookieJar.cookies.find(c => (c.key === name || c.name === name || c.name === name));
1180
+ if (found) return found.value ?? found.value;
1181
+ }
1182
+ } catch (e) {
1183
+ // ignore
1184
+ }
1185
+
1186
+ return null;
1187
+ }
1188
+
1189
+ // Extract cookie-derived fields (sessionid, csrftoken, ds_user_id, mid)
1190
+ async function extractCookieFields(igState) {
1191
+ const out = {
1192
+ sessionIdCookie: null,
1193
+ csrfTokenCookie: null,
1194
+ dsUserIdCookie: null,
1195
+ midCookie: null
1196
+ };
1197
+ try {
1198
+ const cookieJar = igState.cookieJar;
1199
+ out.sessionIdCookie = await getCookieValueFromJar(cookieJar, 'sessionid');
1200
+ out.csrfTokenCookie = await getCookieValueFromJar(cookieJar, 'csrftoken');
1201
+ out.dsUserIdCookie = await getCookieValueFromJar(cookieJar, 'ds_user_id') || await getCookieValueFromJar(cookieJar, 'ds_user');
1202
+ out.midCookie = await getCookieValueFromJar(cookieJar, 'mid');
1203
+ } catch (e) {
1204
+ // ignore
1205
+ }
1206
+ return out;
215
1207
  }
216
1208
 
217
1209
  async function extractStateData(igState) {
1210
+ // Basic creds (existing)
218
1211
  const creds = {
219
1212
  authorization: igState.authorization || null,
220
1213
  igWWWClaim: igState.igWWWClaim || null,
@@ -222,6 +1215,7 @@ async function extractStateData(igState) {
222
1215
  passwordEncryptionPubKey: igState.passwordEncryptionPubKey || null
223
1216
  };
224
1217
 
1218
+ // Device remains same
225
1219
  const device = {
226
1220
  deviceString: igState.deviceString || null,
227
1221
  deviceId: igState.deviceId || null,
@@ -231,15 +1225,10 @@ async function extractStateData(igState) {
231
1225
  build: igState.build || null
232
1226
  };
233
1227
 
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
- }
1228
+ // Try to serialize cookies (kept separate)
1229
+ const cookies = await trySerializeCookieJar(igState);
242
1230
 
1231
+ // App state remains same
243
1232
  const appState = {
244
1233
  language: igState.language || 'en_US',
245
1234
  timezoneOffset: igState.timezoneOffset || null,
@@ -249,6 +1238,62 @@ async function extractStateData(igState) {
249
1238
  challenge: igState.challenge || null
250
1239
  };
251
1240
 
1241
+ // --- NEW: richer creds fields derived from igState + cookies ---
1242
+ // try to obtain cookie fields
1243
+ const cookieFields = await extractCookieFields(igState);
1244
+
1245
+ // Primary identifiers
1246
+ const userIdFromState = igState.cookieUserId || igState.userId || igState.user_id || null;
1247
+ const dsUserIdFromCookies = cookieFields.dsUserIdCookie || igState.dsUserId || igState.ds_user_id || null;
1248
+ const sessionIdFromCookies = cookieFields.sessionIdCookie || null;
1249
+ const csrfFromCookies = cookieFields.csrfTokenCookie || null;
1250
+ const midFromCookies = cookieFields.midCookie || igState.mid || null;
1251
+
1252
+ // username if exposed on state
1253
+ const usernameFromState = igState.username || igState.userName || igState.user?.username || null;
1254
+
1255
+ // rankToken: if userId + uuid available, form `${userId}_${uuid}`
1256
+ let rankToken = null;
1257
+ try {
1258
+ if (userIdFromState && (igState.uuid || device.uuid)) {
1259
+ rankToken = `${userIdFromState}_${igState.uuid || device.uuid}`;
1260
+ } else if (igState.rankToken) {
1261
+ rankToken = igState.rankToken;
1262
+ }
1263
+ } catch (e) {
1264
+ rankToken = igState.rankToken || null;
1265
+ }
1266
+
1267
+ // sessionId and csrfToken - prefer cookies, fallback to state fields if present
1268
+ const sessionId = sessionIdFromCookies || igState.sessionId || igState.sessionid || null;
1269
+ const csrfToken = csrfFromCookies || igState.csrfToken || igState.csrftoken || null;
1270
+
1271
+ // mid fallback
1272
+ const mid = midFromCookies || igState.mid || null;
1273
+
1274
+ // isLoggedIn heuristic: presence of sessionid cookie or an 'isLoggedIn' flag or authorization header
1275
+ const isLoggedIn = Boolean(
1276
+ sessionId ||
1277
+ igState.isLoggedIn ||
1278
+ (creds.authorization && creds.authorization.length)
1279
+ );
1280
+
1281
+ // loginAt / lastValidatedAt: try to take from igState if present, otherwise null
1282
+ const loginAt = igState.loginAt || igState.loggedInAt || null;
1283
+ const lastValidatedAt = igState.lastValidatedAt || igState.lastValidated || null;
1284
+
1285
+ // Attach all new fields into creds object (non-destructive)
1286
+ creds.userId = userIdFromState || dsUserIdFromCookies || creds.userId || null;
1287
+ creds.dsUserId = dsUserIdFromCookies || null;
1288
+ creds.username = usernameFromState || null;
1289
+ creds.rankToken = rankToken;
1290
+ creds.sessionId = sessionId;
1291
+ creds.csrfToken = csrfToken;
1292
+ creds.mid = mid;
1293
+ creds.isLoggedIn = isLoggedIn;
1294
+ creds.loginAt = loginAt;
1295
+ creds.lastValidatedAt = lastValidatedAt;
1296
+
252
1297
  return { creds, device, cookies, appState };
253
1298
  }
254
1299
 
@@ -260,7 +1305,25 @@ async function applyStateData(igState, authState) {
260
1305
  if (creds.igWWWClaim) igState.igWWWClaim = creds.igWWWClaim;
261
1306
  if (creds.passwordEncryptionKeyId) igState.passwordEncryptionKeyId = creds.passwordEncryptionKeyId;
262
1307
  if (creds.passwordEncryptionPubKey) igState.passwordEncryptionPubKey = creds.passwordEncryptionPubKey;
263
- igState.updateAuthorization();
1308
+ // if igState provides a helper to update authorization, call it
1309
+ if (typeof igState.updateAuthorization === 'function') {
1310
+ try { igState.updateAuthorization(); } catch (e) {}
1311
+ }
1312
+
1313
+ // apply new creds-derived fields if state supports them (non-intrusive)
1314
+ try {
1315
+ if (creds.userId && !igState.cookieUserId) igState.cookieUserId = creds.userId;
1316
+ if (creds.username && !igState.username) igState.username = creds.username;
1317
+ if (creds.rankToken && !igState.rankToken) igState.rankToken = creds.rankToken;
1318
+ if (creds.sessionId && !igState.sessionId) igState.sessionId = creds.sessionId;
1319
+ if (creds.csrfToken && !igState.csrfToken) igState.csrfToken = creds.csrfToken;
1320
+ if (creds.mid && !igState.mid) igState.mid = creds.mid;
1321
+ if (typeof creds.isLoggedIn !== 'undefined' && !igState.isLoggedIn) igState.isLoggedIn = creds.isLoggedIn;
1322
+ if (creds.loginAt && !igState.loginAt) igState.loginAt = creds.loginAt;
1323
+ if (creds.lastValidatedAt && !igState.lastValidatedAt) igState.lastValidatedAt = creds.lastValidatedAt;
1324
+ } catch (e) {
1325
+ // ignore if igState shape different
1326
+ }
264
1327
  }
265
1328
 
266
1329
  if (device) {
@@ -274,7 +1337,15 @@ async function applyStateData(igState, authState) {
274
1337
 
275
1338
  if (cookies) {
276
1339
  try {
277
- await igState.deserializeCookieJar(cookies);
1340
+ if (typeof igState.deserializeCookieJar === 'function') {
1341
+ await igState.deserializeCookieJar(cookies);
1342
+ } else if (igState.cookieJar && typeof igState.cookieJar.restore === 'function') {
1343
+ // fallback restore
1344
+ await util.promisify(igState.cookieJar.restore).bind(igState.cookieJar)(cookies);
1345
+ } else if (igState.cookieJar && typeof igState.cookieJar._importCookies === 'function') {
1346
+ // last-resort, not likely
1347
+ try { igState.cookieJar._importCookies(cookies); } catch (e) {}
1348
+ }
278
1349
  } catch (e) {
279
1350
  console.warn('[MultiFileAuthState] Could not deserialize cookies:', e.message);
280
1351
  }
@@ -290,26 +1361,46 @@ async function applyStateData(igState, authState) {
290
1361
  }
291
1362
  }
292
1363
 
1364
+ // Main exported helper that wires MultiFileAuthState to your clients
293
1365
  async function useMultiFileAuthState(folder) {
294
1366
  const authState = new MultiFileAuthState(folder);
295
1367
  await authState.loadAll();
296
1368
 
1369
+ // Helper: persist additional derived creds fields when saving creds
1370
+ const enrichAndSaveCreds = async (igClientState) => {
1371
+ const { creds, device, cookies, appState } = await extractStateData(igClientState);
1372
+ // merge with existing creds non-destructive
1373
+ authState.data.creds = Object.assign({}, authState.data.creds || {}, creds);
1374
+ authState.data.device = Object.assign({}, authState.data.device || {}, device);
1375
+ authState.data.cookies = cookies || authState.data.cookies;
1376
+ authState.data.appState = Object.assign({}, authState.data.appState || {}, appState);
1377
+ await authState.saveCreds();
1378
+ await authState.saveAppState();
1379
+ };
1380
+
297
1381
  const saveCreds = async (igClient) => {
298
1382
  if (!igClient || !igClient.state) {
299
1383
  console.warn('[useMultiFileAuthState] No igClient provided to saveCreds');
300
1384
  return;
301
1385
  }
302
1386
 
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
-
1387
+ // Use the enriched saver
1388
+ await enrichAndSaveCreds(igClient.state);
1389
+
1390
+ // Also attempt to extract some cookie-derived fields for convenience
1391
+ try {
1392
+ const cookieFields = await extractCookieFields(igClient.state);
1393
+ if (cookieFields.sessionIdCookie) {
1394
+ if (!authState.data.creds) authState.data.creds = {};
1395
+ authState.data.creds.sessionId = cookieFields.sessionIdCookie;
1396
+ }
1397
+ if (cookieFields.dsUserIdCookie) {
1398
+ if (!authState.data.creds) authState.data.creds = {};
1399
+ authState.data.creds.dsUserId = cookieFields.dsUserIdCookie;
1400
+ }
1401
+ authState._debouncedSave(['creds']);
1402
+ } catch (e) {}
1403
+
313
1404
  console.log('[useMultiFileAuthState] Credentials saved to', folder);
314
1405
  };
315
1406
 
@@ -319,6 +1410,7 @@ async function useMultiFileAuthState(folder) {
319
1410
  return;
320
1411
  }
321
1412
 
1413
+ // base mqtt session
322
1414
  const mqttSession = {
323
1415
  sessionId: null,
324
1416
  mqttSessionId: null,
@@ -328,30 +1420,92 @@ async function useMultiFileAuthState(folder) {
328
1420
 
329
1421
  try {
330
1422
  if (realtimeClient.ig && realtimeClient.ig.state) {
331
- mqttSession.userId = realtimeClient.ig.state.cookieUserId;
332
- mqttSession.sessionId = realtimeClient.extractSessionIdFromJWT?.() || null;
1423
+ mqttSession.userId = realtimeClient.ig.state.cookieUserId || realtimeClient.ig.state.userId || null;
1424
+ // attempt to extract sessionId from JWT helper if available
1425
+ mqttSession.sessionId = (typeof realtimeClient.extractSessionIdFromJWT === 'function') ? realtimeClient.extractSessionIdFromJWT() : null;
333
1426
  }
334
1427
  if (realtimeClient.connection) {
335
1428
  mqttSession.mqttSessionId = realtimeClient.connection?.clientInfo?.clientMqttSessionId?.toString() || null;
336
1429
  }
337
- } catch (e) {}
1430
+ } catch (e) {
1431
+ // ignore
1432
+ }
338
1433
 
1434
+ // subscriptions
339
1435
  const subscriptions = {
340
1436
  graphQlSubs: realtimeClient.initOptions?.graphQlSubs || [],
341
1437
  skywalkerSubs: realtimeClient.initOptions?.skywalkerSubs || [],
342
1438
  subscribedAt: new Date().toISOString()
343
1439
  };
344
1440
 
1441
+ // seq ids (iris)
345
1442
  const seqIds = {};
346
1443
  if (realtimeClient.initOptions?.irisData) {
347
1444
  seqIds.seq_id = realtimeClient.initOptions.irisData.seq_id || null;
348
1445
  seqIds.snapshot_at_ms = realtimeClient.initOptions.irisData.snapshot_at_ms || null;
349
1446
  }
350
1447
 
1448
+ // --- Extended MQTT / realtime info (new) ---
1449
+ // irisState: things like subscription_id / device id / other iris metadata
1450
+ const irisState = {};
1451
+ try {
1452
+ // try to glean from various possible places on realtimeClient
1453
+ irisState.subscription_id = realtimeClient.initOptions?.irisData?.subscription_id || realtimeClient.iris?.subscriptionId || realtimeClient.irisState?.subscription_id || null;
1454
+ irisState.user_id = realtimeClient.ig?.state?.cookieUserId || realtimeClient.ig?.state?.userId || irisState.user_id || null;
1455
+ irisState.device_id = realtimeClient.connection?.clientInfo?.deviceId || realtimeClient.initOptions?.deviceId || null;
1456
+ irisState.created_at = new Date().toISOString();
1457
+ } catch (e) {}
1458
+
1459
+ // mqttTopics: topics observed/subscribed
1460
+ const mqttTopics = {};
1461
+ try {
1462
+ mqttTopics.topics = realtimeClient.connection?.subscribedTopics || realtimeClient._subscribedTopics || realtimeClient.subscribedTopics || [];
1463
+ mqttTopics.updatedAt = new Date().toISOString();
1464
+ } catch (e) {
1465
+ mqttTopics.topics = [];
1466
+ mqttTopics.updatedAt = new Date().toISOString();
1467
+ }
1468
+
1469
+ // mqttCapabilities: features supported / protocol version
1470
+ const mqttCapabilities = {};
1471
+ try {
1472
+ mqttCapabilities.supportsTyping = Boolean(realtimeClient.initOptions?.supportsTyping ?? realtimeClient.capabilities?.supportsTyping);
1473
+ mqttCapabilities.supportsReactions = Boolean(realtimeClient.initOptions?.supportsReactions ?? realtimeClient.capabilities?.supportsReactions);
1474
+ mqttCapabilities.supportsVoice = Boolean(realtimeClient.initOptions?.supportsVoice ?? realtimeClient.capabilities?.supportsVoice);
1475
+ mqttCapabilities.protocolVersion = realtimeClient.connection?.protocolVersion || realtimeClient.initOptions?.protocolVersion || null;
1476
+ mqttCapabilities.collectedAt = new Date().toISOString();
1477
+ } catch (e) {}
1478
+
1479
+ // mqttAuth: if realtimeClient keeps an auth token / jwt for mqtt, store (but be careful: short lived)
1480
+ const mqttAuth = {};
1481
+ try {
1482
+ mqttAuth.jwt = realtimeClient.connection?.authToken || realtimeClient.mqttAuth?.jwt || realtimeClient.initOptions?.mqttJwt || null;
1483
+ mqttAuth.expiresAt = realtimeClient.connection?.authExpiresAt || realtimeClient.mqttAuth?.expiresAt || null;
1484
+ mqttAuth.collectedAt = new Date().toISOString();
1485
+ } catch (e) {}
1486
+
1487
+ // lastPublishIds: tracking last message ids for reliable publish/acks
1488
+ const lastPublishIds = {};
1489
+ try {
1490
+ lastPublishIds.lastMessageId = realtimeClient._lastMessageId || realtimeClient.lastMessageId || null;
1491
+ lastPublishIds.lastAckedAt = realtimeClient._lastAckedAt || null;
1492
+ lastPublishIds.collectedAt = new Date().toISOString();
1493
+ } catch (e) {}
1494
+
1495
+ // Assign all into authState and persist
351
1496
  authState.data.mqttSession = mqttSession;
352
1497
  authState.data.subscriptions = subscriptions;
353
1498
  authState.data.seqIds = seqIds;
354
1499
 
1500
+ authState.data.irisState = irisState;
1501
+ authState.data.mqttTopics = mqttTopics;
1502
+ authState.data.mqttCapabilities = mqttCapabilities;
1503
+ authState.data.mqttAuth = mqttAuth;
1504
+ authState.data.lastPublishIds = lastPublishIds;
1505
+
1506
+ // Increment stats
1507
+ authState.incrementStat('mqttMessages');
1508
+
355
1509
  await authState.saveMqttState();
356
1510
  console.log('[useMultiFileAuthState] MQTT session saved to', folder);
357
1511
  };
@@ -374,11 +1528,20 @@ async function useMultiFileAuthState(folder) {
374
1528
 
375
1529
  const getMqttConnectOptions = () => {
376
1530
  if (!authState.hasMqttSession()) {
377
- 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
+ };
378
1540
  }
379
1541
 
380
1542
  const subs = authState.getSubscriptions() || {};
381
1543
  const seqIds = authState.getSeqIds() || {};
1544
+ const mqttAuth = authState.getMqttAuth() || null;
382
1545
 
383
1546
  return {
384
1547
  graphQlSubs: subs.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
@@ -386,7 +1549,9 @@ async function useMultiFileAuthState(folder) {
386
1549
  irisData: seqIds.seq_id ? {
387
1550
  seq_id: seqIds.seq_id,
388
1551
  snapshot_at_ms: seqIds.snapshot_at_ms
389
- } : null
1552
+ } : { seq_id: 0, snapshot_at_ms: Date.now() },
1553
+ mqttAuthToken: mqttAuth?.jwt || null,
1554
+ mqttAuthExpiresAt: mqttAuth?.expiresAt || null
390
1555
  };
391
1556
  };
392
1557
 
@@ -400,32 +1565,213 @@ async function useMultiFileAuthState(folder) {
400
1565
  if (!igClient) return true;
401
1566
 
402
1567
  try {
403
- await igClient.account.currentUser();
1568
+ // quick check by calling account/currentUser or equivalent
1569
+ if (igClient.account && typeof igClient.account.currentUser === 'function') {
1570
+ await igClient.account.currentUser();
1571
+ return true;
1572
+ }
1573
+ // fallback assume valid if we have creds - up to caller to verify
404
1574
  return true;
405
1575
  } catch (e) {
406
1576
  console.warn('[useMultiFileAuthState] Session validation failed:', e.message);
1577
+ // emit session-expired if credentials appear invalid
1578
+ authState.emit('session-expired', { reason: e.message });
407
1579
  return false;
408
1580
  }
409
1581
  };
410
1582
 
1583
+ // --- Auto-refresh / revalidation helpers ---
1584
+ const _autoRefreshCheck = async () => {
1585
+ try {
1586
+ if (!authState.data.creds) return;
1587
+ // If creds indicate expiry timestamp, check it
1588
+ const expiresAt = authState.data.creds.sessionExpiresAt ? new Date(authState.data.creds.sessionExpiresAt).getTime() : null;
1589
+ if (expiresAt && Date.now() > expiresAt) {
1590
+ authState.data.creds.isExpired = true;
1591
+ authState._debouncedSave(['creds']);
1592
+ authState.emit('session-expired', { reason: 'sessionExpiresAt' });
1593
+ } else {
1594
+ // if igClient provided, optionally validate remote
1595
+ if (authState._autoRefreshClient) {
1596
+ try {
1597
+ const valid = await isSessionValid(authState._autoRefreshClient);
1598
+ if (!valid) {
1599
+ authState.emit('session-expired', { reason: 'remote-check' });
1600
+ } else {
1601
+ // update lastValidatedAt
1602
+ if (!authState.data.creds) authState.data.creds = {};
1603
+ authState.data.creds.lastValidatedAt = new Date().toISOString();
1604
+ authState._debouncedSave(['creds']);
1605
+ }
1606
+ } catch (e) {}
1607
+ }
1608
+ }
1609
+ } catch (e) {
1610
+ // ignore
1611
+ }
1612
+ };
1613
+
1614
+ const enableAutoRefresh = (igClient, intervalMs = 5 * 60 * 1000) => {
1615
+ // igClient optional: if provided, will be used to do remote validation
1616
+ try {
1617
+ disableAutoRefresh();
1618
+ authState._autoRefreshClient = igClient || null;
1619
+ authState._autoRefreshIntervalMs = intervalMs;
1620
+ authState._autoRefreshTimer = setInterval(_autoRefreshCheck, intervalMs);
1621
+ // run immediately once
1622
+ _autoRefreshCheck();
1623
+ authState.emit('auto-refresh-enabled', { intervalMs });
1624
+ } catch (e) {
1625
+ console.warn('[useMultiFileAuthState] enableAutoRefresh error:', e.message);
1626
+ }
1627
+ };
1628
+
1629
+ const disableAutoRefresh = () => {
1630
+ try {
1631
+ if (authState._autoRefreshTimer) {
1632
+ clearInterval(authState._autoRefreshTimer);
1633
+ authState._autoRefreshTimer = null;
1634
+ authState._autoRefreshClient = null;
1635
+ authState.emit('auto-refresh-disabled');
1636
+ }
1637
+ } catch (e) {}
1638
+ };
1639
+
1640
+ // --- Login history / audit helpers ---
1641
+ const recordLoginAttempt = async ({ success, ip = null, userAgent = null, method = 'password', reason = null }) => {
1642
+ const entry = authState.recordLoginAttempt({ success, ip, userAgent, method, reason });
1643
+ // return entry for caller
1644
+ return entry;
1645
+ };
1646
+
1647
+ // --- Account restriction helpers ---
1648
+ const markAccountRestricted = ({ status = 'checkpoint', reason = null }) => {
1649
+ authState.markAccountRestricted({ status, reason });
1650
+ };
1651
+
1652
+ const clearAccountRestriction = () => {
1653
+ authState.clearAccountRestriction();
1654
+ };
1655
+
1656
+ // --- Usage stats helpers ---
1657
+ const incrementStat = (statName, by = 1) => {
1658
+ authState.incrementStat(statName, by);
1659
+ };
1660
+
1661
+ // --- Backup / restore helpers exposed ---
1662
+ const restoreLatestBackup = async (key) => {
1663
+ return await authState.restoreLatestBackup(key);
1664
+ };
1665
+
1666
+ // --- Device fingerprinting ---
1667
+ const lockDeviceFingerprint = async () => {
1668
+ return await authState.lockDeviceFingerprint();
1669
+ };
1670
+
1671
+ const verifyDeviceFingerprint = (deviceObj) => {
1672
+ return authState.verifyDeviceFingerprint(deviceObj);
1673
+ };
1674
+
1675
+ // --- Accounts manager ---
1676
+ const registerAccount = async (name, folderPath) => {
1677
+ if (!authState.data.accountsIndex) authState.data.accountsIndex = {};
1678
+ authState.data.accountsIndex[name] = folderPath;
1679
+ await authState._writeFile('accountsIndex', authState.data.accountsIndex);
1680
+ authState.emit('account-registered', { name, folderPath });
1681
+ return authState.data.accountsIndex;
1682
+ };
1683
+
1684
+ const listAccounts = () => {
1685
+ return authState.getAccountsIndex();
1686
+ };
1687
+
1688
+ // --- Health check ---
1689
+ const getHealth = () => {
1690
+ return authState.getHealth();
1691
+ };
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
+
1710
+ // --- Misc helpers: loginHistory & usageStats getters ---
1711
+ const getLoginHistory = () => authState.getLoginHistory();
1712
+ const getUsageStats = () => authState.getUsageStats();
1713
+
411
1714
  return {
412
1715
  state: authState,
413
-
1716
+
414
1717
  saveCreds,
415
1718
  loadCreds,
416
-
1719
+
417
1720
  saveMqttSession,
418
1721
  getMqttConnectOptions,
419
-
1722
+
420
1723
  clearSession,
421
1724
  isSessionValid,
422
1725
 
423
1726
  hasSession: () => authState.hasValidSession(),
424
1727
  hasMqttSession: () => authState.hasMqttSession(),
425
-
1728
+
426
1729
  folder,
427
-
428
- getData: () => authState.data
1730
+
1731
+ // pass-through for convenience
1732
+ getData: () => authState.data,
1733
+
1734
+ // convenience setters/getters for the new data
1735
+ setIrisState: (s) => authState.setIrisState(s),
1736
+ setMqttTopics: (t) => authState.setMqttTopics(t),
1737
+ setMqttCapabilities: (c) => authState.setMqttCapabilities(c),
1738
+ setMqttAuth: (a) => authState.setMqttAuth(a),
1739
+ setLastPublishIds: (p) => authState.setLastPublishIds(p),
1740
+ getIrisState: () => authState.getIrisState(),
1741
+ getMqttTopics: () => authState.getMqttTopics(),
1742
+ getMqttCapabilities: () => authState.getMqttCapabilities(),
1743
+ getMqttAuth: () => authState.getMqttAuth(),
1744
+ getLastPublishIds: () => authState.getLastPublishIds(),
1745
+
1746
+ // new utilities
1747
+ enableAutoRefresh,
1748
+ disableAutoRefresh,
1749
+ recordLoginAttempt,
1750
+ getLoginHistory,
1751
+ markAccountRestricted,
1752
+ clearAccountRestriction,
1753
+ incrementStat,
1754
+ getUsageStats,
1755
+ restoreLatestBackup,
1756
+ lockDeviceFingerprint,
1757
+ verifyDeviceFingerprint,
1758
+ registerAccount,
1759
+ listAccounts,
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
429
1775
  };
430
1776
  }
431
1777