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.
- package/CHANGELOG.md +346 -69
- package/{docs/UPSTREAM_API_DOCS.md → DOCS.md} +3 -3
- package/README.md +536 -161
- package/func/checkUpdate.js +1 -0
- package/index.d.ts +13 -74
- package/index.js +0 -6
- package/module/config.js +2 -5
- package/module/login.js +1 -2
- package/module/loginHelper.js +9 -41
- package/module/options.js +6 -6
- package/package.json +44 -39
- package/src/api/action/logout.js +0 -19
- package/src/api/messaging/sendMessage.js +1 -1
- package/src/api/messaging/sessionGuard.js +8 -0
- package/src/api/messaging/setMessageReaction.js +2 -7
- package/src/api/socket/core/connectMqtt.js +72 -99
- package/src/api/socket/core/emitAuth.js +17 -9
- package/src/api/socket/e2ee/index.js +15 -33
- package/src/api/socket/e2ee/native/NOTICE.md +2 -2
- package/src/api/socket/e2ee/native/lib/index.mjs +1 -2
- package/src/api/socket/e2ee/native/nativeMediaBridge.js +4 -4
- package/src/api/socket/listenE2EE.js +1 -3
- package/src/api/socket/listenMqtt.js +60 -67
- package/src/utils/antiSuspension.js +3 -3
- package/src/utils/fingerprint.js +35 -16
- package/src/utils/headers.js +1 -1
- package/src/utils/versionCheck.js +47 -0
- package/LICENSE +0 -23
- package/MERGE_REPORT.md +0 -54
- package/THIRD_PARTY_NOTICES.md +0 -36
- package/docs/TEST_RESULTS.md +0 -68
- package/scripts/run-tests.js +0 -19
- package/scripts/syntax-check.js +0 -22
- package/src/api/socket/core/lifecycle.js +0 -117
- package/src/api/socket/e2ee/proto/proto-writer.ts +0 -76
- package/src/utils/typingIndicator.js +0 -202
- package/test/helpers/stub-modules.js +0 -122
- package/test/integration/logout.test.js +0 -69
- package/test/integration/mqtt-lifecycle.test.js +0 -195
- package/test/integration/package.test.js +0 -116
- package/test/integration/typing-wiring.test.js +0 -71
- package/test/unit/fingerprint.test.js +0 -19
- package/test/unit/humanizer.test.js +0 -32
- package/test/unit/lifecycle.test.js +0 -92
- package/test/unit/parsers.test.js +0 -43
- 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 =
|
|
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
|
-
|
|
63
|
-
|
|
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.
|
|
119
|
-
logger("mqtt getSeqID skipped -
|
|
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
|
|
146
|
-
// Retry
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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 =
|
|
308
|
-
const JITTER_MS =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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,
|
|
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
|
|
429
|
-
api.stopListeningAsync = msgEmitter.stopListeningAsync
|
|
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 =
|
|
67
|
-
this.normalHourlyCap =
|
|
68
|
-
this.dailyCap =
|
|
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;
|
package/src/utils/fingerprint.js
CHANGED
|
@@ -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
|
-
"
|
|
14
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/src/utils/headers.js
CHANGED
|
@@ -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
|
|
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.
|
package/THIRD_PARTY_NOTICES.md
DELETED
|
@@ -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).
|
package/docs/TEST_RESULTS.md
DELETED
|
@@ -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).
|
package/scripts/run-tests.js
DELETED
|
@@ -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);
|
package/scripts/syntax-check.js
DELETED
|
@@ -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);
|