hridoy-ultimate 9.0.0 → 9.0.2

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 (46) hide show
  1. package/CHANGELOG.md +346 -69
  2. package/{docs/UPSTREAM_API_DOCS.md → DOCS.md} +3 -3
  3. package/README.md +536 -161
  4. package/func/checkUpdate.js +1 -0
  5. package/index.d.ts +13 -74
  6. package/index.js +0 -6
  7. package/module/config.js +2 -5
  8. package/module/login.js +1 -2
  9. package/module/loginHelper.js +9 -41
  10. package/module/options.js +6 -6
  11. package/package.json +44 -39
  12. package/src/api/action/logout.js +0 -19
  13. package/src/api/messaging/sendMessage.js +1 -1
  14. package/src/api/messaging/sessionGuard.js +8 -0
  15. package/src/api/messaging/setMessageReaction.js +2 -7
  16. package/src/api/socket/core/connectMqtt.js +72 -99
  17. package/src/api/socket/core/emitAuth.js +17 -9
  18. package/src/api/socket/e2ee/index.js +15 -33
  19. package/src/api/socket/e2ee/native/NOTICE.md +2 -2
  20. package/src/api/socket/e2ee/native/lib/index.mjs +1 -2
  21. package/src/api/socket/e2ee/native/nativeMediaBridge.js +4 -4
  22. package/src/api/socket/listenE2EE.js +1 -3
  23. package/src/api/socket/listenMqtt.js +60 -67
  24. package/src/utils/antiSuspension.js +3 -3
  25. package/src/utils/fingerprint.js +35 -16
  26. package/src/utils/headers.js +1 -1
  27. package/src/utils/versionCheck.js +47 -0
  28. package/LICENSE +0 -23
  29. package/MERGE_REPORT.md +0 -54
  30. package/THIRD_PARTY_NOTICES.md +0 -36
  31. package/docs/TEST_RESULTS.md +0 -68
  32. package/scripts/run-tests.js +0 -19
  33. package/scripts/syntax-check.js +0 -22
  34. package/src/api/socket/core/lifecycle.js +0 -117
  35. package/src/api/socket/e2ee/proto/proto-writer.ts +0 -76
  36. package/src/utils/typingIndicator.js +0 -202
  37. package/test/helpers/stub-modules.js +0 -122
  38. package/test/integration/logout.test.js +0 -69
  39. package/test/integration/mqtt-lifecycle.test.js +0 -195
  40. package/test/integration/package.test.js +0 -116
  41. package/test/integration/typing-wiring.test.js +0 -71
  42. package/test/unit/fingerprint.test.js +0 -19
  43. package/test/unit/humanizer.test.js +0 -32
  44. package/test/unit/lifecycle.test.js +0 -92
  45. package/test/unit/parsers.test.js +0 -43
  46. package/test/unit/typing.test.js +0 -136
@@ -16,7 +16,6 @@ const createListenMqtt = require("./core/connectMqtt");
16
16
  const createGetSeqID = require("./core/getSeqID");
17
17
  const getTaskResponseData = require("./core/getTaskResponseData");
18
18
  const createEmitAuth = require("./core/emitAuth");
19
- const lifecycle = require("./core/lifecycle");
20
19
  const createMiddlewareSystem = require("./middleware");
21
20
  const { createCommandHumanizer, createHumanizerMiddleware } = require("../../utils/humanizer");
22
21
 
