hridoy-ultimate 9.0.2 → 9.0.4

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.
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * logoutFromFacebook - REAL Facebook logout.
5
+ *
6
+ * This POSTs facebook.com/logout.php with the session cookies. Facebook then
7
+ * invalidates that session everywhere it is used, so the same account is also
8
+ * logged out in your normal browser. It is NOT called by api.logout() unless you
9
+ * opt in with `allowFacebookLogout: true` in fca-config.json. Call it directly
10
+ * only when you really want to end the Facebook session.
11
+ */
12
+
13
+ const log = require("../../../func/logAdapter");
14
+ const { parseAndCheckLogin } = require("../../utils/client");
15
+ const { getFrom } = require("../../utils/constants");
16
+ const { saveCookies } = require("../../utils/client");
17
+ module.exports = function(defaultFuncs, api, ctx) {
18
+ return function logoutFromFacebook(callback) {
19
+ let resolveFunc = function() {};
20
+ let rejectFunc = function() {};
21
+ const returnPromise = new Promise(function(resolve, reject) {
22
+ resolveFunc = resolve;
23
+ rejectFunc = reject;
24
+ });
25
+
26
+ if (!callback) {
27
+ callback = function(err, friendList) {
28
+ if (err) {
29
+ return rejectFunc(err);
30
+ }
31
+ resolveFunc(friendList);
32
+ };
33
+ }
34
+
35
+ const form = {
36
+ pmid: "0"
37
+ };
38
+
39
+ defaultFuncs
40
+ .post(
41
+ "https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
42
+ ctx.jar,
43
+ form
44
+ )
45
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
46
+ .then(function(resData) {
47
+ const elem = resData.jsmods.instances[0][2][0].filter(function(v) {
48
+ return v.value === "logout";
49
+ })[0];
50
+
51
+ const html = resData.jsmods.markup.filter(function(v) {
52
+ return v[0] === elem.markup.__m;
53
+ })[0][1].__html;
54
+
55
+ const form = {
56
+ fb_dtsg: getFrom(html, '"fb_dtsg" value="', '"'),
57
+ ref: getFrom(html, '"ref" value="', '"'),
58
+ h: getFrom(html, '"h" value="', '"')
59
+ };
60
+
61
+ return defaultFuncs
62
+ .post("https://www.facebook.com/logout.php", ctx.jar, form)
63
+ .then(saveCookies(ctx.jar));
64
+ })
65
+ .then(function(res) {
66
+ if (!res.headers) {
67
+ throw { error: "An error occurred when logging out." };
68
+ }
69
+
70
+ return defaultFuncs
71
+ .get(res.headers.location, ctx.jar)
72
+ .then(saveCookies(ctx.jar));
73
+ })
74
+ .then(function() {
75
+ ctx.loggedIn = false;
76
+ log.info("logoutFromFacebook", "Logged out of Facebook (this also ends the browser session).");
77
+ callback();
78
+ })
79
+ .catch(function(err) {
80
+ log.error("logoutFromFacebook", err);
81
+ return callback(err);
82
+ });
83
+
84
+ return returnPromise;
85
+ };
86
+ };
@@ -12,7 +12,7 @@ module.exports = function (defaultFuncs, api, ctx) {
12
12
  }
13
13
  if (!obj) obj = {};
14
14
  if (getType(obj) !== "Object") {
15
- throw new CustomError("The first parameter must be an object or a callback function");
15
+ throw new Error("The first parameter must be an object or a callback function");
16
16
  }
17
17
  let resolveFunc, rejectFunc;
