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.
- package/dist/useMultiFileAuthState.js +1391 -45
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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 !!(
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
} :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|