@@ -27,6 +26,7 @@ const { createCommandHumanizer, createHumanizerMiddleware } = require("../../uti
27
26
  // session-touching requests. Reconnects still happen on actual socket
28
27
  // close/error. Pass { cycleMs: <ms> } to listenMqtt to re-enable.
29
28
  const CYCLE_MS_DEFAULT = 0;
29
+ const RECONNECT_DELAY_MS_DEFAULT = 2000;
30
30
  const UNSUB_ALL_TIMEOUT_MS = 5000;
31
31
 
32
32
  const parseDelta = createParseDelta({ parseAndCheckLogin });
@@ -34,11 +34,7 @@ const emitAuth = createEmitAuth({ logger });
34
34
  const listenMqtt = createListenMqtt({ WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy, topics, parseDelta, getTaskResponseData, logger, emitAuth });
35
35
  const getSeqIDFactory = createGetSeqID({ parseAndCheckLogin, listenMqtt, logger, emitAuth });
36
36
 
37
- const MQTT_DEFAULTS = Object.assign({
38
- cycleMs: CYCLE_MS_DEFAULT,
39
- autoReconnect: true,
40
- reconnectAfterStop: false
41
- }, lifecycle.DEFAULTS); // reconnectDelayMs, maxReconnectDelayMs, reconnectJitter, maxReconnectAttempts
37
+ const MQTT_DEFAULTS = { cycleMs: CYCLE_MS_DEFAULT, reconnectDelayMs: RECONNECT_DELAY_MS_DEFAULT, autoReconnect: true, reconnectAfterStop: false };
42
38
  function mqttConf(ctx, overrides) {
43
39
  ctx._mqttOpt = Object.assign({}, MQTT_DEFAULTS, ctx._mqttOpt || {}, overrides || {});
44
40
  if (typeof ctx._mqttOpt.autoReconnect === "boolean") ctx.globalOptions.autoReconnect = ctx._mqttOpt.autoReconnect;
@@ -59,10 +55,8 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
59
55
  ctx._humanizer = createCommandHumanizer(ctx.globalOptions.humanize);
60
56
  }
61
57
  if (!ctx._humanizerMiddlewareInstalled) {
62
- // OPT-IN in hridoy-ultimate: the humanizer delays replies and silently drops
63
- // commands inside its cooldown window, so it must never be an invisible default.
64
- const hz = ctx.globalOptions.humanize;
65
- const humanizeEnabled = hz === true || !!(hz && typeof hz === "object" && hz.enabled === true);
58
+ const humanizeEnabled = ctx.globalOptions.humanize === undefined ||
59
+ (ctx.globalOptions.humanize && ctx.globalOptions.humanize.enabled !== false);
66
60
  if (humanizeEnabled) {
67
61
  try {
68
62
  middleware.use("humanizer", createHumanizerMiddleware(ctx._humanizer, logger, api));
@@ -75,18 +69,6 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
75
69
  }
76
70
  api.getHumanizer = () => ctx._humanizer || null;
77
71
 
78
- // Typing indicator middleware: shared by MQTT and E2EE events (listenE2EE uses the same
79
- // ctx._middleware). When the humanizer is enabled it already shows typing during its own
80
- // delay, so ours stays out of its way.
81
- if (ctx._typing && !ctx._typingMiddlewareInstalled && !ctx._humanizerMiddlewareInstalled) {
82
- try {
83
- middleware.use("typing", ctx._typing.middleware());
84
- ctx._typingMiddlewareInstalled = true;
85
- } catch (e) {
86
- logger(`Typing middleware install failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
87
- }
88
- }
89
-
90
72
  function installPostGuard() {
91
73
  if (ctx._postGuarded) return defaultFuncs.post;
92
74
  const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
@@ -115,8 +97,8 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
115
97
  let conf = mqttConf(ctx, opts);
116
98
 
117
99
  function getSeqIDWrapper() {
118
- if (ctx._stopped || (ctx._ending && !ctx._cycling)) {
119
- logger("mqtt getSeqID skipped - stopped/ending", "warn");
100
+ if (ctx._ending && !ctx._cycling) {
101
+ logger("mqtt getSeqID skipped - ending", "warn");
120
102
  return Promise.resolve();
121
103
  }
122
104
  const form = {
@@ -142,18 +124,14 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
142
124
  const errMsg = e && e.message ? e.message : String(e || "Unknown error");
143
125
  logger(`mqtt getSeqID error: ${errMsg}`, "error");
144
126
  // Don't reconnect if we're ending
145
- if (ctx._ending || ctx._stopped) return;
146
- // Retry with bounded exponential backoff if autoReconnect is enabled.
147
- // The timer is tracked on ctx so stopListening()/logout() can cancel it.
127
+ if (ctx._ending) return;
128
+ // Retry after delay if autoReconnect is enabled
148
129
  if (ctx.globalOptions.autoReconnect) {
149
- const res = lifecycle.scheduleReconnect(ctx, logger, getSeqIDWrapper, "getSeqID failed");
150
- if (res === "exhausted") {
151
- ctx._stopped = true;
152
- lifecycle.clearListenerTimers(ctx, false);
153
- const msg = `mqtt getSeqID retries exhausted (${errMsg})`;
154
- logger(msg, "error");
155
- globalCallback({ type: "stop_listen", error: msg }, null);
156
- }
130
+ const d = conf.reconnectDelayMs;
131
+ logger(`mqtt getSeqID will retry in ${d}ms`, "warn");
132
+ setTimeout(() => {
133
+ if (!ctx._ending) getSeqIDWrapper();
134
+ }, d);
157
135
  }
158
136
  });
159
137
  }
@@ -203,7 +181,11 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
203
181
 
204
182
  function endQuietly(next) {
205
183
  const finish = () => {
206
- lifecycle.detachClient(ctx.mqttClient);
184
+ try {
185
+ if (ctx.mqttClient) {
186
+ ctx.mqttClient.removeAllListeners();
187
+ }
188
+ } catch (_) { }
207
189
  ctx.mqttClient = undefined;
208
190
  ctx.lastSeqId = null;
209
191
  ctx.syncToken = undefined;
@@ -262,12 +244,9 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
262
244
  }
263
245
 
264
246
  function delayedReconnect() {
265
- const res = lifecycle.scheduleReconnect(ctx, logger, refreshSessionAndReconnect, "cycle/restart");
266
- if (res === "exhausted") {
267
- ctx._stopped = true;
268
- lifecycle.clearListenerTimers(ctx, false);
269
- globalCallback({ type: "stop_listen", error: "mqtt reconnect attempts exhausted" }, null);
270
- }
247
+ const d = conf.reconnectDelayMs;
248
+ logger(`mqtt reconnect in ${d}ms`, "info");
249
+ setTimeout(() => refreshSessionAndReconnect(), d);
271
250
  }
272
251
 
273
252
  function refreshSessionAndReconnect() {
@@ -284,7 +263,7 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
284
263
  logger(`Session refresh before reconnect failed (non-fatal): ${err && err.message ? err.message : String(err)}`, "warn");
285
264
  })
286
265
  .finally(() => {
287
- if (!ctx._stopped) getSeqIDWrapper();
266
+ getSeqIDWrapper();
288
267
  });
289
268
  }
290
269
 
@@ -304,10 +283,9 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
304
283
  // Self-rescheduling timeout with ±15 min jitter around 90 min, instead of
305
284
  // a clockwork-exact 90:00.000 interval — perfectly periodic traffic is
306
285
  // trivial to fingerprint.
307
- const BASE_MS = 90 * 60 * 1000;
308
- const JITTER_MS = 15 * 60 * 1000;
286
+ const BASE_MS = 6 * 60 * 60 * 1000;
287
+ const JITTER_MS = 60 * 60 * 1000;
309
288
  const scheduleNext = function () {
310
- if (ctx._stopped) { ctx._sessionKeeperInterval = null; return; } // stopped: do not re-arm
311
289
  const delay = BASE_MS - JITTER_MS + Math.floor(Math.random() * 2 * JITTER_MS);
312
290
  ctx._sessionKeeperInterval = setTimeout(function () {
313
291
  if (!ctx.loggedIn) { scheduleNext(); return; }
@@ -338,7 +316,7 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
338
316
  // chat message arrive". The old check force-cycled a perfectly healthy
339
317
  // connection every 45 quiet minutes (e.g. overnight), producing
340
318
  // re-auth bursts exactly when a real user would be asleep.
341
- if (!isConnected() && !ctx._ending && !ctx._cycling && !ctx._stopped && !ctx._reconnectTimer) {
319
+ if (!isConnected() && !ctx._ending && !ctx._cycling) {
342
320
  logger("heartbeat: MQTT socket not connected, forcing reconnect", "warn");
343
321
  ctx.lastMqttActivity = Date.now();
344
322
  forceCycle();
@@ -347,32 +325,40 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
347
325
  if (ctx._heartbeatInterval.unref) ctx._heartbeatInterval.unref();
348
326
  }
349
327
 
350
- // listen(callback[, options]) - options (all optional): cycleMs, autoReconnect, reconnectAfterStop,
351
- // reconnectDelayMs, maxReconnectDelayMs, reconnectJitter, maxReconnectAttempts (0 = unlimited),
352
- // tmsWaitTimeoutMs, connectTimeoutMs.
353
- return function (callback, options) {
328
+ return function (callback) {
354
329
  class MessageEmitter extends EventEmitter {
355
330
  stopListening(callback2) {
356
331
  const cb = callback2 || function () { };
357
332
  logger("mqtt stop requested", "info");
358
333
  globalCallback = identity;
359
334
 
360
- // Mark stopped FIRST so any in-flight reconnect/retry timer or
361
- // getSeqID promise sees it, then cancel every timer we own.
362
- ctx._stopped = true;
363
- ctx._reconnectAttempt = 0;
364
- lifecycle.clearListenerTimers(ctx, false);
365
- if (ctx._typing && typeof ctx._typing.stopAll === "function") ctx._typing.stopAll();
335
+ if (ctx._autoCycleTimer) {
336
+ clearInterval(ctx._autoCycleTimer);
337
+ ctx._autoCycleTimer = null;
338
+ logger("mqtt auto-cycle cleared", "info");
339
+ }
340
+
341
+ if (ctx._sessionKeeperInterval) {
342
+ clearInterval(ctx._sessionKeeperInterval);
343
+ ctx._sessionKeeperInterval = null;
344
+ }
345
+
346
+ if (ctx._heartbeatInterval) {
347
+ clearInterval(ctx._heartbeatInterval);
348
+ ctx._heartbeatInterval = null;
349
+ }
350
+
351
+ if (ctx._reconnectTimer) {
352
+ clearTimeout(ctx._reconnectTimer);
353
+ ctx._reconnectTimer = null;
354
+ }
366
355
 
367
356
  ctx._ending = true;
368
357
  unsubAll(() => endQuietly(() => {
369
358
  logger("mqtt stopped", "info");
370
359
  cb();
371
360
  conf = mqttConf(ctx, conf);
372
- if (conf.reconnectAfterStop) {
373
- ctx._stopped = false; // explicit opt-in restart
374
- delayedReconnect();
375
- }
361
+ if (conf.reconnectAfterStop) delayedReconnect();
376
362
  }));
377
363
  }
378
364
  async stopListeningAsync() {
@@ -397,10 +383,7 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
397
383
  globalCallback = originalCallback;
398
384
  }
399
385
 
400
- conf = mqttConf(ctx, options && typeof options === "object" ? options : conf);
401
- ctx._stopped = false;
402
- ctx._reconnectAttempt = 0;
403
- lifecycle.clearReconnect(ctx);
386
+ conf = mqttConf(ctx, conf);
404
387
 
405
388
  installPostGuard();
406
389
 
@@ -425,8 +408,8 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
425
408
  listenMqtt(defaultFuncs, api, ctx, globalCallback);
426
409
  }
427
410
 
428
- api.stopListening = msgEmitter.stopListening.bind(msgEmitter);
429
- api.stopListeningAsync = msgEmitter.stopListeningAsync.bind(msgEmitter);
411
+ api.stopListening = msgEmitter.stopListening;
412
+ api.stopListeningAsync = msgEmitter.stopListeningAsync;
430
413
 
431
414
  // Store original callback for re-wrapping when middleware is added/removed
432
415
  let currentOriginalCallback = originalCallback;
@@ -497,3 +480,13 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
497
480
  };
498
481
  };
499
482
 
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) {}
@@ -63,9 +63,9 @@ class SendPacer {
63
63
 
64
64
  this.startedAt = Date.now();
65
65
  this.warmupWindowMs = 15 * 60 * 1000;
66
- this.warmupHourlyCap = 40;
67
- this.normalHourlyCap = 350;
68
- this.dailyCap = 2500;
66
+ this.warmupHourlyCap = 20;
67
+ this.normalHourlyCap = 120;
68
+ this.dailyCap = 800;
69
69
 
70
70
  this.hourWindowStart = Date.now();
71
71
  this.hourSendCount = 0;
@@ -6,24 +6,20 @@
6
6
  // claiming the exact same browser build is a trivially detectable pattern.
7
7
  // Keep this list current; entries that are far behind Chrome's real release
8
8
  // timeline are themselves a signal.
9
- // Builds taken from Google's published Chrome stable release notes (149.0.7827.102 on
10
- // 2026-06, 151.0.7922.75 on 2026-08). Chrome's release cadence is about one major per
11
- // 4 weeks (two weeks from Chrome 153 on), so refresh this list every few months.
12
9
  const CHROME_VERSIONS = [
13
- "149.0.7827.102",
14
- "151.0.7922.75"
10
+ "131.0.6778.140",
11
+ "132.0.6834.111",
12
+ "133.0.6943.98",
13
+ "134.0.6998.117",
14
+ "135.0.7049.85"
15
15
  ];
16
16
 
17
17
  function pickChromeVersion() {
18
18
  return CHROME_VERSIONS[Math.floor(Math.random() * CHROME_VERSIONS.length)];
19
19
  }
20
20
 
21
- // Real Chrome sends a *reduced* User-Agent ("Chrome/<major>.0.0.0"); the full build only
22
- // appears in Sec-CH-UA-Full-Version-List. A UA carrying the full build number is itself
23
- // an anomaly, so the UA is reduced here and the full build is kept in the fingerprint.
24
21
  function buildUserAgent(version) {
25
- const major = String(version).split(".")[0];
26
- return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`;
22
+ return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`;
27
23
  }
28
24
 
29
25
  // Chrome's Sec-Ch-Ua "GREASE" brand slot rotates through a small set of
@@ -39,7 +35,31 @@ function greaseForVersion(major) {
39
35
  // to different hardcoded defaults) is itself inconsistent and detectable.
40
36
  let cachedFingerprint = null;
41
37
 
38
+ // The fingerprint is also saved to disk. Picking a new random Chrome build
39
+ // on every restart made the same account look like a new browser each time
40
+ // the bot restarted, which is a common reason for "session dropped" logouts.
41
+ // Best option of all: pass your real browser's User-Agent (the one the
42
+ // cookies were exported from) via the userAgent option; this file is only
43
+ // the fallback.
44
+ const fs = require("fs");
45
+ const path = require("path");
46
+ function fingerprintFile() {
47
+ return path.join(process.cwd(), "data", "session", "fca-fingerprint.json");
48
+ }
49
+
42
50
  function getFingerprint() {
51
+ if (!cachedFingerprint) {
52
+ try {
53
+ const saved = JSON.parse(fs.readFileSync(fingerprintFile(), "utf8"));
54
+ if (saved && CHROME_VERSIONS.includes(saved.version)) {
55
+ cachedFingerprint = {
56
+ userAgent: buildUserAgent(saved.version),
57
+ version: saved.version,
58
+ major: parseInt(saved.version.split(".")[0], 10)
59
+ };
60
+ }
61
+ } catch (_) { /* first run */ }
62
+ }
43
63
  if (!cachedFingerprint) {
44
64
  const version = pickChromeVersion();
45
65
  const major = parseInt(version.split(".")[0], 10);
@@ -48,6 +68,10 @@ function getFingerprint() {
48
68
  version,
49
69
  major
50
70
  };
71
+ try {
72
+ fs.mkdirSync(path.dirname(fingerprintFile()), { recursive: true });
73
+ fs.writeFileSync(fingerprintFile(), JSON.stringify({ version }), "utf8");
74
+ } catch (_) { /* read-only fs: fall back to per-process */ }
51
75
  }
52
76
  return cachedFingerprint;
53
77
  }
@@ -64,12 +88,7 @@ function clientHintsFromUA(ua) {
64
88
  const s = String(ua || getDefaultUserAgent());
65
89
  const chromeMatch = s.match(/Chrome\/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
66
90
  const major = chromeMatch ? chromeMatch[1] : String(getFingerprint().major);
67
- let full = chromeMatch ? chromeMatch[0].split("/")[1] : getFingerprint().version;
68
- // Reduced UA ("151.0.0.0"): report the real full build for our own fingerprint major.
69
- if (chromeMatch && chromeMatch[2] === "0" && chromeMatch[3] === "0" && chromeMatch[4] === "0" &&
70
- String(getFingerprint().major) === major) {
71
- full = getFingerprint().version;
72
- }
91
+ const full = chromeMatch ? chromeMatch[0].split("/")[1] : getFingerprint().version;
73
92
  const isMobile = /Mobile|Android|iPhone|iPad/i.test(s);
74
93
 
75
94
  let platform = "Windows";
@@ -45,7 +45,7 @@ function getHeaders(url, options, ctx, customHeader) {
45
45
  const referer = options?.referer || "https://www.facebook.com/";
46
46
  const origin = referer.replace(/\/+$/, "");
47
47
  const contentType = options?.contentType || "application/x-www-form-urlencoded";
48
- const acceptLang = options?.acceptLanguage || "en-US,en;q=0.9,vi;q=0.8";
48
+ const acceptLang = options?.acceptLanguage || "en-US,en;q=0.9";
49
49
  // NOTE: keep the && chain parenthesized — `||` binds looser than `&&`.
50
50
  const isNavigation = (u.pathname === "/" &&
51
51
  !url.includes("ajax") &&
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ const https = require("https");
4
+ const pkg = require("../../package.json");
5
+
6
+ function getLatestVersion(cb) {
7
+ const req = https.get(
8
+ "https://registry.npmjs.org/hridoy-ultimate/latest",
9
+ { headers: { "Accept": "application/json" } },
10
+ (res) => {
11
+ let data = "";
12
+ res.on("data", chunk => { data += chunk; });
13
+ res.on("end", () => {
14
+ try { cb(null, JSON.parse(data).version); }
15
+ catch { cb(new Error("parse failed")); }
16
+ });
17
+ }
18
+ );
19
+ req.on("error", cb);
20
+ req.setTimeout(5000, () => { req.destroy(); cb(new Error("timeout")); });
21
+ }
22
+
23
+ function compareVersion(a, b) {
24
+ const pa = a.split(".").map(Number);
25
+ const pb = b.split(".").map(Number);
26
+ for (let i = 0; i < 3; i++) {
27
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
28
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ function checkForUpdate(logger) {
34
+ const current = pkg.version;
35
+ getLatestVersion((err, latest) => {
36
+ if (err || !latest) return;
37
+ if (compareVersion(current, latest) < 0) {
38
+ if (typeof logger === "function") {
39
+ logger(`⚠️ hridoy-ultimate outdated! You are using v${current}, latest is v${latest}. Run: npm install hridoy-ultimate@latest`, "warn");
40
+ } else {
41
+ console.warn(`\x1b[33m[FCA-WARN] ⚠️ hridoy-ultimate outdated! v${current} → v${latest} | npm install hridoy-ultimate@latest\x1b[0m`);
42
+ }
43
+ }
44
+ });
45
+ }
46
+
47
+ module.exports = { checkForUpdate };
package/LICENSE DELETED
@@ -1,23 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) EryXenX and contributors (eryxenx-fca 1.0.0 - 1.1.9)
4
- Copyright (c) DongDev and contributors (original fca-unofficial lineage)
5
- Copyright (c) 2026 HR ID OY (hridoy-ultimate modifications)
6
-
7
- Permission is hereby granted, free of charge, to any person obtaining a copy
8
- of this software and associated documentation files (the "Software"), to deal
9
- in the Software without restriction, including without limitation the rights
10
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- copies of the Software, and to permit persons to whom the Software is
12
- furnished to do so, subject to the following conditions:
13
-
14
- The above copyright notice and this permission notice shall be included in all
15
- copies or substantial portions of the Software.
16
-
17
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- SOFTWARE.
package/MERGE_REPORT.md DELETED
@@ -1,54 +0,0 @@
1
- # Merge report - hridoy-ultimate 9.0.0
2
-
3
- ## Inputs
4
- - 83 published packages, analysed statically (extract, per-version diff, `node --check`, reading
5
- the core paths): `@eryxenx/fca` 1.0.0-1.1.9 (18), `fca-eryxenx` 1.0.0-29.0.0 (20),
6
- 3.0.2 + 30.0.0-39.1.8 (20), 4.0.x + 39.1.9-39.9.0 (20), 6.0.0-9.0.3 (5).
7
- - `fca.zip` = hridoy-ultimate 8.0.5, used as the base tree (already a hardened merge of
8
- `@eryxenx/fca` 1.1.9 with `loginHelper.js` from 1.1.8, with a 36-test suite).
9
- - `messagix.so` / `messagix.dll` supplied by the maintainer.
10
-
11
- Nothing was run against Facebook. Not every one of the ~14,000 extracted files was read line by
12
- line; the 12,144 `.js/.cjs/.mjs` files contain only 352 distinct contents.
13
-
14
- ## What the best builds contributed, and where it stands in 9.0.0
15
- Verified by reading the 8.0.5 code before touching it.
16
-
17
- | Improvement | Best source | Status |
18
- |---|---|---|
19
- | Stable MQTT (backoff, SUBACK, 8 s / 12 s timeouts, stale-client guard) | `fca-eryxenx` 20-23 | already in base (own implementation, tested) |
20
- | Native client reuse + `koffi.disposable` string free (leak fixes) | 39.2.0 / 39.2.1 | already in base |
21
- | 15 s `withTimeout` on native calls | 39.1.8 | already in base |
22
- | `getThreadInfo` E2EE branch | 39.1.6 | already in base |
23
- | E2EE group routing in react / unsend / edit | 39.2.2 | already in base (`isE2EEGroup`) |
24
- | Session keeper (cookie + `fb_dtsg` refresh) | 39.2.7 | already in base |
25
- | `Accept` header navigation vs XHR | 39.2.5 | already in base |
26
- | Attachment id fallback in `sendMessage` | 7.0.0 | already in base |
27
- | Native E2EE engine opt-in (not auto-connected) | 39.2.8 | already in base |
28
- | Logout stops MQTT / E2EE / timers | none (all builds lacked it) | already in base |
29
- | Typing indicator | none (only the opt-in humanizer had one) | **new in 9.0.0** |
30
- | Bundled `messagix` binaries | maintainer upload | **restored in 9.0.0, opt-in** |
31
- | Reduced UA + refreshed Chrome pool | fingerprint.js from 1.1.7, rebuilt | **new in 9.0.0** |
32
- | `setMessageReaction` thread `"0"` fallback | present in every build | **fixed in 9.0.0** |
33
- | `index.d.ts` coverage | 23 of 66 methods missing in 1.1.9 | **fixed in 9.0.0** |
34
-
35
- ## Rejected on purpose
36
- - `autoLogin: true` default and the `minhdong.site` credential host (every `fca-eryxenx` build and
37
- `@eryxenx/fca` 1.0.0-1.0.5). Credentials and the 2FA secret would go to a third party.
38
- - Default-on humanizer (silently drops commands inside its cooldown), default-on native engine
39
- (kicks the vendored E2EE socket), bogus `versionCheck` (1.1.9), inert auto-update.
40
- - 39.3.0-39.9.0: branch cut from the 39.1.2-39.1.4 tree, loses the 39.2.x fixes, adds
41
- `[E2EE-DEBUG]` logging of full reaction payloads. 39.1.0-39.1.4 / 39.3.0+ also ship a duplicate
42
- 17.5 MB native copy.
43
- - `fca-eryxenx` 9.0.0 (syntax error in `core/channel.js:178`), 3.0.2-6.0.0 (`editMessage` requires a
44
- non-existent `../utils`), 4.0.4's dead "stability patch", the stray `index.js.nexca`, nested
45
- `e2ee/e2ee/` copies.
46
-
47
- ## Known issues kept (documented, not hidden)
48
- - `module/login.js` still installs process-wide `unhandledRejection` / `uncaughtException`
49
- handlers that log and continue; they keep bots alive but can hide a fatal state.
50
- - `src/api/messaging/sendMessage.js` is loaded and then overridden by `src/api/socket/sendMessage.js`
51
- (`loginHelper.js`); kept for backward compatibility.
52
- - Legacy HTTP paths still use the deprecated `request` library (`src/utils/nexca-utils.js`).
53
- - Native binaries: x86-64 only, stripped, license wording conflicts (see `THIRD_PARTY_NOTICES.md`).
54
- - Live Facebook behaviour (login, MQTT, send, react, E2EE, typing) is NOT verified here.
@@ -1,36 +0,0 @@
1
- # Third-party notices
2
-
3
- hridoy-ultimate is MIT licensed (see `LICENSE`). It contains or builds on the following.
4
-
5
- ## Code lineage (MIT)
6
- - `eryxenx-fca` / `@eryxenx/fca` and its contributors (builds 1.x - 39.x were analysed and merged).
7
- - DongDev and the original `fca-unofficial` lineage (<https://github.com/dongp06>).
8
- - Optional dependency `fca-unofficial` from `github:VangBanLaNhat/fca-unofficial`.
9
-
10
- ## Bundled native binaries (opt-in, `FCA_E2EE_NATIVE=1`)
11
- Location: `src/api/socket/e2ee/native/build/`
12
-
13
- | File | Size (bytes) | Type | SHA-256 |
14
- |---|---|---|---|
15
- | `messagix.so` | 17,504,872 | ELF 64-bit x86-64, stripped | `6249f50cc23de0b11a710266a2323c306d44cf46530880e58e0cd1cf4b5bf8d9` |
16
- | `messagix.dll` | 15,802,368 | PE32+ DLL x86-64, stripped | `f8597ba86c90c39cec19b660e75eba59f2022678eb20866b740cfa321036885e` |
17
-
18
- - Supplied by the maintainer. They are byte-identical to the copies found in every analysed
19
- `eryxenx` package (58 `.so` and 45 `.dll` copies, no differing hash). They are stripped, so
20
- they could **not** be audited here, and no source ships with them.
21
- - A test (`test/integration/package.test.js`) fails if either hash changes.
22
- - Platforms: x86-64 Linux and Windows only. macOS and ARM (e.g. Termux) need your own build via
23
- `FCA_E2EE_NATIVE_LIB=/path/to/messagix`.
24
- - **License question to resolve before you redistribute:** the analysed packages disagree about the
25
- upstream. `native/NOTICE.md` describes mautrix-go / mautrix-meta as MPL-2.0, while the
26
- `vendor/stfca-native/NOTICE.md` that shipped in some builds (39.1.0-39.1.4, 39.3.0-39.9.0) says the
27
- `messagix` part is AGPL-3.0. AGPL-3.0 carries network-use source-offer obligations. Check the
28
- upstream repository for the exact build you use. See also `src/api/socket/e2ee/native/NOTICE.md`.
29
-
30
- ## Vendored E2EE bundle
31
- `src/api/socket/e2ee/vendor/fme/dist/index.cjs` (bundled `fb-messenger-e2ee` client) has no license
32
- header or source in the analysed packages. Treat its terms as unknown.
33
-
34
- ## Runtime dependencies
35
- Declared in `package.json`; each keeps its own license (axios, mqtt, ws, sequelize, sqlite3,
36
- @signalapp/libsignal-client, protobufjs, koffi and others).
@@ -1,68 +0,0 @@
1
- # Test results (actual output)
2
-
3
- Environment: v22.22.2, Linux 6.18.44-fc-v114, no network, third-party modules stubbed (MOCKED where marked).
4
- Generated: 2026-10-13T20:03Z
5
-
6
- ## npm test (node --test, spec reporter)
7
- ```
8
- ✔ [mocked] logout stops MQTT, disconnects E2EE and clears every timer (50.186778ms)
9
- ✔ [mocked] logout works when no listener was ever started (14.258077ms)
10
- ✔ [mocked] a failed logout request is reported, not swallowed, and the session stays usable (10.037149ms)
11
- ✔ [mocked] sync queue is published only after every SUBACK (46.187501ms)
12
- ✔ [mocked] reconnect after socket close uses backoff, then recovers (17.548562ms)
13
- ✔ [mocked] stopListening during a cycle must not be revived by a pending retry (race regression) (16.384888ms)
14
- ✔ [mocked] stopListening clears timers, ends the client and is safe to call twice (21.34972ms)
15
- ✔ [mocked] late socket 'error' after stopListening does not throw (9.571458ms)
16
- ✔ [mocked] calling listen twice replaces the old client (no duplicate connection) (12.14378ms)
17
- ✔ [mocked] gives up after maxReconnectAttempts and reports stop_listen (no infinite loop) (9.182964ms)
18
- ✔ [mocked] a healthy first sync resets the backoff counter (16.256224ms)
19
- ✔ [mocked] humanizer middleware is NOT installed by default, and is when opted in (26.861027ms)
20
- ✔ [mocked] emitAuth stops everything and reports account_inactive (8.374336ms)
21
- ✔ [mocked] listen(callback, options) applies reconnect options per call (12.31406ms)
22
- ✔ package metadata: name, version, owner, entry points exist (1.413356ms)
23
- ✔ regression: every third-party module the source requires is declared (24.539389ms)
24
- ✔ vendored E2EE bundle: its external requires are declared too (2.38059ms)
25
- ✔ only the pinned messagix binaries ship, native stays opt-in, no default third-party credential host (125.254664ms)
26
- ✔ every JS file in the package parses (6235.728346ms)
27
- ✔ [mocked] package loads with consistent exports (23.934551ms)
28
- ✔ [mocked] login with empty options reports an error via callback and never throws (3129.536404ms)
29
- ✔ [mocked] typing middleware is installed by default and shows typing for a command without delaying delivery (44.078959ms)
30
- ✔ [mocked] plain chat does not trigger typing, and stopListening clears pending typing timers (16.606205ms)
31
- ✔ [mocked] typing middleware is NOT installed when the humanizer owns typing, or when no controller exists (24.276443ms)
32
- ✔ fingerprint: UA is the reduced Chrome/<major>.0.0.0 form and the full build only appears in client hints (2.989355ms)
33
- ✔ humanizer: first command accepted with a bounded delay, burst inside cooldown dropped (2.724519ms)
34
- ✔ humanizer: non-command messages pass untouched; per-thread isolation (0.56259ms)
35
- ✔ humanizer: enabled:false never delays or drops (1.317371ms)
36
- ✔ computeBackoff grows exponentially, is bounded and never negative (5.845535ms)
37
- ✔ computeBackoff jitter stays within +/- jitter and clamps to max (0.418652ms)
38
- ✔ computeBackoff tolerates garbage input (0.318463ms)
39
- ✔ scheduleReconnect: single timer, debounce, stop, exhaustion (1.587298ms)
40
- ✔ scheduleReconnect: a timer that fires after stop does nothing (race regression) (0.359697ms)
41
- ✔ maxReconnectAttempts: 0 means unlimited only when explicitly set (1.961505ms)
42
- ✔ clearListenerTimers is idempotent and clears every owned timer (0.47163ms)
43
- ✔ detachClient: a late 'error' after detach must not throw (0.605172ms)
44
- ✔ extractDtsg handles each supported markup shape (2.885756ms)
45
- ✔ extractDtsg: jazoest forms, empty and non-string input (1.591823ms)
46
- ✔ fingerprint: default UA and client hints are mutually consistent (1.64789ms)
47
- ✔ fingerprint: Android UA yields mobile Android hints (no desktop contradiction) (0.50116ms)
48
- ✔ typing: command-like message starts typing; plain chat / own messages / non-message events do not (1.739404ms)
49
- ✔ typing: GoatBot prefix and extra prefixes count as commands; onlyCommands:false types for any text (0.355195ms)
50
- ✔ typing: middleware calls next() synchronously (zero added latency) and then starts typing (1.268156ms)
51
- ✔ typing: sending a reply clears typing after the send resolves (16.890953ms)
52
- ✔ typing: safety timeout stops a typing indicator that never got a reply (1.652981ms)
53
- ✔ typing: repeated starts inside minGapMs do not re-publish; after the gap they do (0.464619ms)
54
- ✔ typing: disabled by option false, and by GoatBot enableTypingIndicator:false unless explicitly overridden (0.321804ms)
55
- ✔ typing: configure(false) clears active timers; stopAll is idempotent and publishes nothing (0.424433ms)
56
- ✔ typing: a throwing / rejecting sendTypingIndicator never breaks the bot (11.013855ms)
57
- ✔ typing: withTyping shows typing around fn and clears it even when fn throws (1.512949ms)
58
- ℹ tests 50
59
- ℹ suites 0
60
- ℹ pass 50
61
- ℹ fail 0
62
- ℹ cancelled 0
63
- ℹ skipped 0
64
- ℹ todo 0
65
- ```
66
-
67
- ## Not verified here
68
- Live Facebook login, MQTT traffic, sending, reactions, typing indicator on the wire, E2EE and the bundled native binaries (no network, no account).
@@ -1,19 +0,0 @@
1
- "use strict";
2
- // Portable test runner (node --test needs explicit files on Node 18/20).
3
- // Usage: node scripts/run-tests.js [unit|integration|all]
4
- const fs = require("fs");
5
- const path = require("path");
6
- const { spawnSync } = require("child_process");
7
-
8
- const which = process.argv[2] || "all";
9
- const base = path.join(__dirname, "..", "test");
10
- const dirs = which === "all" ? ["unit", "integration"] : [which];
11
- const files = [];
12
- for (const d of dirs) {
13
- const full = path.join(base, d);
14
- if (!fs.existsSync(full)) continue;
15
- for (const f of fs.readdirSync(full).sort()) if (f.endsWith(".test.js")) files.push(path.join(full, f));
16
- }
17
- if (!files.length) { console.error("no test files found"); process.exit(2); }
18
- const r = spawnSync(process.execPath, ["--test", ...files], { stdio: "inherit" });
19
- process.exit(r.status === null ? 1 : r.status);
@@ -1,22 +0,0 @@
1
- "use strict";
2
- // Dependency-free "lint": every .js/.cjs file in the package must parse.
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { spawnSync } = require("child_process");
6
- const root = path.join(__dirname, "..");
7
- const skip = new Set(["node_modules", "Fca_Database", ".git"]);
8
- let bad = 0, n = 0;
9
- (function walk(dir) {
10
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
11
- if (skip.has(e.name)) continue;
12
- const p = path.join(dir, e.name);
13
- if (e.isDirectory()) walk(p);
14
- else if (/\.(js|cjs)$/.test(e.name)) {
15
- n++;
16
- const r = spawnSync(process.execPath, ["--check", p], { encoding: "utf8" });
17
- if (r.status !== 0) { bad++; console.error("SYNTAX ERROR:", path.relative(root, p), "\n" + r.stderr); }
18
- }
19
- }
20
- })(root);
21
- console.log(`checked ${n} files, ${bad} with syntax errors`);
22
- process.exit(bad ? 1 : 0);