nodejs-insta-private-api-mqt 1.3.70

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.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3677 -0
  3. package/dist/constants/constants.js +342 -0
  4. package/dist/constants/index.js +58 -0
  5. package/dist/core/client.js +419 -0
  6. package/dist/core/nav-chain.js +282 -0
  7. package/dist/core/repository.js +7 -0
  8. package/dist/core/request.js +390 -0
  9. package/dist/core/state.js +1473 -0
  10. package/dist/core/utils.js +786 -0
  11. package/dist/downloadMedia.js +381 -0
  12. package/dist/errors/index.d.ts +16 -0
  13. package/dist/errors/index.js +38 -0
  14. package/dist/errors/index.js.map +1 -0
  15. package/dist/extend.js +167 -0
  16. package/dist/fbns/fbns.client.d.ts +32 -0
  17. package/dist/fbns/fbns.client.events.d.ts +41 -0
  18. package/dist/fbns/fbns.client.events.js +3 -0
  19. package/dist/fbns/fbns.client.events.js.map +1 -0
  20. package/dist/fbns/fbns.client.js +252 -0
  21. package/dist/fbns/fbns.client.js.map +1 -0
  22. package/dist/fbns/fbns.device-auth.d.ts +17 -0
  23. package/dist/fbns/fbns.device-auth.js +54 -0
  24. package/dist/fbns/fbns.device-auth.js.map +1 -0
  25. package/dist/fbns/fbns.types.d.ts +83 -0
  26. package/dist/fbns/fbns.types.js +3 -0
  27. package/dist/fbns/fbns.types.js.map +1 -0
  28. package/dist/fbns/fbns.utilities.d.ts +2 -0
  29. package/dist/fbns/fbns.utilities.js +79 -0
  30. package/dist/fbns/fbns.utilities.js.map +1 -0
  31. package/dist/fbns/index.d.ts +4 -0
  32. package/dist/fbns/index.js +21 -0
  33. package/dist/fbns/index.js.map +1 -0
  34. package/dist/index.js +139 -0
  35. package/dist/mqtt-shim.d.ts +96 -0
  36. package/dist/mqtt-shim.js +15 -0
  37. package/dist/mqttot/index.d.ts +4 -0
  38. package/dist/mqttot/index.js +21 -0
  39. package/dist/mqttot/index.js.map +1 -0
  40. package/dist/mqttot/mqttot.client.d.ts +39 -0
  41. package/dist/mqttot/mqttot.client.js +318 -0
  42. package/dist/mqttot/mqttot.client.js.map +1 -0
  43. package/dist/mqttot/mqttot.connect.request.packet.d.ts +7 -0
  44. package/dist/mqttot/mqttot.connect.request.packet.js +9 -0
  45. package/dist/mqttot/mqttot.connect.request.packet.js.map +1 -0
  46. package/dist/mqttot/mqttot.connect.response.packet.d.ts +7 -0
  47. package/dist/mqttot/mqttot.connect.response.packet.js +24 -0
  48. package/dist/mqttot/mqttot.connect.response.packet.js.map +1 -0
  49. package/dist/mqttot/mqttot.connection.d.ts +57 -0
  50. package/dist/mqttot/mqttot.connection.js +79 -0
  51. package/dist/mqttot/mqttot.connection.js.map +1 -0
  52. package/dist/package.json +59 -0
  53. package/dist/realtime/commands/commands.d.ts +15 -0
  54. package/dist/realtime/commands/commands.js +71 -0
  55. package/dist/realtime/commands/commands.js.map +1 -0
  56. package/dist/realtime/commands/direct.commands.d.ts +75 -0
  57. package/dist/realtime/commands/direct.commands.js +417 -0
  58. package/dist/realtime/commands/direct.commands.js.map +1 -0
  59. package/dist/realtime/commands/enhanced.direct.commands.js +1731 -0
  60. package/dist/realtime/commands/enhanced.direct.commands.js.bak +967 -0
  61. package/dist/realtime/commands/index.d.ts +2 -0
  62. package/dist/realtime/commands/index.js +20 -0
  63. package/dist/realtime/commands/index.js.map +1 -0
  64. package/dist/realtime/delta-sync.manager.js +293 -0
  65. package/dist/realtime/features/dm-sender.js +88 -0
  66. package/dist/realtime/features/error-handler.js +185 -0
  67. package/dist/realtime/features/gap-handler.js +61 -0
  68. package/dist/realtime/features/persistent-logger.js +186 -0
  69. package/dist/realtime/features/presence.manager.js +66 -0
  70. package/dist/realtime/features/session-health-monitor.js +345 -0
  71. package/dist/realtime/index.js +30 -0
  72. package/dist/realtime/messages/app-presence.event.d.ts +9 -0
  73. package/dist/realtime/messages/app-presence.event.js +3 -0
  74. package/dist/realtime/messages/app-presence.event.js.map +1 -0
  75. package/dist/realtime/messages/index.d.ts +3 -0
  76. package/dist/realtime/messages/index.js +20 -0
  77. package/dist/realtime/messages/index.js.map +1 -0
  78. package/dist/realtime/messages/message-sync.message.d.ts +222 -0
  79. package/dist/realtime/messages/message-sync.message.js +43 -0
  80. package/dist/realtime/messages/message-sync.message.js.map +1 -0
  81. package/dist/realtime/messages/realtime-sub.direct.data.d.ts +11 -0
  82. package/dist/realtime/messages/realtime-sub.direct.data.js +3 -0
  83. package/dist/realtime/messages/realtime-sub.direct.data.js.map +1 -0
  84. package/dist/realtime/messages/thread-update.message.d.ts +68 -0
  85. package/dist/realtime/messages/thread-update.message.js +3 -0
  86. package/dist/realtime/messages/thread-update.message.js.map +1 -0
  87. package/dist/realtime/mixins/index.d.ts +3 -0
  88. package/dist/realtime/mixins/index.js +20 -0
  89. package/dist/realtime/mixins/index.js.map +1 -0
  90. package/dist/realtime/mixins/message-sync.mixin.d.ts +8 -0
  91. package/dist/realtime/mixins/message-sync.mixin.js +596 -0
  92. package/dist/realtime/mixins/message-sync.mixin.js.map +1 -0
  93. package/dist/realtime/mixins/mixin.d.ts +19 -0
  94. package/dist/realtime/mixins/mixin.js +41 -0
  95. package/dist/realtime/mixins/mixin.js.map +1 -0
  96. package/dist/realtime/mixins/presence-typing.mixin.js +33 -0
  97. package/dist/realtime/mixins/realtime-sub.mixin.d.ts +8 -0
  98. package/dist/realtime/mixins/realtime-sub.mixin.js +181 -0
  99. package/dist/realtime/mixins/realtime-sub.mixin.js.map +1 -0
  100. package/dist/realtime/parsers/graphql-parser.js +43 -0
  101. package/dist/realtime/parsers/graphql.parser.d.ts +15 -0
  102. package/dist/realtime/parsers/graphql.parser.js +22 -0
  103. package/dist/realtime/parsers/graphql.parser.js.map +1 -0
  104. package/dist/realtime/parsers/index.d.ts +6 -0
  105. package/dist/realtime/parsers/index.js +23 -0
  106. package/dist/realtime/parsers/index.js.map +1 -0
  107. package/dist/realtime/parsers/iris-parser.js +43 -0
  108. package/dist/realtime/parsers/iris.parser.d.ts +17 -0
  109. package/dist/realtime/parsers/iris.parser.js +10 -0
  110. package/dist/realtime/parsers/iris.parser.js.map +1 -0
  111. package/dist/realtime/parsers/json-parser.js +43 -0
  112. package/dist/realtime/parsers/json.parser.d.ts +6 -0
  113. package/dist/realtime/parsers/json.parser.js +10 -0
  114. package/dist/realtime/parsers/json.parser.js.map +1 -0
  115. package/dist/realtime/parsers/parser.d.ts +9 -0
  116. package/dist/realtime/parsers/parser.js +3 -0
  117. package/dist/realtime/parsers/parser.js.map +1 -0
  118. package/dist/realtime/parsers/region-hint-parser.js +43 -0
  119. package/dist/realtime/parsers/region-hint.parser.d.ts +12 -0
  120. package/dist/realtime/parsers/region-hint.parser.js +15 -0
  121. package/dist/realtime/parsers/region-hint.parser.js.map +1 -0
  122. package/dist/realtime/parsers/skywalker-parser.js +43 -0
  123. package/dist/realtime/parsers/skywalker.parser.d.ts +12 -0
  124. package/dist/realtime/parsers/skywalker.parser.js +15 -0
  125. package/dist/realtime/parsers/skywalker.parser.js.map +1 -0
  126. package/dist/realtime/parsers-advanced.js +158 -0
  127. package/dist/realtime/proto/common.proto +38 -0
  128. package/dist/realtime/proto/direct.proto +65 -0
  129. package/dist/realtime/proto/ig-messages.proto +83 -0
  130. package/dist/realtime/proto/iris.proto +188 -0
  131. package/dist/realtime/proto-parser.js +195 -0
  132. package/dist/realtime/protocols/iris.handshake.js +74 -0
  133. package/dist/realtime/protocols/proto-definitions.js +80 -0
  134. package/dist/realtime/protocols/skywalker.protocol.js +91 -0
  135. package/dist/realtime/realtime.client.events.js +3 -0
  136. package/dist/realtime/realtime.client.js +1915 -0
  137. package/dist/realtime/realtime.service.js +462 -0
  138. package/dist/realtime/reconnect.manager.js +88 -0
  139. package/dist/realtime/session.manager.js +121 -0
  140. package/dist/realtime/subscriptions/graphql.subscription.d.ts +47 -0
  141. package/dist/realtime/subscriptions/graphql.subscription.js +99 -0
  142. package/dist/realtime/subscriptions/graphql.subscription.js.map +1 -0
  143. package/dist/realtime/subscriptions/index.d.ts +2 -0
  144. package/dist/realtime/subscriptions/index.js +19 -0
  145. package/dist/realtime/subscriptions/index.js.map +1 -0
  146. package/dist/realtime/subscriptions/skywalker.subscription.d.ts +4 -0
  147. package/dist/realtime/subscriptions/skywalker.subscription.js +13 -0
  148. package/dist/realtime/subscriptions/skywalker.subscription.js.map +1 -0
  149. package/dist/realtime/topic-map.js +71 -0
  150. package/dist/realtime/topic.js +80 -0
  151. package/dist/repositories/account.repository.js +575 -0
  152. package/dist/repositories/bloks.repository.js +70 -0
  153. package/dist/repositories/captcha.repository.js +44 -0
  154. package/dist/repositories/challenge.repository.js +120 -0
  155. package/dist/repositories/clip.repository.js +165 -0
  156. package/dist/repositories/close-friends.repository.js +46 -0
  157. package/dist/repositories/collection.repository.js +68 -0
  158. package/dist/repositories/direct-thread.repository.js +446 -0
  159. package/dist/repositories/direct.repository.js +232 -0
  160. package/dist/repositories/explore.repository.js +70 -0
  161. package/dist/repositories/fbsearch.repository.js +140 -0
  162. package/dist/repositories/feed.repository.js +245 -0
  163. package/dist/repositories/friendship.repository.js +296 -0
  164. package/dist/repositories/fundraiser.repository.js +49 -0
  165. package/dist/repositories/hashtag.repository.js +99 -0
  166. package/dist/repositories/highlights.repository.js +121 -0
  167. package/dist/repositories/insights.repository.js +82 -0
  168. package/dist/repositories/location.repository.js +84 -0
  169. package/dist/repositories/media.repository.js +395 -0
  170. package/dist/repositories/multiple-accounts.repository.js +41 -0
  171. package/dist/repositories/news.repository.js +35 -0
  172. package/dist/repositories/note.repository.js +57 -0
  173. package/dist/repositories/notification.repository.js +79 -0
  174. package/dist/repositories/share.repository.js +35 -0
  175. package/dist/repositories/signup.repository.js +218 -0
  176. package/dist/repositories/story.repository.js +290 -0
  177. package/dist/repositories/timeline.repository.js +60 -0
  178. package/dist/repositories/totp.repository.js +139 -0
  179. package/dist/repositories/track.repository.js +53 -0
  180. package/dist/repositories/upload.repository.js +204 -0
  181. package/dist/repositories/user.repository.js +360 -0
  182. package/dist/sendmedia/index.js +27 -0
  183. package/dist/sendmedia/sendFile.js +72 -0
  184. package/dist/sendmedia/sendPhoto.js +142 -0
  185. package/dist/sendmedia/sendRavenPhoto.js +153 -0
  186. package/dist/sendmedia/sendRavenVideo.js +158 -0
  187. package/dist/sendmedia/uploadPhoto.js +107 -0
  188. package/dist/sendmedia/uploadfFile.js +130 -0
  189. package/dist/services/live.service.js +139 -0
  190. package/dist/services/search.service.js +115 -0
  191. package/dist/shared/index.js +96 -0
  192. package/dist/shared/shared.js +86 -0
  193. package/dist/thrift/index.d.ts +3 -0
  194. package/dist/thrift/index.js +20 -0
  195. package/dist/thrift/index.js.map +1 -0
  196. package/dist/thrift/thrift.d.ts +59 -0
  197. package/dist/thrift/thrift.js +101 -0
  198. package/dist/thrift/thrift.js.map +1 -0
  199. package/dist/thrift/thrift.reading.d.ts +41 -0
  200. package/dist/thrift/thrift.reading.js +327 -0
  201. package/dist/thrift/thrift.reading.js.map +1 -0
  202. package/dist/thrift/thrift.writing.d.ts +44 -0
  203. package/dist/thrift/thrift.writing.js +342 -0
  204. package/dist/thrift/thrift.writing.js.map +1 -0
  205. package/dist/types/index.js +285 -0
  206. package/dist/useMultiFileAuthState.js +1768 -0
  207. package/dist/utils/helper-1.js +1 -0
  208. package/dist/utils/helper-10.js +1 -0
  209. package/dist/utils/helper-11.js +1 -0
  210. package/dist/utils/helper-12.js +1 -0
  211. package/dist/utils/helper-13.js +1 -0
  212. package/dist/utils/helper-14.js +1 -0
  213. package/dist/utils/helper-15.js +1 -0
  214. package/dist/utils/helper-16.js +1 -0
  215. package/dist/utils/helper-17.js +1 -0
  216. package/dist/utils/helper-18.js +1 -0
  217. package/dist/utils/helper-19.js +1 -0
  218. package/dist/utils/helper-2.js +1 -0
  219. package/dist/utils/helper-20.js +1 -0
  220. package/dist/utils/helper-21.js +1 -0
  221. package/dist/utils/helper-22.js +1 -0
  222. package/dist/utils/helper-23.js +1 -0
  223. package/dist/utils/helper-24.js +1 -0
  224. package/dist/utils/helper-25.js +1 -0
  225. package/dist/utils/helper-26.js +1 -0
  226. package/dist/utils/helper-27.js +1 -0
  227. package/dist/utils/helper-28.js +1 -0
  228. package/dist/utils/helper-29.js +1 -0
  229. package/dist/utils/helper-3.js +1 -0
  230. package/dist/utils/helper-30.js +1 -0
  231. package/dist/utils/helper-4.js +1 -0
  232. package/dist/utils/helper-5.js +1 -0
  233. package/dist/utils/helper-6.js +1 -0
  234. package/dist/utils/helper-7.js +1 -0
  235. package/dist/utils/helper-8.js +1 -0
  236. package/dist/utils/helper-9.js +1 -0
  237. package/dist/utils/index.js +280 -0
  238. package/dist/utils/insta-mqtt-helper.js +128 -0
  239. package/examples/listen-to-messages.js +86 -0
  240. package/package.json +82 -0