18
18
  const returnPromise = new Promise((resolve, reject) => {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { generateOfflineThreadingID } = require("../../utils/format");
4
4
  const log = require("../../../func/logAdapter");
5
+ const { parseAndCheckLogin } = require("../../utils/client");
5
6
 
6
7
  module.exports = function (defaultFuncs, api, ctx) {
7
8
  function handleUpload(image) {
@@ -119,7 +119,10 @@ module.exports = function (defaultFuncs, api, ctx) {
119
119
  form.signatureID = utils.getGUID().replace(/-/g, "").slice(0, 8);
120
120
  const doSend = () => sendContent(form, threadID, isSingleUser, messageAndOTID, callback);
121
121
  if (!ctx.globalOptions || ctx.globalOptions.antiBan !== false) {
122
- globalAntiSuspension.prepareBeforeMessage(threadID, form.body || "").then(doSend).catch(doSend);
122
+ globalAntiSuspension.prepareBeforeMessage(threadID, form.body || "").then(doSend).catch(err => {
123
+ if (typeof callback === "function") return callback(err);
124
+ return undefined;
125
+ });
123
126
  } else {
124
127
  doSend();
125
128
  }
@@ -16,9 +16,25 @@ module.exports = function createListenMqtt(deps) {
16
16
 
17
17
  return function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
18
18
 
19
+ // Reconnects used to be a fixed 2 s forever: a flapping or refused socket meant a new
20
+ // WebSocket handshake to edge-chat every 2 seconds. Now exponential with jitter, capped
21
+ // at 5 min, reset as soon as the broker delivers /t_ms again, with a hard budget.
22
+ const MQTT_MAX_FAILS = 12;
23
+ const MQTT_MAX_DELAY_MS = 5 * 60 * 1000;
19
24
  function scheduleReconnect(delayMs) {
20
25
  const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || DEFAULT_RECONNECT_DELAY_MS;
21
- const ms = typeof delayMs === "number" ? delayMs : d;
26
+ const base = typeof delayMs === "number" ? delayMs : d;
27
+ const streak = ctx._mqttFailStreak || 0;
28
+ if (!ctx._reconnectTimer && !ctx._ending) {
29
+ if (streak >= MQTT_MAX_FAILS) {
30
+ ctx._mqttFailStreak = 0;
31
+ logger(`mqtt reconnect budget used up (${MQTT_MAX_FAILS} attempts without a working connection) - stopping; host app decides`, "error");
32
+ try { globalCallback({ type: "stop_listen", error: "MQTT reconnect budget exhausted" }, null); } catch (_) { }
33
+ return;
34
+ }
35
+ ctx._mqttFailStreak = streak + 1;
36
+ }
37
+ const ms = Math.round(Math.min(base * Math.pow(2, streak), MQTT_MAX_DELAY_MS) * (0.85 + Math.random() * 0.3));
22
38
  if (ctx._reconnectTimer) {
23
39
  logger("mqtt reconnect already scheduled", "warn");
24
40
  return; // debounce
@@ -180,6 +196,7 @@ module.exports = function createListenMqtt(deps) {
180
196
  ctx._rTimeout = rTimeout;
181
197
 
182
198
  ctx.tmsWait = function () {
199
+ ctx._mqttFailStreak = 0; // broker is delivering again: connection is healthy
183
200
  if (rTimeout) {
184
201
  clearTimeout(rTimeout);
185
202
  rTimeout = null;
@@ -254,9 +254,12 @@ module.exports = function createGetSeqID(deps) {
254
254
 
255
255
  throw { error: "Not logged in", originalResponse: resData };
256
256
  }
257
- if (!Array.isArray(resData) || !resData.length) return;
257
+ if (!Array.isArray(resData) || !resData.length) throw new Error("getSeqID: empty response");
258
258
  const lastRes = resData[resData.length - 1];
259
- if (lastRes && lastRes.successful_results === 0) return;
259
+ // Used to `return` silently here: no listener was started and nothing retried, so the
260
+ // bot sat "running" but deaf until the 5-minute heartbeat noticed. Now it is a
261
+ // retryable error (bounded back-off in listenMqtt's getSeqIDWrapper).
262
+ if (lastRes && lastRes.successful_results === 0) throw new Error("getSeqID: Facebook returned no successful results");
260
263
 
261
264
  const syncSeqId = resData[0]?.o0?.data?.viewer?.message_threads?.sync_sequence_id;
262
265
  if (syncSeqId) {
@@ -266,12 +269,20 @@ module.exports = function createGetSeqID(deps) {
266
269
  ctx.lastSeqId = ctx.lastSeqId || 0;
267
270
  logger("getSeqID: sync_sequence_id not found, fallback lastSeqId=" + ctx.lastSeqId, "warn");
268
271
  }
272
+ ctx.loggedIn = true; // emitAuth() sets this false; a working session must set it back
269
273
  listenMqtt(defaultFuncs, api, ctx, globalCallback);
270
274
  })
271
275
  .catch(async err => {
272
276
  const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
273
277
  const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
274
278
 
279
+ // A human has to dismiss Facebook's automated-behavior warning. Not a dead session and
280
+ // not retryable: stop cleanly and tell the host app with its own reason.
281
+ if (/automation_warning|automated-behavior warning/i.test(msg) || (err && err.error === "automation_warning")) {
282
+ logger(`getSeqID: ${msg} - NOT retrying`, "warn");
283
+ return emitAuth(ctx, api, globalCallback, "automation_warning", msg);
284
+ }
285
+
275
286
  // Definitive server-side session invalidation: error 1357001
276
287
  // ("Log in to continue") or an explicit login block. The session is
277
288
  // dead, not desynced — retrying within seconds (and especially
@@ -96,6 +96,18 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
96
96
 
97
97
  let conf = mqttConf(ctx, opts);
98
98
 
99
+ // Consecutive getSeqID failures. Used to retry every fixed 2 s forever, which is a request
100
+ // storm against Facebook on any persistent error (and exactly what "automation" detection
101
+ // looks for). Now: exponential back-off with jitter, capped, and a hard budget after which
102
+ // the host app is told ({type:"stop_listen"}) instead of looping.
103
+ let seqFailStreak = 0;
104
+ const SEQ_MAX_FAILS = 8;
105
+ const SEQ_MAX_DELAY_MS = 5 * 60 * 1000;
106
+ function seqRetryDelay(streak) {
107
+ const raw = Math.min(conf.reconnectDelayMs * Math.pow(2, streak - 1), SEQ_MAX_DELAY_MS);
108
+ return Math.round(raw * (0.85 + Math.random() * 0.3));
109
+ }
110
+
99
111
  function getSeqIDWrapper() {
100
112
  if (ctx._ending && !ctx._cycling) {
101
113
  logger("mqtt getSeqID skipped - ending", "warn");
@@ -118,6 +130,7 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
118
130
  .then(() => {
119
131
  logger("mqtt getSeqID done", "info");
120
132
  ctx._cycling = false;
133
+ seqFailStreak = 0;
121
134
  })
122
135
  .catch(e => {
123
136
  ctx._cycling = false;
@@ -127,8 +140,15 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
127
140
  if (ctx._ending) return;
128
141
  // Retry after delay if autoReconnect is enabled
129
142
  if (ctx.globalOptions.autoReconnect) {
130
- const d = conf.reconnectDelayMs;
131
- logger(`mqtt getSeqID will retry in ${d}ms`, "warn");
143
+ seqFailStreak++;
144
+ if (seqFailStreak > SEQ_MAX_FAILS) {
145
+ logger(`mqtt getSeqID failed ${SEQ_MAX_FAILS} times in a row - giving up (host app decides what happens next)`, "error");
146
+ seqFailStreak = 0;
147
+ try { globalCallback({ type: "stop_listen", error: errMsg }, null); } catch (_) { }
148
+ return;
149
+ }
150
+ const d = seqRetryDelay(seqFailStreak);
151
+ logger(`mqtt getSeqID will retry in ${d}ms (failure ${seqFailStreak}/${SEQ_MAX_FAILS})`, "warn");
132
152
  setTimeout(() => {
133
153
  if (!ctx._ending) getSeqIDWrapper();
134
154
  }, d);
@@ -280,9 +300,8 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
280
300
 
281
301
  function startSessionKeeper() {
282
302
  if (ctx._sessionKeeperInterval) return;
283
- // Self-rescheduling timeout with ±15 min jitter around 90 min, instead of
284
- // a clockwork-exact 90:00.000 interval — perfectly periodic traffic is
285
- // trivial to fingerprint.
303
+ // Self-rescheduling timeout with ±1 h jitter around 6 h, instead of a
304
+ // clockwork-exact interval — perfectly periodic traffic is trivial to fingerprint.
286
305
  const BASE_MS = 6 * 60 * 60 * 1000;
287
306
  const JITTER_MS = 60 * 60 * 1000;
288
307
  const scheduleNext = function () {
@@ -480,13 +499,3 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
480
499
  };
481
500
  };
482
501
 
483
-
484
- // mqtt stability patch
485
- try {
486
- if (typeof client !== "undefined" && client) {
487
- client.on("offline", () => console.log("[FCA] MQTT offline"));
488
- client.on("reconnect", () => console.log("[FCA] MQTT reconnecting"));
489
- client.on("close", () => console.log("[FCA] MQTT closed"));
490
- client.on("error", (e) => console.log("[FCA] MQTT error:", e?.message || e));
491
- }
492
- } catch (e) {}
@@ -254,7 +254,10 @@ module.exports = function (defaultFuncs, api, ctx) {
254
254
  try {
255
255
  await globalAntiSuspension.prepareBeforeMessage(threadID, msg.body || "");
256
256
  } catch (err) {
257
- logger("AntiSuspension prepareBeforeMessage error: " + (err && err.message ? err.message : err), "warn");
257
+ logger.error("sendMessage", "Outbound send blocked by safety guard: " + (err && err.message ? err.message : err));
258
+ if (callback) callback(err);
259
+ else reject(err);
260
+ return promise;
258
261
  }
259
262
  }
260
263
 
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const log = require("../../../func/logAdapter");
4
+ const { parseAndCheckLogin } = require("../../utils/client");
4
5
  const { formatID } = require("../../utils/format");
5
6
  function formatData(data) {
6
7
  return {
@@ -88,6 +88,7 @@ function createFallbackNamespaces(api) {
88
88
  getCurrentUserID: bindOptionalMethod(api, "getCurrentUserID"),
89
89
  handleFriendRequest: bindOptionalMethod(api, "handleFriendRequest"),
90
90
  logout: bindOptionalMethod(api, "logout"),
91
+ logoutFromFacebook: bindOptionalMethod(api, "logoutFromFacebook"),
91
92
  refreshDtsg: bindOptionalMethod(api, "refreshFb_dtsg"),
92
93
  changeBlockedStatus: bindOptionalMethod(api, "changeBlockedStatus"),
93
94
  setOptions: bindOptionalMethod(api, "setOptions"),
@@ -6,17 +6,21 @@ const log = {
6
6
  };
7
7
 
8
8
  // Phrases Facebook responses use when it thinks this account is being
9
- // operated by a bot / script rather than a human. If any of these show up
10
- // in a response body or an error message, we treat it as a signal that
11
- // current traffic looks automated and should slow down / back off.
12
- const SUSPICION_PHRASES = [
9
+ // operated by a bot / script rather than a human, or when it is restricting it.
10
+ //
11
+ // HARD signals mean the account itself is flagged / locked / needs a human
12
+ // (checkpoint, automated-behavior warning, disabled, identity check). On these
13
+ // the guard FAILS CLOSED: outbound sending stops until the process is restarted
14
+ // after a manual review of the account.
15
+ //
16
+ // SOFT signals are rate-limit style responses ("too many requests", "rate
17
+ // limited", "temporarily blocked" ...). They are usually transient, so they only
18
+ // count as strikes and trigger the slow-down cooldown. Treating them as hard
19
+ // signals made one passing 429-style error silence the bot for good.
20
+ const HARD_SUSPICION_PHRASES = [
13
21
  "checkpoint",
14
22
  "automated behavior",
15
23
  "automated_behavior",
16
- "unusual activity",
17
- "unusual_activity",
18
- "suspicious activity",
19
- "action_required",
20
24
  "account_locked",
21
25
  "account locked",
22
26
  "account has been disabled",
@@ -26,6 +30,17 @@ const SUSPICION_PHRASES = [
26
30
  "bot detected",
27
31
  "bot_detected",
28
32
  "not a human",
33
+ "verify your account",
34
+ "confirm your identity",
35
+ "confirm it's you",
36
+ "security check required"
37
+ ];
38
+
39
+ const SOFT_SUSPICION_PHRASES = [
40
+ "unusual activity",
41
+ "unusual_activity",
42
+ "suspicious activity",
43
+ "action_required",
29
44
  "spam detected",
30
45
  "spam_detected",
31
46
  "looks like spam",
@@ -35,15 +50,14 @@ const SUSPICION_PHRASES = [
35
50
  "too_many_requests",
36
51
  "temporarily blocked",
37
52
  "feature temporarily unavailable",
38
- "verify your account",
39
- "confirm your identity",
40
- "confirm it's you",
41
- "security check required",
42
53
  "policy violation",
43
54
  "action blocked",
44
55
  "you're blocked from"
45
56
  ];
46
57
 
58
+ // Kept for backwards compatibility with anything that imported the old list.
59
+ const SUSPICION_PHRASES = HARD_SUSPICION_PHRASES.concat(SOFT_SUSPICION_PHRASES);
60
+
47
61
  /**
48
62
  * Tracks send volume and paces outbound traffic so it doesn't look like a
49
63
  * script firing messages as fast as the network allows. Three layers:
@@ -72,13 +86,18 @@ class SendPacer {
72
86
  this.dayWindowStart = Date.now();
73
87
  this.daySendCount = 0;
74
88
 
89
+ this.accountSafetyBlocked = false;
90
+ this.accountSafetyReason = null;
91
+
75
92
  this.cooldown = {
76
93
  active: false,
77
94
  since: null,
78
95
  durationMs: 45 * 60 * 1000,
79
96
  strikes: 0,
80
- strikesToActivate: 2
97
+ strikesToActivate: 2,
98
+ strikeWindowMs: 30 * 60 * 1000
81
99
  };
100
+ this.softStrikeTimes = [];
82
101
 
83
102
  this._gcTimer = setInterval(() => this._rollWindows(), 60 * 1000);
84
103
  if (this._gcTimer.unref) this._gcTimer.unref();
@@ -108,12 +127,30 @@ class SendPacer {
108
127
  return this._inWarmup() ? this.warmupHourlyCap : this.normalHourlyCap;
109
128
  }
110
129
 
111
- /** Records that a suspicious signal was seen; may activate the cooldown. */
112
- noteSuspicionSignal(source) {
113
- this.cooldown.strikes++;
130
+ /**
131
+ * Records that a suspicious signal was seen.
132
+ * hard=true (default): fail closed - stop sending until a human reviews the account.
133
+ * hard=false: count a strike; two strikes inside strikeWindowMs start the cooldown.
134
+ */
135
+ noteSuspicionSignal(source, hard) {
136
+ const isHard = hard !== false;
137
+ if (isHard) {
138
+ // Fail closed: once Facebook signals checkpoint/automation/disabled status,
139
+ // do not continue sending or retrying in this process. Human review is required.
140
+ if (!this.accountSafetyBlocked) {
141
+ log.warn("AntiSuspension", `Outbound sending paused: ${source || "Facebook security signal"}. Review the account manually, then restart the bot.`);
142
+ }
143
+ this.accountSafetyBlocked = true;
144
+ this.accountSafetyReason = String(source || "Facebook security signal");
145
+ return;
146
+ }
147
+ const now = Date.now();
148
+ this.softStrikeTimes = this.softStrikeTimes.filter(t => now - t < this.cooldown.strikeWindowMs);
149
+ this.softStrikeTimes.push(now);
150
+ this.cooldown.strikes = this.softStrikeTimes.length;
114
151
  if (!this.cooldown.active && this.cooldown.strikes >= this.cooldown.strikesToActivate) {
115
152
  this.cooldown.active = true;
116
- this.cooldown.since = Date.now();
153
+ this.cooldown.since = now;
117
154
  log.warn("AntiSuspension", `Cooldown activated after repeated suspicion signals (last: ${source || "unknown"}). Backing off for ${(this.cooldown.durationMs / 60000).toFixed(0)} min.`);
118
155
  }
119
156
  }
@@ -122,9 +159,17 @@ class SendPacer {
122
159
  scanForSuspicion(text) {
123
160
  if (!text || typeof text !== "string") return false;
124
161
  const lower = text.toLowerCase();
125
- const hit = SUSPICION_PHRASES.find(p => lower.includes(p));
126
- if (hit) this.noteSuspicionSignal(hit);
127
- return !!hit;
162
+ const hardHit = HARD_SUSPICION_PHRASES.find(p => lower.includes(p));
163
+ if (hardHit) {
164
+ this.noteSuspicionSignal(hardHit, true);
165
+ return true;
166
+ }
167
+ const softHit = SOFT_SUSPICION_PHRASES.find(p => lower.includes(p));
168
+ if (softHit) {
169
+ this.noteSuspicionSignal(softHit, false);
170
+ return true;
171
+ }
172
+ return false;
128
173
  }
129
174
 
130
175
  /** Inspects a thrown error's message for suspicion phrases. */
@@ -138,6 +183,7 @@ class SendPacer {
138
183
  if (Date.now() - this.cooldown.since >= this.cooldown.durationMs) {
139
184
  this.cooldown.active = false;
140
185
  this.cooldown.strikes = 0;
186
+ this.softStrikeTimes = [];
141
187
  this.cooldown.since = null;
142
188
  return false;
143
189
  }
@@ -151,6 +197,11 @@ class SendPacer {
151
197
  * rather than blocking a real message.
152
198
  */
153
199
  async waitForTurn(threadID) {
200
+ if (this.accountSafetyBlocked) {
201
+ const err = new Error(`Outbound messaging paused after Facebook security signal: ${this.accountSafetyReason}. Review the account manually before restarting.`);
202
+ err.code = "ACCOUNT_SAFETY_PAUSED";
203
+ throw err;
204
+ }
154
205
  try {
155
206
  this._rollWindows();
156
207
 
@@ -185,8 +236,10 @@ class SendPacer {
185
236
  this.daySendCount++;
186
237
  const threadCount = (this.threadSendCounts.get(key) || 0) + 1;
187
238
  this.threadSendCounts.set(key, threadCount);
188
- } catch {
189
- // Pacing must never be the reason a real message fails to send.
239
+ } catch (err) {
240
+ // Safety-related errors must not be swallowed.
241
+ if (err && err.code === "ACCOUNT_SAFETY_PAUSED") throw err;
242
+ log.warn("AntiSuspension", `Pacing check failed: ${err && err.message ? err.message : String(err)}`);
190
243
  }
191
244
  }
192
245
 
@@ -201,7 +254,9 @@ class SendPacer {
201
254
  hourlyCap: this._hourlyCap(),
202
255
  daySendCount: this.daySendCount,
203
256
  dailyCap: this.dailyCap,
204
- coolingDown: this.isCoolingDown()
257
+ coolingDown: this.isCoolingDown(),
258
+ safetyBlocked: this.accountSafetyBlocked,
259
+ safetyReason: this.accountSafetyReason
205
260
  };
206
261
  }
207
262
  }
@@ -210,10 +265,15 @@ const pacer = new SendPacer();
210
265
 
211
266
  module.exports = {
212
267
  SendPacer,
268
+ HARD_SUSPICION_PHRASES,
269
+ SOFT_SUSPICION_PHRASES,
270
+ SUSPICION_PHRASES,
213
271
  globalAntiSuspension: {
214
272
  prepareBeforeMessage: (threadID) => pacer.waitForTurn(threadID),
215
273
  detectSuspensionSignal: (text) => pacer.scanForSuspicion(text),
216
274
  checkAccountHealth: (err) => pacer.noteSendError(err),
217
- getStats: () => pacer.stats()
275
+ getStats: () => pacer.stats(),
276
+ isAccountSafetyBlocked: () => pacer.accountSafetyBlocked,
277
+ getAccountSafetyReason: () => pacer.accountSafetyReason
218
278
  }
219
279
  };
@@ -2,6 +2,28 @@
2
2
 
3
3
  // Cookie helpers extracted from client.js
4
4
 
5
+ const AUTH_COOKIES = new Set(["c_user", "xs", "i_user", "datr", "sb", "fr"]);
6
+
7
+ // True for a Set-Cookie header that deletes one of the login cookies
8
+ // (empty / "deleted" value, Max-Age<=0, or an expiry in the past).
9
+ function isAuthCookieDeletion(setCookieHeader) {
10
+ const first = String(setCookieHeader || "").split(";")[0];
11
+ const eq = first.indexOf("=");
12
+ if (eq <= 0) return false;
13
+ const name = first.slice(0, eq).trim();
14
+ if (!AUTH_COOKIES.has(name)) return false;
15
+ const value = first.slice(eq + 1).trim().replace(/^"|"$/g, "");
16
+ if (value === "" || /^deleted$/i.test(value)) return true;
17
+ const maxAge = /;\s*max-age\s*=\s*(-?\d+)/i.exec(setCookieHeader);
18
+ if (maxAge && parseInt(maxAge[1], 10) <= 0) return true;
19
+ const expires = /;\s*expires\s*=\s*([^;]+)/i.exec(setCookieHeader);
20
+ if (expires) {
21
+ const t = Date.parse(expires[1]);
22
+ if (!Number.isNaN(t) && t <= Date.now()) return true;
23
+ }
24
+ return false;
25
+ }
26
+
5
27
  function saveCookies(jar) {
6
28
  return res => {
7
29
  try {
@@ -29,6 +51,11 @@ function saveCookies(jar) {
29
51
  const mirrored = c.replace(/domain=\.?facebook\.com/i, "domain=.messenger.com");
30
52
  jar.setCookieSync(mirrored, "https://www.messenger.com");
31
53
  } else if (/domain=\.?messenger\.com/i.test(c)) {
54
+ // A messenger.com "delete this cookie" answer must never wipe the matching
55
+ // facebook.com login cookie in our jar: that would silently kill a perfectly
56
+ // good facebook.com session (the cookie values are mirrored, so messenger.com
57
+ // may well reject them while facebook.com is fine).
58
+ if (isAuthCookieDeletion(c)) continue;
32
59
  const mirrored = c.replace(/domain=\.?messenger\.com/i, "domain=.facebook.com");
33
60
  jar.setCookieSync(mirrored, "https://www.facebook.com");
34
61
  }
@@ -80,6 +107,7 @@ function getAppState(jar) {
80
107
 
81
108
  module.exports = {
82
109
  saveCookies,
83
- getAppState
110
+ getAppState,
111
+ isAuthCookieDeletion
84
112
  };
85
113