@@ -0,0 +1,1768 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { CookieJar } = require('tough-cookie');
6
+ const util = require('util');
7
+ const EventEmitter = require('events');
8
+ const crypto = require('crypto');
9
+
10
+ const FILE_NAMES = {
11
+ creds: 'creds.json',
12
+ device: 'device.json',
13
+ cookies: 'cookies.json',
14
+ mqttSession: 'mqtt-session.json',
15
+ subscriptions: 'subscriptions.json',
16
+ seqIds: 'seq-ids.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'
39
+ };
40
+
41
+ class MultiFileAuthState extends EventEmitter {
42
+ constructor(folder) {
43
+ super();
44
+ this.folder = folder;
45
+ this._saveDebounceTimer = null;
46
+ this._saveDebounceMs = 500;
47
+ this._dirty = new Set();
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
+
59
+ this.data = {
60
+ creds: null,
61
+ device: null,
62
+ cookies: null,
63
+ mqttSession: null,
64
+ subscriptions: null,
65
+ seqIds: 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
88
+ };
89
+
90
+ // ensure folder structure
91
+ this._ensureFolder();
92
+ this._ensureBackupFolder();
93
+ }
94
+
95
+ _getFilePath(key) {
96
+ return path.join(this.folder, FILE_NAMES[key]);
97
+ }
98
+
99
+ _getBackupFolder() {
100
+ return path.join(this.folder, 'backup');
101
+ }
102
+
103
+ _ensureFolder() {
104
+ if (!fs.existsSync(this.folder)) {
105
+ fs.mkdirSync(this.folder, { recursive: true, mode: 0o700 });
106
+ }
107
+ }
108
+
109
+ _ensureBackupFolder() {
110
+ // backups disabled — no-op
111
+ return;
112
+ }
113
+
114
+ // sanitize state to avoid nulls for key fields (useful to ensure instant non-null)
115
+ _sanitizeForSave(key, data) {
116
+ try {
117
+ if (!data) return data;
118
+ if (key === 'creds') {
119
+ if (typeof data.userId === 'undefined' || data.userId === null) data.userId = data.userId || 'pending';
120
+ if (typeof data.username === 'undefined' || data.username === null) data.username = data.username || 'pending';
121
+ if (typeof data.sessionId === 'undefined' || data.sessionId === null) data.sessionId = data.sessionId || `pending-${Date.now()}`;
122
+ if (typeof data.csrfToken === 'undefined' || data.csrfToken === null) data.csrfToken = data.csrfToken || 'pending';
123
+ if (typeof data.isLoggedIn === 'undefined') data.isLoggedIn = !!data.sessionId || !!data.authorization;
124
+ if (!data.loginAt) data.loginAt = data.loginAt || new Date().toISOString();
125
+ }
126
+ if (key === 'mqttSession') {
127
+ if (!data.sessionId) data.sessionId = data.sessionId || `local-mqtt-${Date.now()}`;
128
+ if (!data.mqttSessionId) data.mqttSessionId = data.mqttSessionId || 'boot';
129
+ if (!data.lastConnected) data.lastConnected = new Date().toISOString();
130
+ }
131
+ if (key === 'seqIds') {
132
+ if (typeof data.seq_id === 'undefined' || data.seq_id === null) data.seq_id = data.seq_id || 0;
133
+ if (typeof data.snapshot_at_ms === 'undefined' || data.snapshot_at_ms === null) data.snapshot_at_ms = data.snapshot_at_ms || Date.now();
134
+ }
135
+ if (key === 'mqttTopics') {
136
+ if (!data.topics) data.topics = data.topics || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync', 'presence_subscribe'];
137
+ if (!data.updatedAt) data.updatedAt = new Date().toISOString();
138
+ }
139
+ if (key === 'mqttCapabilities') {
140
+ data.supportsTyping = Boolean(data.supportsTyping ?? true);
141
+ data.supportsReactions = Boolean(data.supportsReactions ?? true);
142
+ data.supportsVoice = Boolean(data.supportsVoice ?? false);
143
+ data.protocolVersion = data.protocolVersion || 3;
144
+ data.collectedAt = data.collectedAt || new Date().toISOString();
145
+ }
146
+ if (key === 'mqttHealth') {
147
+ data.lastPingAt = data.lastPingAt || null;
148
+ data.lastPongAt = data.lastPongAt || null;
149
+ data.latencyMs = data.latencyMs || 0;
150
+ data.missedPings = data.missedPings || 0;
151
+ data.status = data.status || 'unknown';
152
+ }
153
+ if (key === 'mqttMessageCache') {
154
+ if (!Array.isArray(data.lastIds)) data.lastIds = [];
155
+ if (!data.max) data.max = data.max || 500;
156
+ }
157
+ if (key === 'mqttOutbox') {
158
+ if (!Array.isArray(data)) return [];
159
+ }
160
+ if (key === 'mqttSubscriptionHealth') {
161
+ data.expected = data.expected || (this.getMqttTopics()?.topics || ['ig_sub_direct']);
162
+ data.active = data.active || data.expected.slice();
163
+ data.lastCheck = data.lastCheck || new Date().toISOString();
164
+ data.needsResubscribe = data.needsResubscribe || false;
165
+ }
166
+ if (key === 'mqttTraffic') {
167
+ data.messagesInPerMin = data.messagesInPerMin || 0;
168
+ data.messagesOutPerMin = data.messagesOutPerMin || 0;
169
+ data.lastReset = data.lastReset || new Date().toISOString();
170
+ }
171
+ if (key === 'mqttReconnect') {
172
+ data.attempts = data.attempts || 0;
173
+ data.lastReason = data.lastReason || null;
174
+ data.lastAttemptAt = data.lastAttemptAt || null;
175
+ data.nextRetryInMs = data.nextRetryInMs || 0;
176
+ }
177
+ if (key === 'mqttRisk') {
178
+ data.riskScore = typeof data.riskScore === 'number' ? data.riskScore : 0;
179
+ data.level = data.level || 'low';
180
+ data.updatedAt = data.updatedAt || new Date().toISOString();
181
+ }
182
+ } catch (e) {
183
+ // ignore sanitize errors
184
+ }
185
+ return data;
186
+ }
187
+
188
+ // create a timestamped backup of existing file (if present)
189
+ async _createBackup(key) {
190
+ // backups disabled — do nothing
191
+ return null;
192
+ }
193
+
194
+ async _writeFileAtomic(filePath, data) {
195
+ const tempPath = filePath + '.tmp';
196
+ const jsonData = JSON.stringify(data, null, 2);
197
+ await fs.promises.writeFile(tempPath, jsonData, { mode: 0o600 });
198
+ await fs.promises.rename(tempPath, filePath);
199
+ }
200
+
201
+ async _readFile(key) {
202
+ const filePath = this._getFilePath(key);
203
+ try {
204
+ if (fs.existsSync(filePath)) {
205
+ const raw = await fs.promises.readFile(filePath, 'utf8');
206
+ return JSON.parse(raw);
207
+ }
208
+ } catch (e) {
209
+ console.warn(`[MultiFileAuthState] Error reading ${key}:`, e.message);
210
+ }
211
+ return null;
212
+ }
213
+
214
+ async _writeFile(key, data) {
215
+ this._ensureFolder();
216
+ const filePath = this._getFilePath(key);
217
+ try {
218
+ // sanitize to avoid nulls for key fields
219
+ const sanitized = this._sanitizeForSave(key, JSON.parse(JSON.stringify(data)));
220
+ // create backup before overwriting existing file
221
+ await this._createBackup(key);
222
+ await this._writeFileAtomic(filePath, sanitized);
223
+ return true;
224
+ } catch (e) {
225
+ console.error(`[MultiFileAuthState] Error writing ${key}:`, e.message);
226
+ return false;
227
+ }
228
+ }
229
+
230
+ // restore the latest backup for a key (returns path of restored file or null)
231
+ async restoreLatestBackup(key) {
232
+ try {
233
+ const base = FILE_NAMES[key];
234
+ const bfolder = this._getBackupFolder();
235
+ if (!fs.existsSync(bfolder)) return null;
236
+ const files = await fs.promises.readdir(bfolder);
237
+ const matches = files.filter(f => f.startsWith(base + '.'));
238
+ if (!matches.length) return null;
239
+ // sort descending by timestamp embedded in filename
240
+ matches.sort().reverse();
241
+ const latest = matches[0];
242
+ const src = path.join(bfolder, latest);
243
+ const dest = this._getFilePath(key);
244
+ await fs.promises.copyFile(src, dest);
245
+ this.emit('backup-restored', { key, file: src });
246
+ // reload into memory
247
+ this.data[key] = await this._readFile(key);
248
+ return src;
249
+ } catch (e) {
250
+ console.warn('[MultiFileAuthState] restoreLatestBackup error:', e.message);
251
+ return null;
252
+ }
253
+ }
254
+
255
+ async loadAll() {
256
+ this._ensureFolder();
257
+ this._ensureBackupFolder();
258
+
259
+ const loadPromises = Object.keys(FILE_NAMES).map(async (key) => {
260
+ this.data[key] = await this._readFile(key);
261
+ });
262
+
263
+ await Promise.all(loadPromises);
264
+
265
+ // ensure usageStats and loginHistory objects exist
266
+ if (!this.data.loginHistory) this.data.loginHistory = [];
267
+ if (!this.data.usageStats) this.data.usageStats = {
268
+ apiRequests: 0,
269
+ mqttMessages: 0,
270
+ errors: 0,
271
+ reconnects: 0,
272
+ lastReset: new Date().toISOString()
273
+ };
274
+ if (!this.data.accountsIndex) this.data.accountsIndex = {};
275
+
276
+ // ensure mqtt helper objects exist
277
+ if (!this.data.mqttHealth) this.data.mqttHealth = { lastPingAt: null, lastPongAt: null, latencyMs: 0, missedPings: 0, status: 'unknown' };
278
+ if (!this.data.mqttReconnect) this.data.mqttReconnect = { attempts: 0, lastReason: null, lastAttemptAt: null, nextRetryInMs: 0 };
279
+ if (!this.data.mqttMessageCache) this.data.mqttMessageCache = { lastIds: [], max: 500 };
280
+ if (!this.data.mqttOutbox) this.data.mqttOutbox = [];
281
+ 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 };
282
+ if (!this.data.mqttTraffic) this.data.mqttTraffic = { messagesInPerMin: 0, messagesOutPerMin: 0, lastReset: new Date().toISOString() };
283
+ if (!this.data.mqttRisk) this.data.mqttRisk = { riskScore: 0, level: 'low', updatedAt: new Date().toISOString() };
284
+
285
+ return {
286
+ creds: this.data.creds,
287
+ device: this.data.device,
288
+ cookies: this.data.cookies,
289
+ mqttSession: this.data.mqttSession,
290
+ subscriptions: this.data.subscriptions,
291
+ seqIds: this.data.seqIds,
292
+ appState: this.data.appState,
293
+ irisState: this.data.irisState,
294
+ mqttTopics: this.data.mqttTopics,
295
+ mqttCapabilities: this.data.mqttCapabilities,
296
+ mqttAuth: this.data.mqttAuth,
297
+ lastPublishIds: this.data.lastPublishIds,
298
+ loginHistory: this.data.loginHistory,
299
+ usageStats: this.data.usageStats,
300
+ accountsIndex: this.data.accountsIndex,
301
+ mqttHealth: this.data.mqttHealth,
302
+ mqttReconnect: this.data.mqttReconnect,
303
+ mqttMessageCache: this.data.mqttMessageCache,
304
+ mqttOutbox: this.data.mqttOutbox,
305
+ mqttSubscriptionHealth: this.data.mqttSubscriptionHealth,
306
+ mqttTraffic: this.data.mqttTraffic,
307
+ mqttRisk: this.data.mqttRisk,
308
+ hasSession: !!(this.data.creds && this.data.cookies)
309
+ };
310
+ }
311
+
312
+ async saveAll() {
313
+ const savePromises = Object.keys(FILE_NAMES).map(async (key) => {
314
+ if (this.data[key] !== null && this.data[key] !== undefined) {
315
+ await this._writeFile(key, this.data[key]);
316
+ }
317
+ });
318
+ // also save usageStats and loginHistory explicitly
319
+ if (this.data.loginHistory) {
320
+ try { await this._writeFile('loginHistory', this.data.loginHistory); } catch (e) {}
321
+ }
322
+ if (this.data.usageStats) {
323
+ try { await this._writeFile('usageStats', this.data.usageStats); } catch (e) {}
324
+ }
325
+ if (this.data.accountsIndex) {
326
+ try { await this._writeFile('accountsIndex', this.data.accountsIndex); } catch (e) {}
327
+ }
328
+ // mqtt helpers
329
+ try { await this._writeFile('mqttHealth', this.data.mqttHealth); } catch (e) {}
330
+ try { await this._writeFile('mqttReconnect', this.data.mqttReconnect); } catch (e) {}
331
+ try { await this._writeFile('mqttMessageCache', this.data.mqttMessageCache); } catch (e) {}
332
+ try { await this._writeFile('mqttOutbox', this.data.mqttOutbox); } catch (e) {}
333
+ try { await this._writeFile('mqttSubscriptionHealth', this.data.mqttSubscriptionHealth); } catch (e) {}
334
+ try { await this._writeFile('mqttTraffic', this.data.mqttTraffic); } catch (e) {}
335
+ try { await this._writeFile('mqttRisk', this.data.mqttRisk); } catch (e) {}
336
+
337
+ await Promise.all(savePromises);
338
+ }
339
+
340
+ async saveCreds() {
341
+ const promises = [];
342
+ if (this.data.creds) promises.push(this._writeFile('creds', this.data.creds));
343
+ if (this.data.device) promises.push(this._writeFile('device', this.data.device));
344
+ if (this.data.cookies) promises.push(this._writeFile('cookies', this.data.cookies));
345
+ // also persist loginHistory and usageStats after creds save
346
+ if (this.data.loginHistory) promises.push(this._writeFile('loginHistory', this.data.loginHistory));
347
+ if (this.data.usageStats) promises.push(this._writeFile('usageStats', this.data.usageStats));
348
+ await Promise.all(promises);
349
+ this.emit('creds-saved');
350
+ }
351
+
352
+ async saveMqttState() {
353
+ // Save base mqtt data
354
+ const promises = [];
355
+ if (this.data.mqttSession) promises.push(this._writeFile('mqttSession', this.data.mqttSession));
356
+ if (this.data.subscriptions) promises.push(this._writeFile('subscriptions', this.data.subscriptions));
357
+ if (this.data.seqIds) promises.push(this._writeFile('seqIds', this.data.seqIds));
358
+
359
+ // Save extended mqtt/realtime data (new files)
360
+ if (this.data.irisState) promises.push(this._writeFile('irisState', this.data.irisState));
361
+ if (this.data.mqttTopics) promises.push(this._writeFile('mqttTopics', this.data.mqttTopics));
362
+ if (this.data.mqttCapabilities) promises.push(this._writeFile('mqttCapabilities', this.data.mqttCapabilities));
363
+ if (this.data.mqttAuth) promises.push(this._writeFile('mqttAuth', this.data.mqttAuth));
364
+ if (this.data.lastPublishIds) promises.push(this._writeFile('lastPublishIds', this.data.lastPublishIds));
365
+
366
+ // mqtt helpers
367
+ promises.push(this._writeFile('mqttHealth', this.data.mqttHealth));
368
+ promises.push(this._writeFile('mqttReconnect', this.data.mqttReconnect));
369
+ promises.push(this._writeFile('mqttMessageCache', this.data.mqttMessageCache));
370
+ promises.push(this._writeFile('mqttOutbox', this.data.mqttOutbox));
371
+ promises.push(this._writeFile('mqttSubscriptionHealth', this.data.mqttSubscriptionHealth));
372
+ promises.push(this._writeFile('mqttTraffic', this.data.mqttTraffic));
373
+ promises.push(this._writeFile('mqttRisk', this.data.mqttRisk));
374
+
375
+ await Promise.all(promises);
376
+ this.emit('mqtt-state-saved');
377
+ }
378
+
379
+ async saveAppState() {
380
+ if (this.data.appState) {
381
+ await this._writeFile('appState', this.data.appState);
382
+ }
383
+ }
384
+
385
+ _debouncedSave(keys) {
386
+ keys.forEach(k => this._dirty.add(k));
387
+
388
+ if (this._saveDebounceTimer) {
389
+ clearTimeout(this._saveDebounceTimer);
390
+ }
391
+
392
+ this._saveDebounceTimer = setTimeout(async () => {
393
+ const toSave = Array.from(this._dirty);
394
+ this._dirty.clear();
395
+
396
+ for (const key of toSave) {
397
+ if (this.data[key] !== null && this.data[key] !== undefined) {
398
+ try {
399
+ // ensure backup for critical files
400
+ if (['creds', 'cookies', 'device'].includes(key)) {
401
+ await this._createBackup(key);
402
+ }
403
+ await this._writeFile(key, this.data[key]);
404
+ } catch (e) {
405
+ console.error(`[MultiFileAuthState] Debounced write error for ${key}:`, e.message);
406
+ // increment usageStats.errors
407
+ try { this.incrementStat('errors'); } catch (e2) {}
408
+ }
409
+ }
410
+ }
411
+ this.emit('debounced-save-complete', toSave);
412
+ }, this._saveDebounceMs);
413
+ }
414
+
415
+ // --- Setters that schedule debounced saves ---
416
+ setCreds(creds) {
417
+ this.data.creds = creds;
418
+ this._debouncedSave(['creds']);
419
+ }
420
+
421
+ setDevice(device) {
422
+ this.data.device = device;
423
+ this._debouncedSave(['device']);
424
+ }
425
+
426
+ setCookies(cookies) {
427
+ this.data.cookies = cookies;
428
+ this._debouncedSave(['cookies']);
429
+ }
430
+
431
+ setMqttSession(session) {
432
+ this.data.mqttSession = session;
433
+ this._debouncedSave(['mqttSession']);
434
+ }
435
+
436
+ setSubscriptions(subs) {
437
+ this.data.subscriptions = subs;
438
+ this._debouncedSave(['subscriptions']);
439
+ }
440
+
441
+ setSeqIds(seqIds) {
442
+ this.data.seqIds = seqIds;
443
+ this._debouncedSave(['seqIds']);
444
+ }
445
+
446
+ setAppState(state) {
447
+ this.data.appState = state;
448
+ this._debouncedSave(['appState']);
449
+ }
450
+
451
+ // --- New setters for additional MQTT files ---
452
+ setIrisState(irisState) {
453
+ this.data.irisState = irisState;
454
+ this._debouncedSave(['irisState']);
455
+ }
456
+
457
+ setMqttTopics(topics) {
458
+ this.data.mqttTopics = topics;
459
+ this._debouncedSave(['mqttTopics']);
460
+ }
461
+
462
+ setMqttCapabilities(capabilities) {
463
+ this.data.mqttCapabilities = capabilities;
464
+ this._debouncedSave(['mqttCapabilities']);
465
+ }
466
+
467
+ setMqttAuth(auth) {
468
+ this.data.mqttAuth = auth;
469
+ this._debouncedSave(['mqttAuth']);
470
+ }
471
+
472
+ setLastPublishIds(lastPublishIds) {
473
+ this.data.lastPublishIds = lastPublishIds;
474
+ this._debouncedSave(['lastPublishIds']);
475
+ }
476
+
477
+ // --- Utility setters ---
478
+ addLoginHistory(entry) {
479
+ if (!this.data.loginHistory) this.data.loginHistory = [];
480
+ this.data.loginHistory.unshift(entry);
481
+ // keep bounded history length
482
+ if (this.data.loginHistory.length > 200) this.data.loginHistory.length = 200;
483
+ this._debouncedSave(['loginHistory']);
484
+ this.emit('login-attempt', entry);
485
+ }
486
+
487
+ incrementStat(statName, by = 1) {
488
+ if (!this.data.usageStats) this.data.usageStats = {
489
+ apiRequests: 0,
490
+ mqttMessages: 0,
491
+ errors: 0,
492
+ reconnects: 0,
493
+ lastReset: new Date().toISOString()
494
+ };
495
+ if (typeof this.data.usageStats[statName] === 'number') {
496
+ this.data.usageStats[statName] += by;
497
+ } else {
498
+ this.data.usageStats[statName] = (this.data.usageStats[statName] || 0) + by;
499
+ }
500
+ this._debouncedSave(['usageStats']);
501
+ }
502
+
503
+ // --- MQTT helper methods ---
504
+
505
+ // update mqtt health and persist
506
+ _updateMqttHealth(partial) {
507
+ try {
508
+ if (!this.data.mqttHealth) this.data.mqttHealth = {};
509
+ Object.assign(this.data.mqttHealth, partial);
510
+ // recompute status
511
+ if (this.data.mqttHealth.missedPings > 3) {
512
+ this.data.mqttHealth.status = 'degraded';
513
+ } else if (this.data.mqttHealth.lastPongAt) {
514
+ this.data.mqttHealth.status = 'ok';
515
+ } else {
516
+ this.data.mqttHealth.status = this.data.mqttHealth.status || 'unknown';
517
+ }
518
+ this._debouncedSave(['mqttHealth']);
519
+ } catch (e) {}
520
+ }
521
+
522
+ // mark ping sent
523
+ markPing() {
524
+ this._updateMqttHealth({ lastPingAt: new Date().toISOString() });
525
+ }
526
+
527
+ // mark pong received and update latency
528
+ markPong() {
529
+ const now = Date.now();
530
+ const lastPing = this.data.mqttHealth?.lastPingAt ? new Date(this.data.mqttHealth.lastPingAt).getTime() : null;
531
+ const latency = lastPing ? (now - lastPing) : 0;
532
+ const missed = Math.max(0, (this.data.mqttHealth?.missedPings || 0) - 1);
533
+ this._updateMqttHealth({ lastPongAt: new Date().toISOString(), latencyMs: latency, missedPings: missed });
534
+ }
535
+
536
+ // increment missed ping
537
+ markMissedPing() {
538
+ const missed = (this.data.mqttHealth?.missedPings || 0) + 1;
539
+ this._updateMqttHealth({ missedPings: missed });
540
+ }
541
+
542
+ // message deduplication: return true if duplicate
543
+ isDuplicateMessage(messageId) {
544
+ if (!messageId) return false;
545
+ if (!this.data.mqttMessageCache) this.data.mqttMessageCache = { lastIds: [], max: 500 };
546
+ const idx = this.data.mqttMessageCache.lastIds.indexOf(messageId);
547
+ if (idx !== -1) return true;
548
+ // push and cap
549
+ this.data.mqttMessageCache.lastIds.push(messageId);
550
+ if (this.data.mqttMessageCache.lastIds.length > (this.data.mqttMessageCache.max || 500)) {
551
+ this.data.mqttMessageCache.lastIds.shift();
552
+ }
553
+ this._debouncedSave(['mqttMessageCache']);
554
+ return false;
555
+ }
556
+
557
+ // enqueue outgoing message to outbox
558
+ enqueueOutbox(item) {
559
+ if (!this.data.mqttOutbox) this.data.mqttOutbox = [];
560
+ // item: { topic, payload, qos, createdAt, tries }
561
+ const i = Object.assign({ qos: 1, createdAt: new Date().toISOString(), tries: 0 }, item);
562
+ this.data.mqttOutbox.push(i);
563
+ this._debouncedSave(['mqttOutbox']);
564
+ this.emit('outbox-enqueued', i);
565
+ return i;
566
+ }
567
+
568
+ // flush outbox (attempt send). Tries to use common publish/send methods on client
569
+ async flushOutbox(realtimeClient, maxPerRun = 50) {
570
+ if (!this.data.mqttOutbox || !this.data.mqttOutbox.length) return 0;
571
+ const sent = [];
572
+ const keep = [];
573
+ let processed = 0;
574
+ for (const item of this.data.mqttOutbox) {
575
+ if (processed >= maxPerRun) {
576
+ keep.push(item);
577
+ continue;
578
+ }
579
+ processed++;
580
+ try {
581
+ item.tries = (item.tries || 0) + 1;
582
+ // try publish methods in order of likelihood
583
+ let ok = false;
584
+ if (realtimeClient && typeof realtimeClient.publish === 'function') {
585
+ // mqtt.js style
586
+ try {
587
+ realtimeClient.publish(item.topic, typeof item.payload === 'string' ? item.payload : JSON.stringify(item.payload), { qos: item.qos });
588
+ ok = true;
589
+ } catch (e) {
590
+ ok = false;
591
+ }
592
+ } else if (realtimeClient && typeof realtimeClient.send === 'function') {
593
+ try {
594
+ realtimeClient.send(item.topic, item.payload);
595
+ ok = true;
596
+ } catch (e) { ok = false; }
597
+ } else if (realtimeClient && typeof realtimeClient.sendMessage === 'function') {
598
+ try {
599
+ realtimeClient.sendMessage(item.payload);
600
+ ok = true;
601
+ } catch (e) { ok = false; }
602
+ } else {
603
+ // cannot send: keep in outbox
604
+ ok = false;
605
+ }
606
+
607
+ if (ok) {
608
+ sent.push(item);
609
+ this.incrementStat('mqttMessages', 1);
610
+ } else {
611
+ // keep for retry, but if tries exceed threshold move to dead-letter (drop)
612
+ if (item.tries >= 10) {
613
+ this.emit('outbox-dropped', item);
614
+ } else {
615
+ keep.push(item);
616
+ }
617
+ }
618
+ } catch (e) {
619
+ keep.push(item);
620
+ }
621
+ }
622
+ // replace queue
623
+ this.data.mqttOutbox = keep.concat(this.data.mqttOutbox.slice(processed));
624
+ this._debouncedSave(['mqttOutbox']);
625
+ this.emit('outbox-flushed', { sentCount: sent.length, remaining: this.data.mqttOutbox.length });
626
+ return sent.length;
627
+ }
628
+
629
+ // subscription watchdog: check and attempt resubscribe
630
+ async checkSubscriptions(realtimeClient) {
631
+ try {
632
+ const expected = this.data.mqttSubscriptionHealth?.expected || (this.data.mqttTopics?.topics || []);
633
+ let active = [];
634
+ // try to read from client
635
+ if (realtimeClient?.connection?.subscribedTopics) active = realtimeClient.connection.subscribedTopics;
636
+ else if (realtimeClient?._subscribedTopics) active = realtimeClient._subscribedTopics;
637
+ else if (Array.isArray(this.data.mqttTopics?.topics)) active = this.data.mqttTopics.topics;
638
+
639
+ const needs = expected.filter(t => !active.includes(t));
640
+ this.data.mqttSubscriptionHealth = {
641
+ expected,
642
+ active,
643
+ lastCheck: new Date().toISOString(),
644
+ needsResubscribe: needs.length > 0
645
+ };
646
+ this._debouncedSave(['mqttSubscriptionHealth']);
647
+
648
+ if (needs.length && realtimeClient) {
649
+ for (const t of needs) {
650
+ try {
651
+ // call common subscribe methods
652
+ if (typeof realtimeClient.subscribe === 'function') {
653
+ realtimeClient.subscribe(t);
654
+ } else if (realtimeClient.connection && typeof realtimeClient.connection.subscribe === 'function') {
655
+ realtimeClient.connection.subscribe(t);
656
+ } else if (typeof realtimeClient.subscriptions === 'function') {
657
+ realtimeClient.subscriptions([t]);
658
+ }
659
+ // update active
660
+ if (!this.data.mqttSubscriptionHealth.active.includes(t)) this.data.mqttSubscriptionHealth.active.push(t);
661
+ this.emit('resubscribe-attempt', { topic: t });
662
+ } catch (e) {
663
+ this.emit('resubscribe-failed', { topic: t, error: e.message });
664
+ }
665
+ }
666
+ this._debouncedSave(['mqttSubscriptionHealth']);
667
+ }
668
+ return this.data.mqttSubscriptionHealth;
669
+ } catch (e) {
670
+ return null;
671
+ }
672
+ }
673
+
674
+ // traffic profiler: update incoming/outgoing counters
675
+ _updateTraffic(incoming = 0, outgoing = 0) {
676
+ try {
677
+ if (!this.data.mqttTraffic) this.data.mqttTraffic = { messagesInPerMin: 0, messagesOutPerMin: 0, lastReset: new Date().toISOString() };
678
+ const lastReset = new Date(this.data.mqttTraffic.lastReset).getTime();
679
+ if (Date.now() - lastReset > 60 * 1000) {
680
+ // reset every minute
681
+ this.data.mqttTraffic.messagesInPerMin = 0;
682
+ this.data.mqttTraffic.messagesOutPerMin = 0;
683
+ this.data.mqttTraffic.lastReset = new Date().toISOString();
684
+ }
685
+ this.data.mqttTraffic.messagesInPerMin += incoming;
686
+ this.data.mqttTraffic.messagesOutPerMin += outgoing;
687
+ this._debouncedSave(['mqttTraffic']);
688
+ } catch (e) {}
689
+ }
690
+
691
+ // compute simple risk score from reconnects, errors, traffic
692
+ _computeRiskScore() {
693
+ try {
694
+ const reconnects = this.data.usageStats?.reconnects || 0;
695
+ const errors = this.data.usageStats?.errors || 0;
696
+ const outPerMin = this.data.mqttTraffic?.messagesOutPerMin || 0;
697
+ // simple heuristic
698
+ let score = 0;
699
+ score += Math.min(1, reconnects / 10) * 0.4;
700
+ score += Math.min(1, errors / 20) * 0.3;
701
+ score += Math.min(1, outPerMin / 200) * 0.3;
702
+ const level = score > 0.7 ? 'high' : score > 0.35 ? 'medium' : 'low';
703
+ this.data.mqttRisk = { riskScore: Number(score.toFixed(3)), level, updatedAt: new Date().toISOString() };
704
+ this._debouncedSave(['mqttRisk']);
705
+ this.emit('risk-updated', this.data.mqttRisk);
706
+ return this.data.mqttRisk;
707
+ } catch (e) {
708
+ return null;
709
+ }
710
+ }
711
+
712
+ // schedule auto reconnect using backoff; will attempt to call client.reconnect() if available or emit event
713
+ _scheduleReconnect(realtimeClient, reason = 'unknown') {
714
+ try {
715
+ this._reconnectAttempts = (this._reconnectAttempts || 0) + 1;
716
+ const idx = Math.min(this._reconnectAttempts - 1, this._reconnectBackoff.length - 1);
717
+ const delay = this._reconnectBackoff[idx] || this._reconnectBackoff[this._reconnectBackoff.length - 1];
718
+ this.data.mqttReconnect = {
719
+ attempts: this._reconnectAttempts,
720
+ lastReason: reason,
721
+ lastAttemptAt: new Date().toISOString(),
722
+ nextRetryInMs: delay
723
+ };
724
+ this.incrementStat('reconnects', 1);
725
+ this._debouncedSave(['mqttReconnect']);
726
+ if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
727
+ this._reconnectTimer = setTimeout(async () => {
728
+ try {
729
+ if (realtimeClient) {
730
+ if (typeof realtimeClient.reconnect === 'function') {
731
+ realtimeClient.reconnect();
732
+ } else if (typeof realtimeClient.connect === 'function') {
733
+ // some libs have connect
734
+ realtimeClient.connect();
735
+ } else {
736
+ // can't auto-call: emit event so app can handle
737
+ this.emit('should-reconnect', { reason });
738
+ }
739
+ } else {
740
+ this.emit('should-reconnect', { reason });
741
+ }
742
+ } catch (e) {
743
+ this.emit('reconnect-failed', { reason, error: e.message });
744
+ }
745
+ }, delay);
746
+ } catch (e) {}
747
+ }
748
+
749
+ // reset reconnect attempts after successful connect
750
+ _resetReconnect() {
751
+ this._reconnectAttempts = 0;
752
+ if (this._reconnectTimer) {
753
+ clearTimeout(this._reconnectTimer);
754
+ this._reconnectTimer = null;
755
+ }
756
+ if (this.data.mqttReconnect) {
757
+ this.data.mqttReconnect.attempts = 0;
758
+ this.data.mqttReconnect.nextRetryInMs = 0;
759
+ this._debouncedSave(['mqttReconnect']);
760
+ }
761
+ }
762
+
763
+ // warm-restore: apply saved options into realtimeClient before connect to speed startup
764
+ warmRestoreClient(realtimeClient) {
765
+ try {
766
+ const opts = this.getMqttConnectOptions();
767
+ if (!opts) return false;
768
+ // Merge irisData, mqttAuthToken etc. into client init options if possible
769
+ if (realtimeClient.initOptions && typeof realtimeClient.initOptions === 'object') {
770
+ Object.assign(realtimeClient.initOptions, opts);
771
+ } else {
772
+ realtimeClient.initOptions = opts;
773
+ }
774
+ // also set connection clientInfo if supported
775
+ if (realtimeClient.connection && realtimeClient.connection.clientInfo && this.data.mqttSession?.mqttSessionId) {
776
+ realtimeClient.connection.clientInfo.clientMqttSessionId = this.data.mqttSession.mqttSessionId;
777
+ }
778
+ this.emit('warm-restore', { options: opts });
779
+ return true;
780
+ } catch (e) {
781
+ return false;
782
+ }
783
+ }
784
+
785
+ // attach runtime helpers to realtimeClient to manage instant improvements
786
+ attachRealtimeClient(realtimeClient, opts = {}) {
787
+ try {
788
+ if (!realtimeClient) return;
789
+
790
+ // warm restore immediately
791
+ try { this.warmRestoreClient(realtimeClient); } catch (e) {}
792
+
793
+ // ping/pong monitoring
794
+ const pingIntervalMs = opts.pingIntervalMs || 30 * 1000; // 30s
795
+ let pingTimer = null;
796
+ const startPing = () => {
797
+ if (pingTimer) clearInterval(pingTimer);
798
+ pingTimer = setInterval(() => {
799
+ try {
800
+ this.markPing();
801
+ if (typeof realtimeClient.ping === 'function') {
802
+ realtimeClient.ping();
803
+ } else if (typeof realtimeClient.pingReq === 'function') {
804
+ realtimeClient.pingReq();
805
+ } else if (typeof realtimeClient.pingServer === 'function') {
806
+ realtimeClient.pingServer();
807
+ } else {
808
+ // some libraries use publish to $SYS topic or nothing; we still mark ping
809
+ }
810
+ // after ping, schedule missed ping detection
811
+ setTimeout(() => {
812
+ // if lastPongAt older than lastPingAt, consider missed
813
+ const lastPing = new Date(this.data.mqttHealth?.lastPingAt || 0).getTime();
814
+ const lastPong = new Date(this.data.mqttHealth?.lastPongAt || 0).getTime();
815
+ if (lastPing && (!lastPong || lastPong < lastPing)) {
816
+ this.markMissedPing();
817
+ }
818
+ }, Math.min(5000, Math.floor(pingIntervalMs / 2)));
819
+ } catch (e) {}
820
+ }, pingIntervalMs);
821
+ };
822
+ const stopPing = () => { if (pingTimer) clearInterval(pingTimer); pingTimer = null; };
823
+
824
+ // event handlers (subscribe to multiple common event names)
825
+ const onConnect = async (...args) => {
826
+ this._resetReconnect();
827
+ this.data.mqttSession = this.data.mqttSession || {};
828
+ this.data.mqttSession.lastConnected = new Date().toISOString();
829
+ this._debouncedSave(['mqttSession']);
830
+ this._updateMqttHealth({ status: 'ok', lastPongAt: new Date().toISOString(), missedPings: 0 });
831
+ this.emit('realtime-connected', { args });
832
+ // flush outbox on connect
833
+ try { await this.flushOutbox(realtimeClient); } catch (e) {}
834
+ // check subscriptions
835
+ try { await this.checkSubscriptions(realtimeClient); } catch (e) {}
836
+ startPing();
837
+ };
838
+
839
+ const onClose = (...args) => {
840
+ stopPing();
841
+ this._updateMqttHealth({ status: 'dead' });
842
+ this._scheduleReconnect(realtimeClient, 'close');
843
+ this.emit('realtime-closed', { args });
844
+ };
845
+
846
+ const onReconnect = (...args) => {
847
+ this.emit('realtime-reconnect', { args });
848
+ };
849
+
850
+ const onOffline = (...args) => {
851
+ this._updateMqttHealth({ status: 'degraded' });
852
+ this._scheduleReconnect(realtimeClient, 'offline');
853
+ this.emit('realtime-offline', { args });
854
+ };
855
+
856
+ const onError = (err) => {
857
+ this.incrementStat('errors', 1);
858
+ this._scheduleReconnect(realtimeClient, err?.message || 'error');
859
+ this.emit('realtime-error', { error: err?.message || String(err) });
860
+ };
861
+
862
+ const onMessage = async (topic, payload, packet) => {
863
+ // generic message handler: try to extract id for dedupe
864
+ // Many IG messages contain an id field in JSON payload; try to parse
865
+ let messageId = null;
866
+ try {
867
+ const s = (typeof payload === 'string') ? payload : (payload && payload.toString ? payload.toString() : null);
868
+ if (s) {
869
+ const j = JSON.parse(s);
870
+ if (j && (j.message_id || j.id || j.msg_id)) messageId = j.message_id || j.id || j.msg_id;
871
+ }
872
+ } catch (e) {
873
+ // not json, ignore
874
+ }
875
+ if (messageId && this.isDuplicateMessage(messageId)) {
876
+ this.emit('message-duplicate', { topic, messageId });
877
+ return;
878
+ }
879
+ // update traffic counters
880
+ this._updateTraffic(1, 0);
881
+ this.incrementStat('mqttMessages', 1);
882
+ // mark pong if message looks like pong or heartbeat
883
+ if (topic && topic.includes('pong')) this.markPong();
884
+ this.emit('realtime-message', { topic, payload, packet });
885
+ };
886
+
887
+ // wire up events safely for common handlers
888
+ try {
889
+ // mqtt.js style
890
+ if (typeof realtimeClient.on === 'function') {
891
+ realtimeClient.on('connect', onConnect);
892
+ realtimeClient.on('reconnect', onReconnect);
893
+ realtimeClient.on('close', onClose);
894
+ realtimeClient.on('offline', onOffline);
895
+ realtimeClient.on('error', onError);
896
+ realtimeClient.on('message', onMessage);
897
+ }
898
+ // websockets or other: attempt to attach possible event names
899
+ if (realtimeClient.addEventListener && typeof realtimeClient.addEventListener === 'function') {
900
+ try { realtimeClient.addEventListener('open', onConnect); } catch (e) {}
901
+ try { realtimeClient.addEventListener('close', onClose); } catch (e) {}
902
+ try { realtimeClient.addEventListener('error', onError); } catch (e) {}
903
+ try { realtimeClient.addEventListener('message', (ev) => onMessage(ev.topic || ev.type, ev.data || ev.payload, ev)); } catch (e) {}
904
+ }
905
+ } catch (e) {}
906
+
907
+ // also attach one-time status: if the client supports events differently, let app listen to 'should-reconnect'
908
+ // attach a cleanup handle
909
+ const cleanup = () => {
910
+ stopPing();
911
+ try {
912
+ if (typeof realtimeClient.off === 'function') {
913
+ realtimeClient.off('connect', onConnect);
914
+ realtimeClient.off('reconnect', onReconnect);
915
+ realtimeClient.off('close', onClose);
916
+ realtimeClient.off('offline', onOffline);
917
+ realtimeClient.off('error', onError);
918
+ realtimeClient.off('message', onMessage);
919
+ }
920
+ } catch (e) {}
921
+ this.emit('realtime-detach');
922
+ };
923
+
924
+ // expose cleanup on client for convenience
925
+ try { realtimeClient._authStateCleanup = cleanup; } catch (e) {}
926
+
927
+ // return helper object to caller
928
+ return {
929
+ pingIntervalMs,
930
+ stop: cleanup,
931
+ flushOutbox: async (max) => { return await this.flushOutbox(realtimeClient, max); },
932
+ checkSubscriptions: async () => { return await this.checkSubscriptions(realtimeClient); },
933
+ warmRestore: () => { return this.warmRestoreClient(realtimeClient); }
934
+ };
935
+ } catch (e) {
936
+ // emit attach failure
937
+ this.emit('attach-failed', { error: e.message });
938
+ return null;
939
+ }
940
+ }
941
+
942
+ // --- Getters ---
943
+ getCreds() { return this.data.creds; }
944
+ getDevice() { return this.data.device; }
945
+ getCookies() { return this.data.cookies; }
946
+ getMqttSession() { return this.data.mqttSession; }
947
+ getSubscriptions() { return this.data.subscriptions; }
948
+ getSeqIds() { return this.data.seqIds; }
949
+ getAppState() { return this.data.appState; }
950
+ getIrisState() { return this.data.irisState; }
951
+ getMqttTopics() { return this.data.mqttTopics; }
952
+ getMqttCapabilities() { return this.data.mqttCapabilities; }
953
+ getMqttAuth() { return this.data.mqttAuth; }
954
+ getLastPublishIds() { return this.data.lastPublishIds; }
955
+ getLoginHistory() { return this.data.loginHistory || []; }
956
+ getUsageStats() { return this.data.usageStats || {}; }
957
+ getAccountsIndex() { return this.data.accountsIndex || {}; }
958
+ getMqttHealth() { return this.data.mqttHealth || {}; }
959
+ getMqttReconnect() { return this.data.mqttReconnect || {}; }
960
+ getMqttMessageCache() { return this.data.mqttMessageCache || {}; }
961
+ getMqttOutbox() { return this.data.mqttOutbox || []; }
962
+ getMqttSubscriptionHealth() { return this.data.mqttSubscriptionHealth || {}; }
963
+ getMqttTraffic() { return this.data.mqttTraffic || {}; }
964
+ getMqttRisk() { return this.data.mqttRisk || {}; }
965
+
966
+ hasValidSession() {
967
+ return !!(this.data.creds && this.data.cookies && this.data.creds.authorization);
968
+ }
969
+
970
+ hasMqttSession() {
971
+ return !!(
972
+ (this.data.mqttSession && this.data.mqttSession.sessionId) ||
973
+ (this.data.mqttAuth && this.data.mqttAuth.jwt)
974
+ );
975
+ }
976
+
977
+ async clearAll() {
978
+ for (const key of Object.keys(FILE_NAMES)) {
979
+ const filePath = this._getFilePath(key);
980
+ try {
981
+ if (fs.existsSync(filePath)) {
982
+ await fs.promises.unlink(filePath);
983
+ }
984
+ } catch (e) {}
985
+ this.data[key] = null;
986
+ }
987
+ // clear backups too? leave backups intact but emit event
988
+ this.emit('cleared-all');
989
+ }
990
+
991
+ // --- Backup utilities exposed ---
992
+ async listBackups() {
993
+ try {
994
+ const b = this._getBackupFolder();
995
+ if (!fs.existsSync(b)) return [];
996
+ const files = await fs.promises.readdir(b);
997
+ return files.map(f => path.join(b, f));
998
+ } catch (e) {
999
+ return [];
1000
+ }
1001
+ }
1002
+
1003
+ // --- Device fingerprint helpers ---
1004
+ computeDeviceFingerprint(deviceObj) {
1005
+ try {
1006
+ const s = JSON.stringify(deviceObj || this.data.device || {});
1007
+ return crypto.createHash('sha256').update(s).digest('hex');
1008
+ } catch (e) {
1009
+ return null;
1010
+ }
1011
+ }
1012
+
1013
+ // lock device fingerprint: saves hash into device.json (device.locked = true)
1014
+ async lockDeviceFingerprint() {
1015
+ try {
1016
+ if (!this.data.device) this.data.device = {};
1017
+ const hash = this.computeDeviceFingerprint(this.data.device);
1018
+ this.data.device.fingerprintHash = hash;
1019
+ this.data.device.locked = true;
1020
+ await this._writeFile('device', this.data.device);
1021
+ this.emit('device-locked', { fingerprintHash: hash });
1022
+ return hash;
1023
+ } catch (e) {
1024
+ console.warn('[MultiFileAuthState] lockDeviceFingerprint failed:', e.message);
1025
+ return null;
1026
+ }
1027
+ }
1028
+
1029
+ verifyDeviceFingerprint(deviceObj) {
1030
+ try {
1031
+ const stored = this.data.device?.fingerprintHash || null;
1032
+ if (!stored) return true; // no lock present
1033
+ const hash = this.computeDeviceFingerprint(deviceObj || this.data.device);
1034
+ return stored === hash;
1035
+ } catch (e) {
1036
+ return false;
1037
+ }
1038
+ }
1039
+
1040
+ // --- Login history helpers ---
1041
+ recordLoginAttempt({ success, ip, userAgent, method = 'password', reason = null }) {
1042
+ const entry = {
1043
+ date: new Date().toISOString(),
1044
+ success: Boolean(success),
1045
+ ip: ip || null,
1046
+ userAgent: userAgent || null,
1047
+ method,
1048
+ reason
1049
+ };
1050
+ this.addLoginHistory(entry);
1051
+ return entry;
1052
+ }
1053
+
1054
+ // mark account restricted / checkpoint
1055
+ markAccountRestricted({ status = 'checkpoint', reason = null }) {
1056
+ if (!this.data.creds) this.data.creds = {};
1057
+ this.data.creds.accountStatus = status;
1058
+ this.data.creds.restrictedAt = new Date().toISOString();
1059
+ this.data.creds.lastError = reason;
1060
+ this._debouncedSave(['creds']);
1061
+ this.emit('account-restricted', { status, reason });
1062
+ }
1063
+
1064
+ // clear restriction
1065
+ clearAccountRestriction() {
1066
+ if (this.data.creds) {
1067
+ delete this.data.creds.accountStatus;
1068
+ delete this.data.creds.restrictedAt;
1069
+ delete this.data.creds.lastError;
1070
+ this._debouncedSave(['creds']);
1071
+ this.emit('account-unrestricted');
1072
+ }
1073
+ }
1074
+
1075
+ // --- Health check ---
1076
+ getHealth() {
1077
+ return {
1078
+ validSession: this.hasValidSession(),
1079
+ hasCookies: !!this.data.cookies,
1080
+ hasMqtt: this.hasMqttSession(),
1081
+ lastLogin: (this.data.loginHistory && this.data.loginHistory[0]?.date) || null,
1082
+ usageStats: this.getUsageStats(),
1083
+ accountStatus: this.data.creds?.accountStatus || 'ok',
1084
+ deviceLocked: !!this.data.device?.locked,
1085
+ mqttHealth: this.getMqttHealth(),
1086
+ mqttRisk: this.getMqttRisk()
1087
+ };
1088
+ }
1089
+ }
1090
+
1091
+ // Helper: try to safely extract cookieJar serialization if exists
1092
+ async function trySerializeCookieJar(igState) {
1093
+ let cookies = null;
1094
+ try {
1095
+ if (igState.cookieJar && typeof igState.serializeCookieJar === 'function') {
1096
+ cookies = await igState.serializeCookieJar();
1097
+ } else if (igState.cookieJar && typeof igState.cookieJar.serialize === 'function') {
1098
+ // fallback: tough-cookie jar serialize
1099
+ const ser = await util.promisify(igState.cookieJar.serialize).bind(igState.cookieJar)();
1100
+ cookies = ser;
1101
+ }
1102
+ } catch (e) {
1103
+ console.warn('[MultiFileAuthState] Could not serialize cookies:', e.message);
1104
+ }
1105
+ return cookies;
1106
+ }
1107
+
1108
+ // Helper: try to read a cookie value from a tough-cookie Jar or equivalent
1109
+ async function getCookieValueFromJar(cookieJar, name) {
1110
+ if (!cookieJar) return null;
1111
+
1112
+ // try getCookieString (tough-cookie)
1113
+ try {
1114
+ if (typeof cookieJar.getCookieString === 'function') {
1115
+ const getCookieString = util.promisify(cookieJar.getCookieString).bind(cookieJar);
1116
+ const urls = ['https://www.instagram.com', 'https://instagram.com', 'https://i.instagram.com'];
1117
+ for (const url of urls) {
1118
+ try {
1119
+ const cookieString = await getCookieString(url);
1120
+ if (cookieString && typeof cookieString === 'string') {
1121
+ const pairs = cookieString.split(';').map(s => s.trim());
1122
+ for (const p of pairs) {
1123
+ const [k, ...rest] = p.split('=');
1124
+ if (k === name) return rest.join('=');
1125
+ }
1126
+ }
1127
+ } catch (e) {
1128
+ // ignore and try next url
1129
+ }
1130
+ }
1131
+ }
1132
+ } catch (e) {
1133
+ // ignore
1134
+ }
1135
+
1136
+ // try getCookies which returns Cookie objects
1137
+ try {
1138
+ if (typeof cookieJar.getCookies === 'function') {
1139
+ const getCookies = util.promisify(cookieJar.getCookies).bind(cookieJar);
1140
+ const urls = ['https://www.instagram.com', 'https://instagram.com', 'https://i.instagram.com'];
1141
+ for (const url of urls) {
1142
+ try {
1143
+ const cookies = await getCookies(url);
1144
+ if (Array.isArray(cookies)) {
1145
+ for (const c of cookies) {
1146
+ // cookie object shapes vary: check common keys
1147
+ const key = c.key ?? c.name ?? c.name;
1148
+ const val = c.value ?? c.value ?? c.value;
1149
+ if (key === name) return val;
1150
+ }
1151
+ }
1152
+ } catch (e) {
1153
+ // ignore and try next url
1154
+ }
1155
+ }
1156
+ }
1157
+ } catch (e) {
1158
+ // ignore
1159
+ }
1160
+
1161
+ // try serialized cookie jar structure if present
1162
+ try {
1163
+ if (cookieJar && typeof cookieJar === 'object' && cookieJar.cookies && Array.isArray(cookieJar.cookies)) {
1164
+ const found = cookieJar.cookies.find(c => (c.key === name || c.name === name || c.name === name));
1165
+ if (found) return found.value ?? found.value;
1166
+ }
1167
+ } catch (e) {
1168
+ // ignore
1169
+ }
1170
+
1171
+ return null;
1172
+ }
1173
+
1174
+ // Extract cookie-derived fields (sessionid, csrftoken, ds_user_id, mid)
1175
+ async function extractCookieFields(igState) {
1176
+ const out = {
1177
+ sessionIdCookie: null,
1178
+ csrfTokenCookie: null,
1179
+ dsUserIdCookie: null,
1180
+ midCookie: null
1181
+ };
1182
+ try {
1183
+ const cookieJar = igState.cookieJar;
1184
+ out.sessionIdCookie = await getCookieValueFromJar(cookieJar, 'sessionid');
1185
+ out.csrfTokenCookie = await getCookieValueFromJar(cookieJar, 'csrftoken');
1186
+ out.dsUserIdCookie = await getCookieValueFromJar(cookieJar, 'ds_user_id') || await getCookieValueFromJar(cookieJar, 'ds_user');
1187
+ out.midCookie = await getCookieValueFromJar(cookieJar, 'mid');
1188
+ } catch (e) {
1189
+ // ignore
1190
+ }
1191
+ return out;
1192
+ }
1193
+
1194
+ async function extractStateData(igState) {
1195
+ // Basic creds (existing)
1196
+ const creds = {
1197
+ authorization: igState.authorization || null,
1198
+ igWWWClaim: igState.igWWWClaim || null,
1199
+ passwordEncryptionKeyId: igState.passwordEncryptionKeyId || null,
1200
+ passwordEncryptionPubKey: igState.passwordEncryptionPubKey || null
1201
+ };
1202
+
1203
+ // Device remains same
1204
+ const device = {
1205
+ deviceString: igState.deviceString || null,
1206
+ deviceId: igState.deviceId || null,
1207
+ uuid: igState.uuid || null,
1208
+ phoneId: igState.phoneId || null,
1209
+ adid: igState.adid || null,
1210
+ build: igState.build || null
1211
+ };
1212
+
1213
+ // Try to serialize cookies (kept separate)
1214
+ const cookies = await trySerializeCookieJar(igState);
1215
+
1216
+ // App state remains same
1217
+ const appState = {
1218
+ language: igState.language || 'en_US',
1219
+ timezoneOffset: igState.timezoneOffset || null,
1220
+ connectionTypeHeader: igState.connectionTypeHeader || 'WIFI',
1221
+ capabilitiesHeader: igState.capabilitiesHeader || null,
1222
+ checkpoint: igState.checkpoint || null,
1223
+ challenge: igState.challenge || null
1224
+ };
1225
+
1226
+ // --- NEW: richer creds fields derived from igState + cookies ---
1227
+ // try to obtain cookie fields
1228
+ const cookieFields = await extractCookieFields(igState);
1229
+
1230
+ // Primary identifiers
1231
+ const userIdFromState = igState.cookieUserId || igState.userId || igState.user_id || null;
1232
+ const dsUserIdFromCookies = cookieFields.dsUserIdCookie || igState.dsUserId || igState.ds_user_id || null;
1233
+ const sessionIdFromCookies = cookieFields.sessionIdCookie || null;
1234
+ const csrfFromCookies = cookieFields.csrfTokenCookie || null;
1235
+ const midFromCookies = cookieFields.midCookie || igState.mid || null;
1236
+
1237
+ // username if exposed on state
1238
+ const usernameFromState = igState.username || igState.userName || igState.user?.username || null;
1239
+
1240
+ // rankToken: if userId + uuid available, form `${userId}_${uuid}`
1241
+ let rankToken = null;
1242
+ try {
1243
+ if (userIdFromState && (igState.uuid || device.uuid)) {
1244
+ rankToken = `${userIdFromState}_${igState.uuid || device.uuid}`;
1245
+ } else if (igState.rankToken) {
1246
+ rankToken = igState.rankToken;
1247
+ }
1248
+ } catch (e) {
1249
+ rankToken = igState.rankToken || null;
1250
+ }
1251
+
1252
+ // sessionId and csrfToken - prefer cookies, fallback to state fields if present
1253
+ const sessionId = sessionIdFromCookies || igState.sessionId || igState.sessionid || null;
1254
+ const csrfToken = csrfFromCookies || igState.csrfToken || igState.csrftoken || null;
1255
+
1256
+ // mid fallback
1257
+ const mid = midFromCookies || igState.mid || null;
1258
+
1259
+ // isLoggedIn heuristic: presence of sessionid cookie or an 'isLoggedIn' flag or authorization header
1260
+ const isLoggedIn = Boolean(
1261
+ sessionId ||
1262
+ igState.isLoggedIn ||
1263
+ (creds.authorization && creds.authorization.length)
1264
+ );
1265
+
1266
+ // loginAt / lastValidatedAt: try to take from igState if present, otherwise null
1267
+ const loginAt = igState.loginAt || igState.loggedInAt || null;
1268
+ const lastValidatedAt = igState.lastValidatedAt || igState.lastValidated || null;
1269
+
1270
+ // Attach all new fields into creds object (non-destructive)
1271
+ creds.userId = userIdFromState || dsUserIdFromCookies || creds.userId || null;
1272
+ creds.dsUserId = dsUserIdFromCookies || null;
1273
+ creds.username = usernameFromState || null;
1274
+ creds.rankToken = rankToken;
1275
+ creds.sessionId = sessionId;
1276
+ creds.csrfToken = csrfToken;
1277
+ creds.mid = mid;
1278
+ creds.isLoggedIn = isLoggedIn;
1279
+ creds.loginAt = loginAt;
1280
+ creds.lastValidatedAt = lastValidatedAt;
1281
+
1282
+ return { creds, device, cookies, appState };
1283
+ }
1284
+
1285
+ async function applyStateData(igState, authState) {
1286
+ const { creds, device, cookies, appState } = authState.data;
1287
+
1288
+ if (creds) {
1289
+ if (creds.authorization) igState.authorization = creds.authorization;
1290
+ if (creds.igWWWClaim) igState.igWWWClaim = creds.igWWWClaim;
1291
+ if (creds.passwordEncryptionKeyId) igState.passwordEncryptionKeyId = creds.passwordEncryptionKeyId;
1292
+ if (creds.passwordEncryptionPubKey) igState.passwordEncryptionPubKey = creds.passwordEncryptionPubKey;
1293
+ // if igState provides a helper to update authorization, call it
1294
+ if (typeof igState.updateAuthorization === 'function') {
1295
+ try { igState.updateAuthorization(); } catch (e) {}
1296
+ }
1297
+
1298
+ // apply new creds-derived fields if state supports them (non-intrusive)
1299
+ try {
1300
+ if (creds.userId && !igState.cookieUserId) igState.cookieUserId = creds.userId;
1301
+ if (creds.username && !igState.username) igState.username = creds.username;
1302
+ if (creds.rankToken && !igState.rankToken) igState.rankToken = creds.rankToken;
1303
+ if (creds.sessionId && !igState.sessionId) igState.sessionId = creds.sessionId;
1304
+ if (creds.csrfToken && !igState.csrfToken) igState.csrfToken = creds.csrfToken;
1305
+ if (creds.mid && !igState.mid) igState.mid = creds.mid;
1306
+ if (typeof creds.isLoggedIn !== 'undefined' && !igState.isLoggedIn) igState.isLoggedIn = creds.isLoggedIn;
1307
+ if (creds.loginAt && !igState.loginAt) igState.loginAt = creds.loginAt;
1308
+ if (creds.lastValidatedAt && !igState.lastValidatedAt) igState.lastValidatedAt = creds.lastValidatedAt;
1309
+ } catch (e) {
1310
+ // ignore if igState shape different
1311
+ }
1312
+ }
1313
+
1314
+ if (device) {
1315
+ if (device.deviceString) igState.deviceString = device.deviceString;
1316
+ if (device.deviceId) igState.deviceId = device.deviceId;
1317
+ if (device.uuid) igState.uuid = device.uuid;
1318
+ if (device.phoneId) igState.phoneId = device.phoneId;
1319
+ if (device.adid) igState.adid = device.adid;
1320
+ if (device.build) igState.build = device.build;
1321
+ }
1322
+
1323
+ if (cookies) {
1324
+ try {
1325
+ if (typeof igState.deserializeCookieJar === 'function') {
1326
+ await igState.deserializeCookieJar(cookies);
1327
+ } else if (igState.cookieJar && typeof igState.cookieJar.restore === 'function') {
1328
+ // fallback restore
1329
+ await util.promisify(igState.cookieJar.restore).bind(igState.cookieJar)(cookies);
1330
+ } else if (igState.cookieJar && typeof igState.cookieJar._importCookies === 'function') {
1331
+ // last-resort, not likely
1332
+ try { igState.cookieJar._importCookies(cookies); } catch (e) {}
1333
+ }
1334
+ } catch (e) {
1335
+ console.warn('[MultiFileAuthState] Could not deserialize cookies:', e.message);
1336
+ }
1337
+ }
1338
+
1339
+ if (appState) {
1340
+ if (appState.language) igState.language = appState.language;
1341
+ if (appState.timezoneOffset) igState.timezoneOffset = appState.timezoneOffset;
1342
+ if (appState.connectionTypeHeader) igState.connectionTypeHeader = appState.connectionTypeHeader;
1343
+ if (appState.capabilitiesHeader) igState.capabilitiesHeader = appState.capabilitiesHeader;
1344
+ if (appState.checkpoint) igState.checkpoint = appState.checkpoint;
1345
+ if (appState.challenge) igState.challenge = appState.challenge;
1346
+ }
1347
+ }
1348
+
1349
+ // Main exported helper that wires MultiFileAuthState to your clients
1350
+ async function useMultiFileAuthState(folder) {
1351
+ const authState = new MultiFileAuthState(folder);
1352
+ await authState.loadAll();
1353
+
1354
+ // Helper: persist additional derived creds fields when saving creds
1355
+ const enrichAndSaveCreds = async (igClientState) => {
1356
+ const { creds, device, cookies, appState } = await extractStateData(igClientState);
1357
+ // merge with existing creds non-destructive
1358
+ authState.data.creds = Object.assign({}, authState.data.creds || {}, creds);
1359
+ authState.data.device = Object.assign({}, authState.data.device || {}, device);
1360
+ authState.data.cookies = cookies || authState.data.cookies;
1361
+ authState.data.appState = Object.assign({}, authState.data.appState || {}, appState);
1362
+ await authState.saveCreds();
1363
+ await authState.saveAppState();
1364
+ };
1365
+
1366
+ const saveCreds = async (igClient) => {
1367
+ if (!igClient || !igClient.state) {
1368
+ console.warn('[useMultiFileAuthState] No igClient provided to saveCreds');
1369
+ return;
1370
+ }
1371
+
1372
+ // Use the enriched saver
1373
+ await enrichAndSaveCreds(igClient.state);
1374
+
1375
+ // Also attempt to extract some cookie-derived fields for convenience
1376
+ try {
1377
+ const cookieFields = await extractCookieFields(igClient.state);
1378
+ if (cookieFields.sessionIdCookie) {
1379
+ if (!authState.data.creds) authState.data.creds = {};
1380
+ authState.data.creds.sessionId = cookieFields.sessionIdCookie;
1381
+ }
1382
+ if (cookieFields.dsUserIdCookie) {
1383
+ if (!authState.data.creds) authState.data.creds = {};
1384
+ authState.data.creds.dsUserId = cookieFields.dsUserIdCookie;
1385
+ }
1386
+ authState._debouncedSave(['creds']);
1387
+ } catch (e) {}
1388
+
1389
+ console.log('[useMultiFileAuthState] Credentials saved to', folder);
1390
+ };
1391
+
1392
+ const saveMqttSession = async (realtimeClient) => {
1393
+ if (!realtimeClient) {
1394
+ console.warn('[useMultiFileAuthState] No realtimeClient provided');
1395
+ return;
1396
+ }
1397
+
1398
+ // base mqtt session
1399
+ const mqttSession = {
1400
+ sessionId: null,
1401
+ mqttSessionId: null,
1402
+ lastConnected: new Date().toISOString(),
1403
+ userId: null
1404
+ };
1405
+
1406
+ try {
1407
+ if (realtimeClient.ig && realtimeClient.ig.state) {
1408
+ mqttSession.userId = realtimeClient.ig.state.cookieUserId || realtimeClient.ig.state.userId || null;
1409
+ // attempt to extract sessionId from JWT helper if available
1410
+ mqttSession.sessionId = (typeof realtimeClient.extractSessionIdFromJWT === 'function') ? realtimeClient.extractSessionIdFromJWT() : null;
1411
+ }
1412
+ if (realtimeClient.connection) {
1413
+ mqttSession.mqttSessionId = realtimeClient.connection?.clientInfo?.clientMqttSessionId?.toString() || null;
1414
+ }
1415
+ } catch (e) {
1416
+ // ignore
1417
+ }
1418
+
1419
+ // subscriptions
1420
+ const subscriptions = {
1421
+ graphQlSubs: realtimeClient.initOptions?.graphQlSubs || [],
1422
+ skywalkerSubs: realtimeClient.initOptions?.skywalkerSubs || [],
1423
+ subscribedAt: new Date().toISOString()
1424
+ };
1425
+
1426
+ // seq ids (iris)
1427
+ const seqIds = {};
1428
+ if (realtimeClient.initOptions?.irisData) {
1429
+ seqIds.seq_id = realtimeClient.initOptions.irisData.seq_id || null;
1430
+ seqIds.snapshot_at_ms = realtimeClient.initOptions.irisData.snapshot_at_ms || null;
1431
+ }
1432
+
1433
+ // --- Extended MQTT / realtime info (new) ---
1434
+ // irisState: things like subscription_id / device id / other iris metadata
1435
+ const irisState = {};
1436
+ try {
1437
+ // try to glean from various possible places on realtimeClient
1438
+ irisState.subscription_id = realtimeClient.initOptions?.irisData?.subscription_id || realtimeClient.iris?.subscriptionId || realtimeClient.irisState?.subscription_id || null;
1439
+ irisState.user_id = realtimeClient.ig?.state?.cookieUserId || realtimeClient.ig?.state?.userId || irisState.user_id || null;
1440
+ irisState.device_id = realtimeClient.connection?.clientInfo?.deviceId || realtimeClient.initOptions?.deviceId || null;
1441
+ irisState.created_at = new Date().toISOString();
1442
+ } catch (e) {}
1443
+
1444
+ // mqttTopics: topics observed/subscribed
1445
+ const mqttTopics = {};
1446
+ try {
1447
+ mqttTopics.topics = realtimeClient.connection?.subscribedTopics || realtimeClient._subscribedTopics || realtimeClient.subscribedTopics || [];
1448
+ mqttTopics.updatedAt = new Date().toISOString();
1449
+ } catch (e) {
1450
+ mqttTopics.topics = [];
1451
+ mqttTopics.updatedAt = new Date().toISOString();
1452
+ }
1453
+
1454
+ // mqttCapabilities: features supported / protocol version
1455
+ const mqttCapabilities = {};
1456
+ try {
1457
+ mqttCapabilities.supportsTyping = Boolean(realtimeClient.initOptions?.supportsTyping ?? realtimeClient.capabilities?.supportsTyping);
1458
+ mqttCapabilities.supportsReactions = Boolean(realtimeClient.initOptions?.supportsReactions ?? realtimeClient.capabilities?.supportsReactions);
1459
+ mqttCapabilities.supportsVoice = Boolean(realtimeClient.initOptions?.supportsVoice ?? realtimeClient.capabilities?.supportsVoice);
1460
+ mqttCapabilities.protocolVersion = realtimeClient.connection?.protocolVersion || realtimeClient.initOptions?.protocolVersion || null;
1461
+ mqttCapabilities.collectedAt = new Date().toISOString();
1462
+ } catch (e) {}
1463
+
1464
+ // mqttAuth: if realtimeClient keeps an auth token / jwt for mqtt, store (but be careful: short lived)
1465
+ const mqttAuth = {};
1466
+ try {
1467
+ mqttAuth.jwt = realtimeClient.connection?.authToken || realtimeClient.mqttAuth?.jwt || realtimeClient.initOptions?.mqttJwt || null;
1468
+ mqttAuth.expiresAt = realtimeClient.connection?.authExpiresAt || realtimeClient.mqttAuth?.expiresAt || null;
1469
+ mqttAuth.collectedAt = new Date().toISOString();
1470
+ } catch (e) {}
1471
+
1472
+ // lastPublishIds: tracking last message ids for reliable publish/acks
1473
+ const lastPublishIds = {};
1474
+ try {
1475
+ lastPublishIds.lastMessageId = realtimeClient._lastMessageId || realtimeClient.lastMessageId || null;
1476
+ lastPublishIds.lastAckedAt = realtimeClient._lastAckedAt || null;
1477
+ lastPublishIds.collectedAt = new Date().toISOString();
1478
+ } catch (e) {}
1479
+
1480
+ // Assign all into authState and persist
1481
+ authState.data.mqttSession = mqttSession;
1482
+ authState.data.subscriptions = subscriptions;
1483
+ authState.data.seqIds = seqIds;
1484
+
1485
+ authState.data.irisState = irisState;
1486
+ authState.data.mqttTopics = mqttTopics;
1487
+ authState.data.mqttCapabilities = mqttCapabilities;
1488
+ authState.data.mqttAuth = mqttAuth;
1489
+ authState.data.lastPublishIds = lastPublishIds;
1490
+
1491
+ // Increment stats
1492
+ authState.incrementStat('mqttMessages');
1493
+
1494
+ await authState.saveMqttState();
1495
+ console.log('[useMultiFileAuthState] MQTT session saved to', folder);
1496
+ };
1497
+
1498
+ const loadCreds = async (igClient) => {
1499
+ if (!igClient || !igClient.state) {
1500
+ console.warn('[useMultiFileAuthState] No igClient provided to loadCreds');
1501
+ return false;
1502
+ }
1503
+
1504
+ if (!authState.hasValidSession()) {
1505
+ console.log('[useMultiFileAuthState] No valid session found');
1506
+ return false;
1507
+ }
1508
+
1509
+ await applyStateData(igClient.state, authState);
1510
+ console.log('[useMultiFileAuthState] Credentials loaded from', folder);
1511
+ return true;
1512
+ };
1513
+
1514
+ const getMqttConnectOptions = () => {
1515
+ if (!authState.hasMqttSession()) {
1516
+ // still return a warm default to help warm-restore
1517
+ const defaultTopics = ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'];
1518
+ return {
1519
+ graphQlSubs: defaultTopics,
1520
+ skywalkerSubs: ['presence_subscribe', 'typing_subscribe'],
1521
+ irisData: { seq_id: authState.getSeqIds()?.seq_id || 0, snapshot_at_ms: authState.getSeqIds()?.snapshot_at_ms || Date.now() },
1522
+ mqttAuthToken: authState.getMqttAuth()?.jwt || null,
1523
+ mqttAuthExpiresAt: authState.getMqttAuth()?.expiresAt || null
1524
+ };
1525
+ }
1526
+
1527
+ const subs = authState.getSubscriptions() || {};
1528
+ const seqIds = authState.getSeqIds() || {};
1529
+ const mqttAuth = authState.getMqttAuth() || null;
1530
+
1531
+ return {
1532
+ graphQlSubs: subs.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
1533
+ skywalkerSubs: subs.skywalkerSubs || ['presence_subscribe', 'typing_subscribe'],
1534
+ irisData: seqIds.seq_id ? {
1535
+ seq_id: seqIds.seq_id,
1536
+ snapshot_at_ms: seqIds.snapshot_at_ms
1537
+ } : { seq_id: 0, snapshot_at_ms: Date.now() },
1538
+ mqttAuthToken: mqttAuth?.jwt || null,
1539
+ mqttAuthExpiresAt: mqttAuth?.expiresAt || null
1540
+ };
1541
+ };
1542
+
1543
+ const clearSession = async () => {
1544
+ await authState.clearAll();
1545
+ console.log('[useMultiFileAuthState] Session cleared');
1546
+ };
1547
+
1548
+ const isSessionValid = async (igClient) => {
1549
+ if (!authState.hasValidSession()) return false;
1550
+ if (!igClient) return true;
1551
+
1552
+ try {
1553
+ // quick check by calling account/currentUser or equivalent
1554
+ if (igClient.account && typeof igClient.account.currentUser === 'function') {
1555
+ await igClient.account.currentUser();
1556
+ return true;
1557
+ }
1558
+ // fallback assume valid if we have creds - up to caller to verify
1559
+ return true;
1560
+ } catch (e) {
1561
+ console.warn('[useMultiFileAuthState] Session validation failed:', e.message);
1562
+ // emit session-expired if credentials appear invalid
1563
+ authState.emit('session-expired', { reason: e.message });
1564
+ return false;
1565
+ }
1566
+ };
1567
+
1568
+ // --- Auto-refresh / revalidation helpers ---
1569
+ const _autoRefreshCheck = async () => {
1570
+ try {
1571
+ if (!authState.data.creds) return;
1572
+ // If creds indicate expiry timestamp, check it
1573
+ const expiresAt = authState.data.creds.sessionExpiresAt ? new Date(authState.data.creds.sessionExpiresAt).getTime() : null;
1574
+ if (expiresAt && Date.now() > expiresAt) {
1575
+ authState.data.creds.isExpired = true;
1576
+ authState._debouncedSave(['creds']);
1577
+ authState.emit('session-expired', { reason: 'sessionExpiresAt' });
1578
+ } else {
1579
+ // if igClient provided, optionally validate remote
1580
+ if (authState._autoRefreshClient) {
1581
+ try {
1582
+ const valid = await isSessionValid(authState._autoRefreshClient);
1583
+ if (!valid) {
1584
+ authState.emit('session-expired', { reason: 'remote-check' });
1585
+ } else {
1586
+ // update lastValidatedAt
1587
+ if (!authState.data.creds) authState.data.creds = {};
1588
+ authState.data.creds.lastValidatedAt = new Date().toISOString();
1589
+ authState._debouncedSave(['creds']);
1590
+ }
1591
+ } catch (e) {}
1592
+ }
1593
+ }
1594
+ } catch (e) {
1595
+ // ignore
1596
+ }
1597
+ };
1598
+
1599
+ const enableAutoRefresh = (igClient, intervalMs = 5 * 60 * 1000) => {
1600
+ // igClient optional: if provided, will be used to do remote validation
1601
+ try {
1602
+ disableAutoRefresh();
1603
+ authState._autoRefreshClient = igClient || null;
1604
+ authState._autoRefreshIntervalMs = intervalMs;
1605
+ authState._autoRefreshTimer = setInterval(_autoRefreshCheck, intervalMs);
1606
+ // run immediately once
1607
+ _autoRefreshCheck();
1608
+ authState.emit('auto-refresh-enabled', { intervalMs });
1609
+ } catch (e) {
1610
+ console.warn('[useMultiFileAuthState] enableAutoRefresh error:', e.message);
1611
+ }
1612
+ };
1613
+
1614
+ const disableAutoRefresh = () => {
1615
+ try {
1616
+ if (authState._autoRefreshTimer) {
1617
+ clearInterval(authState._autoRefreshTimer);
1618
+ authState._autoRefreshTimer = null;
1619
+ authState._autoRefreshClient = null;
1620
+ authState.emit('auto-refresh-disabled');
1621
+ }
1622
+ } catch (e) {}
1623
+ };
1624
+
1625
+ // --- Login history / audit helpers ---
1626
+ const recordLoginAttempt = async ({ success, ip = null, userAgent = null, method = 'password', reason = null }) => {
1627
+ const entry = authState.recordLoginAttempt({ success, ip, userAgent, method, reason });
1628
+ // return entry for caller
1629
+ return entry;
1630
+ };
1631
+
1632
+ // --- Account restriction helpers ---
1633
+ const markAccountRestricted = ({ status = 'checkpoint', reason = null }) => {
1634
+ authState.markAccountRestricted({ status, reason });
1635
+ };
1636
+
1637
+ const clearAccountRestriction = () => {
1638
+ authState.clearAccountRestriction();
1639
+ };
1640
+
1641
+ // --- Usage stats helpers ---
1642
+ const incrementStat = (statName, by = 1) => {
1643
+ authState.incrementStat(statName, by);
1644
+ };
1645
+
1646
+ // --- Backup / restore helpers exposed ---
1647
+ const restoreLatestBackup = async (key) => {
1648
+ return await authState.restoreLatestBackup(key);
1649
+ };
1650
+
1651
+ // --- Device fingerprinting ---
1652
+ const lockDeviceFingerprint = async () => {
1653
+ return await authState.lockDeviceFingerprint();
1654
+ };
1655
+
1656
+ const verifyDeviceFingerprint = (deviceObj) => {
1657
+ return authState.verifyDeviceFingerprint(deviceObj);
1658
+ };
1659
+
1660
+ // --- Accounts manager ---
1661
+ const registerAccount = async (name, folderPath) => {
1662
+ if (!authState.data.accountsIndex) authState.data.accountsIndex = {};
1663
+ authState.data.accountsIndex[name] = folderPath;
1664
+ await authState._writeFile('accountsIndex', authState.data.accountsIndex);
1665
+ authState.emit('account-registered', { name, folderPath });
1666
+ return authState.data.accountsIndex;
1667
+ };
1668
+
1669
+ const listAccounts = () => {
1670
+ return authState.getAccountsIndex();
1671
+ };
1672
+
1673
+ // --- Health check ---
1674
+ const getHealth = () => {
1675
+ return authState.getHealth();
1676
+ };
1677
+
1678
+ // --- MQTT helpers exposing internal features ---
1679
+ const attachRealtimeClient = (realtimeClient, opts = {}) => {
1680
+ return authState.attachRealtimeClient(realtimeClient, opts);
1681
+ };
1682
+
1683
+ const enqueueOutbox = (item) => authState.enqueueOutbox(item);
1684
+ const flushOutbox = (client, max) => authState.flushOutbox(client, max);
1685
+ const checkSubscriptions = (client) => authState.checkSubscriptions(client);
1686
+ const computeRisk = () => authState._computeRiskScore();
1687
+ const warmRestore = (client) => authState.warmRestoreClient(client);
1688
+ const getMqttHealth = () => authState.getMqttHealth();
1689
+ const getMqttTraffic = () => authState.getMqttTraffic();
1690
+ const getMqttRisk = () => authState.getMqttRisk();
1691
+ const isDuplicateMessage = (id) => authState.isDuplicateMessage(id);
1692
+ const markPing = () => authState.markPing();
1693
+ const markPong = () => authState.markPong();
1694
+
1695
+ // --- Misc helpers: loginHistory & usageStats getters ---
1696
+ const getLoginHistory = () => authState.getLoginHistory();
1697
+ const getUsageStats = () => authState.getUsageStats();
1698
+
1699
+ return {
1700
+ state: authState,
1701
+
1702
+ saveCreds,
1703
+ loadCreds,
1704
+
1705
+ saveMqttSession,
1706
+ getMqttConnectOptions,
1707
+
1708
+ clearSession,
1709
+ isSessionValid,
1710
+
1711
+ hasSession: () => authState.hasValidSession(),
1712
+ hasMqttSession: () => authState.hasMqttSession(),
1713
+
1714
+ folder,
1715
+
1716
+ // pass-through for convenience
1717
+ getData: () => authState.data,
1718
+
1719
+ // convenience setters/getters for the new data
1720
+ setIrisState: (s) => authState.setIrisState(s),
1721
+ setMqttTopics: (t) => authState.setMqttTopics(t),
1722
+ setMqttCapabilities: (c) => authState.setMqttCapabilities(c),
1723
+ setMqttAuth: (a) => authState.setMqttAuth(a),
1724
+ setLastPublishIds: (p) => authState.setLastPublishIds(p),
1725
+ getIrisState: () => authState.getIrisState(),
1726
+ getMqttTopics: () => authState.getMqttTopics(),
1727
+ getMqttCapabilities: () => authState.getMqttCapabilities(),
1728
+ getMqttAuth: () => authState.getMqttAuth(),
1729
+ getLastPublishIds: () => authState.getLastPublishIds(),
1730
+
1731
+ // new utilities
1732
+ enableAutoRefresh,
1733
+ disableAutoRefresh,
1734
+ recordLoginAttempt,
1735
+ getLoginHistory,
1736
+ markAccountRestricted,
1737
+ clearAccountRestriction,
1738
+ incrementStat,
1739
+ getUsageStats,
1740
+ restoreLatestBackup,
1741
+ lockDeviceFingerprint,
1742
+ verifyDeviceFingerprint,
1743
+ registerAccount,
1744
+ listAccounts,
1745
+ getHealth,
1746
+
1747
+ // mqtt helpers
1748
+ attachRealtimeClient,
1749
+ enqueueOutbox,
1750
+ flushOutbox,
1751
+ checkSubscriptions,
1752
+ computeRisk,
1753
+ warmRestore,
1754
+ getMqttHealth,
1755
+ getMqttTraffic,
1756
+ getMqttRisk,
1757
+ isDuplicateMessage,
1758
+ markPing,
1759
+ markPong
1760
+ };
1761
+ }
1762
+
1763
+ module.exports = {
1764
+ useMultiFileAuthState,
1765
+ MultiFileAuthState,
1766
+ extractStateData,
1767
+ applyStateData
1768
+ };