fca-phantom 1.0.0
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/LICENSE +58 -0
- package/README.md +534 -0
- package/index.js +35 -0
- package/package.json +101 -0
- package/phantom/core/builder/bootstrap.js +334 -0
- package/phantom/core/builder/config.js +78 -0
- package/phantom/core/builder/forge.js +113 -0
- package/phantom/core/builder/ignite.js +386 -0
- package/phantom/core/builder/options.js +61 -0
- package/phantom/core/engine.js +71 -0
- package/phantom/core/reactor.js +2 -0
- package/phantom/datastore/appState.js +2 -0
- package/phantom/datastore/appStateBackup.js +34 -0
- package/phantom/datastore/models/cipher/e2ee.js +48 -0
- package/phantom/datastore/models/cipher/vault.js +153 -0
- package/phantom/datastore/models/index.js +3 -0
- package/phantom/datastore/models/matrix/auth.js +151 -0
- package/phantom/datastore/models/matrix/cache.js +3 -0
- package/phantom/datastore/models/matrix/checker.js +2 -0
- package/phantom/datastore/models/matrix/clients.js +2 -0
- package/phantom/datastore/models/matrix/constants.js +2 -0
- package/phantom/datastore/models/matrix/credentials.js +2 -0
- package/phantom/datastore/models/matrix/cycle.js +2 -0
- package/phantom/datastore/models/matrix/gate.js +282 -0
- package/phantom/datastore/models/matrix/ghost.js +332 -0
- package/phantom/datastore/models/matrix/headers.js +193 -0
- package/phantom/datastore/models/matrix/heartbeat.js +298 -0
- package/phantom/datastore/models/matrix/identity.js +235 -0
- package/phantom/datastore/models/matrix/logger.js +271 -0
- package/phantom/datastore/models/matrix/monitor.js +2 -0
- package/phantom/datastore/models/matrix/net.js +316 -0
- package/phantom/datastore/models/matrix/response.js +193 -0
- package/phantom/datastore/models/matrix/revive.js +255 -0
- package/phantom/datastore/models/matrix/signals.js +2 -0
- package/phantom/datastore/models/matrix/store.js +263 -0
- package/phantom/datastore/models/matrix/telemetry.js +272 -0
- package/phantom/datastore/models/matrix/tools.js +93 -0
- package/phantom/datastore/models/matrix/transform/cookieParser.js +2 -0
- package/phantom/datastore/models/matrix/transform/cookies.js +114 -0
- package/phantom/datastore/models/matrix/transform/index.js +203 -0
- package/phantom/datastore/models/matrix/validator.js +157 -0
- package/phantom/datastore/models/types/index.d.ts +498 -0
- package/phantom/datastore/schema.js +167 -0
- package/phantom/datastore/session.js +129 -0
- package/phantom/datastore/threads.js +22 -0
- package/phantom/datastore/users.js +26 -0
- package/phantom/dispatch/addExternalModule.js +239 -0
- package/phantom/dispatch/addUserToGroup.js +161 -0
- package/phantom/dispatch/changeAdminStatus.js +142 -0
- package/phantom/dispatch/changeArchivedStatus.js +135 -0
- package/phantom/dispatch/changeAvatar.js +123 -0
- package/phantom/dispatch/changeBio.js +86 -0
- package/phantom/dispatch/changeBlockedStatus.js +86 -0
- package/phantom/dispatch/changeGroupImage.js +145 -0
- package/phantom/dispatch/changeThreadColor.js +172 -0
- package/phantom/dispatch/changeThreadEmoji.js +130 -0
- package/phantom/dispatch/comment.js +136 -0
- package/phantom/dispatch/createAITheme.js +333 -0
- package/phantom/dispatch/createNewGroup.js +99 -0
- package/phantom/dispatch/createPoll.js +148 -0
- package/phantom/dispatch/deleteMessage.js +131 -0
- package/phantom/dispatch/deleteThread.js +155 -0
- package/phantom/dispatch/e2ee.js +101 -0
- package/phantom/dispatch/editMessage.js +158 -0
- package/phantom/dispatch/emoji.js +143 -0
- package/phantom/dispatch/fetchThemeData.js +233 -0
- package/phantom/dispatch/follow.js +111 -0
- package/phantom/dispatch/forwardMessage.js +110 -0
- package/phantom/dispatch/friend.js +189 -0
- package/phantom/dispatch/gcmember.js +138 -0
- package/phantom/dispatch/gcname.js +131 -0
- package/phantom/dispatch/gcrule.js +111 -0
- package/phantom/dispatch/getAccess.js +109 -0
- package/phantom/dispatch/getBotInfo.js +81 -0
- package/phantom/dispatch/getBotInitialData.js +110 -0
- package/phantom/dispatch/getFriendsList.js +118 -0
- package/phantom/dispatch/getMessage.js +199 -0
- package/phantom/dispatch/getTheme.js +199 -0
- package/phantom/dispatch/getThemeInfo.js +160 -0
- package/phantom/dispatch/getThreadHistory.js +139 -0
- package/phantom/dispatch/getThreadInfo.js +153 -0
- package/phantom/dispatch/getThreadList.js +132 -0
- package/phantom/dispatch/getThreadPictures.js +93 -0
- package/phantom/dispatch/getUserID.js +147 -0
- package/phantom/dispatch/getUserInfo.js +513 -0
- package/phantom/dispatch/getUserInfoV2.js +146 -0
- package/phantom/dispatch/handleMessageRequest.js +50 -0
- package/phantom/dispatch/httpGet.js +63 -0
- package/phantom/dispatch/httpPost.js +89 -0
- package/phantom/dispatch/httpPostFormData.js +69 -0
- package/phantom/dispatch/listenMqtt.js +1236 -0
- package/phantom/dispatch/listenSpeed.js +179 -0
- package/phantom/dispatch/logout.js +93 -0
- package/phantom/dispatch/markAsDelivered.js +92 -0
- package/phantom/dispatch/markAsRead.js +119 -0
- package/phantom/dispatch/markAsReadAll.js +215 -0
- package/phantom/dispatch/markAsSeen.js +70 -0
- package/phantom/dispatch/mqttDeltaValue.js +278 -0
- package/phantom/dispatch/muteThread.js +253 -0
- package/phantom/dispatch/nickname.js +132 -0
- package/phantom/dispatch/notes.js +263 -0
- package/phantom/dispatch/pinMessage.js +238 -0
- package/phantom/dispatch/produceMetaTheme.js +335 -0
- package/phantom/dispatch/realtime.js +291 -0
- package/phantom/dispatch/removeUserFromGroup.js +248 -0
- package/phantom/dispatch/resolvePhotoUrl.js +217 -0
- package/phantom/dispatch/searchForThread.js +258 -0
- package/phantom/dispatch/sendMessage.js +354 -0
- package/phantom/dispatch/sendMessageMqtt.js +249 -0
- package/phantom/dispatch/sendTypingIndicator.js +206 -0
- package/phantom/dispatch/setMessageReaction.js +188 -0
- package/phantom/dispatch/setMessageReactionMqtt.js +248 -0
- package/phantom/dispatch/setThreadTheme.js +330 -0
- package/phantom/dispatch/setThreadThemeMqtt.js +207 -0
- package/phantom/dispatch/share.js +200 -0
- package/phantom/dispatch/shareContact.js +216 -0
- package/phantom/dispatch/stickers.js +395 -0
- package/phantom/dispatch/story.js +240 -0
- package/phantom/dispatch/theme.js +296 -0
- package/phantom/dispatch/unfriend.js +199 -0
- package/phantom/dispatch/unsendMessage.js +124 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
|
|
5
|
+
const SUPPORTED_ALGOS = {
|
|
6
|
+
"aes-256-gcm": { keyLen: 32, ivLen: 12, tagLen: 16, aead: true },
|
|
7
|
+
"aes-192-gcm": { keyLen: 24, ivLen: 12, tagLen: 16, aead: true },
|
|
8
|
+
"aes-128-gcm": { keyLen: 16, ivLen: 12, tagLen: 16, aead: true },
|
|
9
|
+
"chacha20-poly1305": { keyLen: 32, ivLen: 12, tagLen: 16, aead: true },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class PhantomCipher {
|
|
13
|
+
constructor(algo = "aes-256-gcm") {
|
|
14
|
+
if (!SUPPORTED_ALGOS[algo]) throw new Error(`Unsupported algorithm: ${algo}`);
|
|
15
|
+
this._algo = algo;
|
|
16
|
+
this._spec = SUPPORTED_ALGOS[algo];
|
|
17
|
+
this._keyRing = new Map();
|
|
18
|
+
this._activeKeyID = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
generateKey() {
|
|
22
|
+
return crypto.randomBytes(this._spec.keyLen);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
generateKeyID() {
|
|
26
|
+
return crypto.randomBytes(8).toString("hex");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
addKey(keyID, key) {
|
|
30
|
+
if (!Buffer.isBuffer(key) || key.length !== this._spec.keyLen) {
|
|
31
|
+
throw new Error(`Key must be ${this._spec.keyLen} bytes for ${this._algo}`);
|
|
32
|
+
}
|
|
33
|
+
this._keyRing.set(keyID, key);
|
|
34
|
+
if (!this._activeKeyID) this._activeKeyID = keyID;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setActiveKey(keyID) {
|
|
39
|
+
if (!this._keyRing.has(keyID)) throw new Error(`Key ${keyID} not found in keyring`);
|
|
40
|
+
this._activeKeyID = keyID;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
rotateKey() {
|
|
45
|
+
const newID = this.generateKeyID();
|
|
46
|
+
const newKey = this.generateKey();
|
|
47
|
+
this.addKey(newID, newKey);
|
|
48
|
+
this._activeKeyID = newID;
|
|
49
|
+
return { keyID: newID, key: newKey };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
removeKey(keyID) {
|
|
53
|
+
if (keyID === this._activeKeyID) throw new Error("Cannot remove the active key");
|
|
54
|
+
this._keyRing.delete(keyID);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
encrypt(plaintext, key = null) {
|
|
58
|
+
const k = key ?? (this._activeKeyID ? this._keyRing.get(this._activeKeyID) : null);
|
|
59
|
+
if (!k) throw new Error("No encryption key provided");
|
|
60
|
+
const buf = typeof plaintext === "string" ? Buffer.from(plaintext, "utf8") : Buffer.from(JSON.stringify(plaintext));
|
|
61
|
+
const iv = crypto.randomBytes(this._spec.ivLen);
|
|
62
|
+
const cipher = crypto.createCipheriv(this._algo, k, iv);
|
|
63
|
+
const enc = Buffer.concat([cipher.update(buf), cipher.final()]);
|
|
64
|
+
const tag = cipher.getAuthTag();
|
|
65
|
+
const payload = {
|
|
66
|
+
v: 1,
|
|
67
|
+
alg: this._algo,
|
|
68
|
+
kid: this._activeKeyID,
|
|
69
|
+
data: Buffer.concat([iv, tag, enc]).toString("base64url"),
|
|
70
|
+
};
|
|
71
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
decrypt(token, key = null) {
|
|
75
|
+
let payload;
|
|
76
|
+
try {
|
|
77
|
+
payload = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
|
|
78
|
+
} catch (_) {
|
|
79
|
+
payload = { v: 0, alg: this._algo, data: token };
|
|
80
|
+
}
|
|
81
|
+
const k = key ?? (payload.kid ? this._keyRing.get(payload.kid) : null) ??
|
|
82
|
+
(this._activeKeyID ? this._keyRing.get(this._activeKeyID) : null);
|
|
83
|
+
if (!k) throw new Error("No decryption key available");
|
|
84
|
+
const algo = payload.alg || this._algo;
|
|
85
|
+
const spec = SUPPORTED_ALGOS[algo] || this._spec;
|
|
86
|
+
const buf = Buffer.from(payload.data, "base64url");
|
|
87
|
+
const iv = buf.slice(0, spec.ivLen);
|
|
88
|
+
const tag = buf.slice(spec.ivLen, spec.ivLen + spec.tagLen);
|
|
89
|
+
const enc = buf.slice(spec.ivLen + spec.tagLen);
|
|
90
|
+
const decipher = crypto.createDecipheriv(algo, k, iv);
|
|
91
|
+
decipher.setAuthTag(tag);
|
|
92
|
+
const plain = Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
|
|
93
|
+
try { return JSON.parse(plain); } catch (_) { return plain; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
deriveKey(passphrase, salt = null) {
|
|
97
|
+
const s = salt instanceof Buffer ? salt : (salt ? Buffer.from(salt, "hex") : crypto.randomBytes(32));
|
|
98
|
+
const key = crypto.scryptSync(passphrase, s, this._spec.keyLen, { N: 32768, r: 8, p: 1 });
|
|
99
|
+
return { key, salt: s.toString("hex") };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
hash(data, algo = "sha256", encoding = "hex") {
|
|
103
|
+
return crypto.createHash(algo).update(data).digest(encoding);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
hmac(data, key, algo = "sha256", encoding = "hex") {
|
|
107
|
+
return crypto.createHmac(algo, key).update(data).digest(encoding);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sign(data, privateKeyOrSecret) {
|
|
111
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
112
|
+
const ts = Date.now();
|
|
113
|
+
const toSign = `${ts}.${payload}`;
|
|
114
|
+
const sig = this.hmac(toSign, privateKeyOrSecret, "sha256", "base64url");
|
|
115
|
+
return `${Buffer.from(toSign).toString("base64url")}.${sig}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
verify(token, secretOrKey, maxAgeMs = 0) {
|
|
119
|
+
try {
|
|
120
|
+
const parts = token.split(".");
|
|
121
|
+
if (parts.length < 2) return { valid: false, reason: "malformed_token" };
|
|
122
|
+
const sig = parts.pop();
|
|
123
|
+
const raw = parts.join(".");
|
|
124
|
+
const decoded = Buffer.from(raw, "base64url").toString("utf8");
|
|
125
|
+
const dot = decoded.indexOf(".");
|
|
126
|
+
const ts = parseInt(decoded.slice(0, dot), 10);
|
|
127
|
+
const payload = decoded.slice(dot + 1);
|
|
128
|
+
const expected = this.hmac(decoded, secretOrKey, "sha256", "base64url");
|
|
129
|
+
const valid = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
|
|
130
|
+
if (!valid) return { valid: false, reason: "invalid_signature" };
|
|
131
|
+
if (maxAgeMs > 0 && Date.now() - ts > maxAgeMs) return { valid: false, reason: "token_expired" };
|
|
132
|
+
try { return { valid: true, data: JSON.parse(payload), ts }; }
|
|
133
|
+
catch (_) { return { valid: true, data: payload, ts }; }
|
|
134
|
+
} catch (_) {
|
|
135
|
+
return { valid: false, reason: "verification_error" };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
randomBytes(n = 32) { return crypto.randomBytes(n); }
|
|
140
|
+
randomHex(n = 32) { return crypto.randomBytes(n).toString("hex"); }
|
|
141
|
+
randomBase64(n = 32){ return crypto.randomBytes(n).toString("base64url"); }
|
|
142
|
+
|
|
143
|
+
secureCompare(a, b) {
|
|
144
|
+
const ba = Buffer.from(String(a));
|
|
145
|
+
const bb = Buffer.from(String(b));
|
|
146
|
+
if (ba.length !== bb.length) return false;
|
|
147
|
+
return crypto.timingSafeEqual(ba, bb);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const globalCipher = new PhantomCipher("aes-256-gcm");
|
|
152
|
+
|
|
153
|
+
module.exports = { PhantomCipher, globalCipher, SUPPORTED_ALGOS };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
|
|
5
|
+
const REGION_MAP = new Map([
|
|
6
|
+
["PRN", { code: "PRN", name: "Pacific Northwest", dc: "sjc" }],
|
|
7
|
+
["VLL", { code: "VLL", name: "Valley Region", dc: "sjc" }],
|
|
8
|
+
["ASH", { code: "ASH", name: "Ashburn", dc: "iad" }],
|
|
9
|
+
["DFW", { code: "DFW", name: "Dallas / Fort Worth", dc: "dfw" }],
|
|
10
|
+
["LLA", { code: "LLA", name: "Los Angeles", dc: "lax" }],
|
|
11
|
+
["FRA", { code: "FRA", name: "Frankfurt", dc: "fra" }],
|
|
12
|
+
["SIN", { code: "SIN", name: "Singapore", dc: "sin" }],
|
|
13
|
+
["NRT", { code: "NRT", name: "Tokyo", dc: "nrt" }],
|
|
14
|
+
["HKG", { code: "HKG", name: "Hong Kong", dc: "hkg" }],
|
|
15
|
+
["SYD", { code: "SYD", name: "Sydney", dc: "syd" }],
|
|
16
|
+
["PNB", { code: "PNB", name: "Pacific Northwest Beta",dc: "sjc2"}],
|
|
17
|
+
["AMS", { code: "AMS", name: "Amsterdam", dc: "ams" }],
|
|
18
|
+
["CDG", { code: "CDG", name: "Paris", dc: "cdg" }],
|
|
19
|
+
["GRU", { code: "GRU", name: "Sao Paulo", dc: "gru" }],
|
|
20
|
+
["BOM", { code: "BOM", name: "Mumbai", dc: "bom" }],
|
|
21
|
+
["ICN", { code: "ICN", name: "Seoul", dc: "icn" }],
|
|
22
|
+
["JNB", { code: "JNB", name: "Johannesburg", dc: "jnb" }],
|
|
23
|
+
["MEL", { code: "MEL", name: "Melbourne", dc: "mel" }],
|
|
24
|
+
["ORD", { code: "ORD", name: "Chicago", dc: "ord" }],
|
|
25
|
+
["MIA", { code: "MIA", name: "Miami", dc: "mia" }],
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function parseRegion(html) {
|
|
29
|
+
try {
|
|
30
|
+
for (const pattern of [
|
|
31
|
+
/"endpoint":"([^"]+)"/,
|
|
32
|
+
/endpoint\\":\\"([^\\"]+)\\"/,
|
|
33
|
+
/"mqttEndpoint":"([^"]+)"/,
|
|
34
|
+
/"chat_endpoint":"([^"]+)"/,
|
|
35
|
+
]) {
|
|
36
|
+
const m = html.match(pattern);
|
|
37
|
+
if (!m) continue;
|
|
38
|
+
const raw = m[1].replace(/\\\//g, "/");
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(raw);
|
|
41
|
+
const code = url.searchParams.get("region")?.toUpperCase();
|
|
42
|
+
if (code && REGION_MAP.has(code)) return code;
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
return "PRN";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getRegionInfo(code) { return REGION_MAP.get(code?.toUpperCase()) || null; }
|
|
50
|
+
function getAllRegions() { return [...REGION_MAP.values()]; }
|
|
51
|
+
function isValidRegion(code) { return REGION_MAP.has(code?.toUpperCase()); }
|
|
52
|
+
|
|
53
|
+
function _base32Decode(secret) {
|
|
54
|
+
const alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
55
|
+
const cleaned = secret.replace(/\s+/g, "").toUpperCase().replace(/=+$/, "");
|
|
56
|
+
let bits = "";
|
|
57
|
+
for (const c of cleaned) {
|
|
58
|
+
const v = alpha.indexOf(c);
|
|
59
|
+
if (v === -1) continue;
|
|
60
|
+
bits += v.toString(2).padStart(5, "0");
|
|
61
|
+
}
|
|
62
|
+
const bytes = [];
|
|
63
|
+
for (let i = 0; i + 8 <= bits.length; i += 8) {
|
|
64
|
+
bytes.push(parseInt(bits.substr(i, 8), 2));
|
|
65
|
+
}
|
|
66
|
+
return Buffer.from(bytes);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _computeHotp(key, counter) {
|
|
70
|
+
const buf = Buffer.alloc(8);
|
|
71
|
+
buf.writeBigUInt64BE(BigInt(counter));
|
|
72
|
+
const digest = crypto.createHmac("sha1", key).update(buf).digest();
|
|
73
|
+
const offset = digest[digest.length - 1] & 0x0f;
|
|
74
|
+
const code = (
|
|
75
|
+
((digest[offset] & 0x7f) << 24) |
|
|
76
|
+
((digest[offset + 1] & 0xff) << 16) |
|
|
77
|
+
((digest[offset + 2] & 0xff) << 8) |
|
|
78
|
+
(digest[offset + 3] & 0xff)
|
|
79
|
+
) % 1_000_000;
|
|
80
|
+
return code.toString().padStart(6, "0");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function genTotp(secret, opts = {}) {
|
|
84
|
+
const cleaned = String(secret || "").replace(/\s+/g, "").toUpperCase();
|
|
85
|
+
if (!cleaned) throw new Error("TOTP secret is empty");
|
|
86
|
+
|
|
87
|
+
const period = opts.period || 30;
|
|
88
|
+
const algorithm = opts.algorithm || "SHA1";
|
|
89
|
+
const digits = opts.digits || 6;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const gen = require("totp-generator");
|
|
93
|
+
if (typeof gen?.TOTP?.generate === "function") {
|
|
94
|
+
const r = await gen.TOTP.generate(cleaned, { period, algorithm, digits });
|
|
95
|
+
return typeof r === "object" ? r.otp : String(r);
|
|
96
|
+
}
|
|
97
|
+
const code = gen(cleaned, { period, algorithm, digits });
|
|
98
|
+
return String(code);
|
|
99
|
+
} catch (_) {
|
|
100
|
+
const key = _base32Decode(cleaned);
|
|
101
|
+
const counter = Math.floor(Date.now() / 1000 / period);
|
|
102
|
+
return _computeHotp(key, counter);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getTotpRemainingSeconds(period = 30) {
|
|
107
|
+
return period - (Math.floor(Date.now() / 1000) % period);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateCredentials(creds) {
|
|
111
|
+
const errors = [];
|
|
112
|
+
if (!creds || typeof creds !== "object") return { valid: false, errors: ["Credentials must be an object"] };
|
|
113
|
+
if (!creds.appState && !creds.email && !creds.password) {
|
|
114
|
+
errors.push("Must provide appState cookies or email+password");
|
|
115
|
+
}
|
|
116
|
+
if (creds.email && !creds.password) errors.push("password is required with email");
|
|
117
|
+
if (creds.password && !creds.email) errors.push("email is required with password");
|
|
118
|
+
if (creds.totpSecret && typeof creds.totpSecret !== "string") errors.push("totpSecret must be a string");
|
|
119
|
+
return { valid: errors.length === 0, errors };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateCookieArray(arr) {
|
|
123
|
+
if (!Array.isArray(arr)) return false;
|
|
124
|
+
const required = ["c_user", "xs"];
|
|
125
|
+
const names = arr.map(c => c.name || c.key).filter(Boolean);
|
|
126
|
+
return required.every(k => names.includes(k));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseCookieString(str) {
|
|
130
|
+
if (!str || typeof str !== "string") return [];
|
|
131
|
+
return str.split(/;\s*/)
|
|
132
|
+
.map(p => {
|
|
133
|
+
const eq = p.indexOf("=");
|
|
134
|
+
if (eq === -1) return null;
|
|
135
|
+
return { name: p.slice(0, eq).trim(), value: p.slice(eq + 1).trim() };
|
|
136
|
+
})
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
REGION_MAP,
|
|
142
|
+
parseRegion,
|
|
143
|
+
genTotp,
|
|
144
|
+
getTotpRemainingSeconds,
|
|
145
|
+
getRegionInfo,
|
|
146
|
+
getAllRegions,
|
|
147
|
+
isValidRegion,
|
|
148
|
+
validateCredentials,
|
|
149
|
+
validateCookieArray,
|
|
150
|
+
parseCookieString,
|
|
151
|
+
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const TOKEN_BUCKET_REFILL_MS = 1000;
|
|
4
|
+
|
|
5
|
+
class TokenBucket {
|
|
6
|
+
constructor(capacity, refillRate) {
|
|
7
|
+
this._capacity = capacity;
|
|
8
|
+
this._tokens = capacity;
|
|
9
|
+
this._refillRate = refillRate;
|
|
10
|
+
this._lastRefill = Date.now();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_refill() {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const elapsed = (now - this._lastRefill) / TOKEN_BUCKET_REFILL_MS;
|
|
16
|
+
this._tokens = Math.min(this._capacity, this._tokens + elapsed * this._refillRate);
|
|
17
|
+
this._lastRefill = now;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
consume(n = 1) {
|
|
21
|
+
this._refill();
|
|
22
|
+
if (this._tokens < n) return false;
|
|
23
|
+
this._tokens -= n;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
available() { this._refill(); return this._tokens; }
|
|
28
|
+
remaining() { return Math.max(0, Math.floor(this.available())); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ACTION_COSTS = {
|
|
32
|
+
sendMessage: 2,
|
|
33
|
+
sendFile: 3,
|
|
34
|
+
sendImage: 3,
|
|
35
|
+
react: 1,
|
|
36
|
+
markRead: 1,
|
|
37
|
+
getThread: 1,
|
|
38
|
+
getThreadList: 2,
|
|
39
|
+
getUserInfo: 1,
|
|
40
|
+
getThreadHistory: 2,
|
|
41
|
+
search: 2,
|
|
42
|
+
graphql: 3,
|
|
43
|
+
default: 1,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
class TrafficGate {
|
|
47
|
+
constructor() {
|
|
48
|
+
this._threadCooldown = new Map();
|
|
49
|
+
this._endpointCooldown = new Map();
|
|
50
|
+
this._errorLog = new Map();
|
|
51
|
+
this._endpointHealth = new Map();
|
|
52
|
+
this._priorityQueue = [];
|
|
53
|
+
this._processing = false;
|
|
54
|
+
|
|
55
|
+
this.ERROR_LOG_TTL = 300_000;
|
|
56
|
+
this.COOLDOWN_DURATION = 60_000;
|
|
57
|
+
this.MAX_REQUESTS_PER_MINUTE = 90;
|
|
58
|
+
this.MAX_CONCURRENT_REQUESTS = 10;
|
|
59
|
+
this.MAX_PER_ENDPOINT_MINUTE = 35;
|
|
60
|
+
|
|
61
|
+
this._active = 0;
|
|
62
|
+
this._window = [];
|
|
63
|
+
this._WINDOW_MS = 60_000;
|
|
64
|
+
this._epWindows = new Map();
|
|
65
|
+
this._cdCache = new Map();
|
|
66
|
+
this._CD_TTL = 400;
|
|
67
|
+
|
|
68
|
+
this._errorRate = 0;
|
|
69
|
+
this._totalReqs = 0;
|
|
70
|
+
this._totalErrors= 0;
|
|
71
|
+
this._adaptTimer = setInterval(() => this._adaptLimits(), 30_000);
|
|
72
|
+
try { this._adaptTimer.unref(); } catch (_) {}
|
|
73
|
+
|
|
74
|
+
this._globalBucket = new TokenBucket(this.MAX_REQUESTS_PER_MINUTE, this.MAX_REQUESTS_PER_MINUTE / 60);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_adaptLimits() {
|
|
78
|
+
if (this._totalReqs < 10) return;
|
|
79
|
+
const errRate = this._totalErrors / this._totalReqs;
|
|
80
|
+
if (errRate > 0.2) {
|
|
81
|
+
this.MAX_CONCURRENT_REQUESTS = Math.max(2, this.MAX_CONCURRENT_REQUESTS - 2);
|
|
82
|
+
this.MAX_REQUESTS_PER_MINUTE = Math.max(20, this.MAX_REQUESTS_PER_MINUTE - 10);
|
|
83
|
+
} else if (errRate < 0.02 && this._totalReqs > 30) {
|
|
84
|
+
this.MAX_CONCURRENT_REQUESTS = Math.min(20, this.MAX_CONCURRENT_REQUESTS + 1);
|
|
85
|
+
this.MAX_REQUESTS_PER_MINUTE = Math.min(120, this.MAX_REQUESTS_PER_MINUTE + 5);
|
|
86
|
+
}
|
|
87
|
+
this._errorRate = errRate;
|
|
88
|
+
this._totalReqs = 0;
|
|
89
|
+
this._totalErrors = 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
recordRequest(ok = true) {
|
|
93
|
+
this._totalReqs++;
|
|
94
|
+
if (!ok) this._totalErrors++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
configure(opts = {}) {
|
|
98
|
+
if (typeof opts.maxConcurrentRequests === "number" && opts.maxConcurrentRequests > 0)
|
|
99
|
+
this.MAX_CONCURRENT_REQUESTS = Math.floor(opts.maxConcurrentRequests);
|
|
100
|
+
if (typeof opts.maxRequestsPerMinute === "number" && opts.maxRequestsPerMinute > 0)
|
|
101
|
+
this.MAX_REQUESTS_PER_MINUTE = Math.floor(opts.maxRequestsPerMinute);
|
|
102
|
+
if (typeof opts.requestCooldownMs === "number" && opts.requestCooldownMs >= 0)
|
|
103
|
+
this.COOLDOWN_DURATION = Math.floor(opts.requestCooldownMs);
|
|
104
|
+
if (typeof opts.errorCacheTtlMs === "number" && opts.errorCacheTtlMs >= 0)
|
|
105
|
+
this.ERROR_LOG_TTL = Math.floor(opts.errorCacheTtlMs);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_pruneWindow(arr) {
|
|
109
|
+
const cutoff = Date.now() - this._WINDOW_MS;
|
|
110
|
+
let lo = 0, hi = arr.length;
|
|
111
|
+
while (lo < hi) {
|
|
112
|
+
const mid = (lo + hi) >> 1;
|
|
113
|
+
arr[mid] < cutoff ? (lo = mid + 1) : (hi = mid);
|
|
114
|
+
}
|
|
115
|
+
if (lo > 0) arr.splice(0, lo);
|
|
116
|
+
return arr.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_isCooldown(map, key) {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const ck = String(key);
|
|
122
|
+
const hit = this._cdCache.get(ck);
|
|
123
|
+
if (hit && now - hit.ts < this._CD_TTL) return hit.v;
|
|
124
|
+
const end = map.get(key);
|
|
125
|
+
let v = false;
|
|
126
|
+
if (!end) v = false;
|
|
127
|
+
else if (now >= end) { map.delete(key); v = false; }
|
|
128
|
+
else v = true;
|
|
129
|
+
this._cdCache.set(ck, { ts: now, v });
|
|
130
|
+
return v;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
isThreadOnCooldown(tid) { return this._isCooldown(this._threadCooldown, tid); }
|
|
134
|
+
isEndpointOnCooldown(ep) { return this._isCooldown(this._endpointCooldown, ep); }
|
|
135
|
+
|
|
136
|
+
setThreadCooldown(tid, ms) {
|
|
137
|
+
this._threadCooldown.set(tid, Date.now() + (ms || this.COOLDOWN_DURATION));
|
|
138
|
+
this._cdCache.delete(String(tid));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setEndpointCooldown(ep, ms) {
|
|
142
|
+
this._endpointCooldown.set(ep, Date.now() + (ms || this.COOLDOWN_DURATION));
|
|
143
|
+
this._cdCache.delete(String(ep));
|
|
144
|
+
this._updateEndpointHealth(ep, false);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_updateEndpointHealth(ep, ok) {
|
|
148
|
+
const h = this._endpointHealth.get(ep) || { ok: 0, fail: 0, score: 100 };
|
|
149
|
+
ok ? h.ok++ : h.fail++;
|
|
150
|
+
const total = h.ok + h.fail;
|
|
151
|
+
h.score = total > 0 ? Math.round((h.ok / total) * 100) : 100;
|
|
152
|
+
this._endpointHealth.set(ep, h);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getEndpointHealth(ep) { return this._endpointHealth.get(ep)?.score ?? 100; }
|
|
156
|
+
|
|
157
|
+
shouldSuppressError(key) {
|
|
158
|
+
const t = this._errorLog.get(key);
|
|
159
|
+
if (!t || Date.now() - t > this.ERROR_LOG_TTL) {
|
|
160
|
+
this._errorLog.set(key, Date.now());
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
isGloballyThrottled() {
|
|
167
|
+
return this._pruneWindow(this._window) >= this.MAX_REQUESTS_PER_MINUTE ||
|
|
168
|
+
!this._globalBucket.consume(0.01);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
isEndpointThrottled(ep) {
|
|
172
|
+
if (!this._epWindows.has(ep)) return false;
|
|
173
|
+
return this._pruneWindow(this._epWindows.get(ep)) >= this.MAX_PER_ENDPOINT_MINUTE;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_record(ep) {
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
this._window.push(now);
|
|
179
|
+
if (this._window.length > this.MAX_REQUESTS_PER_MINUTE * 3) this._pruneWindow(this._window);
|
|
180
|
+
if (ep) {
|
|
181
|
+
if (!this._epWindows.has(ep)) this._epWindows.set(ep, []);
|
|
182
|
+
const w = this._epWindows.get(ep);
|
|
183
|
+
w.push(now);
|
|
184
|
+
if (w.length > this.MAX_PER_ENDPOINT_MINUTE * 3) this._pruneWindow(w);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getAdaptiveDelay(retry, errorCode = null) {
|
|
189
|
+
const bases = [600, 1500, 3500, 7000, 15_000];
|
|
190
|
+
const base = bases[Math.min(retry, bases.length - 1)];
|
|
191
|
+
const jitter = Math.floor(Math.random() * 500);
|
|
192
|
+
let mult = 1;
|
|
193
|
+
if (errorCode === 1545012 || errorCode === 1675004) mult = 2;
|
|
194
|
+
else if (errorCode === 368 || errorCode === 10) mult = 3;
|
|
195
|
+
else if (this._errorRate > 0.1) mult = 1.5;
|
|
196
|
+
return Math.floor(base * mult) + jitter;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async addHumanDelay(min = 60, max = 280) {
|
|
200
|
+
const ms = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
201
|
+
await new Promise(r => setTimeout(r, ms));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getActionCost(action) {
|
|
205
|
+
return ACTION_COSTS[action] || ACTION_COSTS.default;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async checkRateLimit(skipDelay = false, ep = null, action = null) {
|
|
209
|
+
const cost = action ? this.getActionCost(action) : 1;
|
|
210
|
+
|
|
211
|
+
const canProceed = () =>
|
|
212
|
+
this._active < this.MAX_CONCURRENT_REQUESTS &&
|
|
213
|
+
!this.isGloballyThrottled() &&
|
|
214
|
+
!(ep && this.isEndpointThrottled(ep)) &&
|
|
215
|
+
this._globalBucket.consume(cost);
|
|
216
|
+
|
|
217
|
+
if (canProceed()) {
|
|
218
|
+
if (!skipDelay) await this.addHumanDelay();
|
|
219
|
+
this._active++;
|
|
220
|
+
this._record(ep);
|
|
221
|
+
return () => { this._active = Math.max(0, this._active - 1); };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let waited = 0;
|
|
225
|
+
while (!canProceed() && waited < 120_000) {
|
|
226
|
+
const pause = this._active >= this.MAX_CONCURRENT_REQUESTS ? 30 : 500;
|
|
227
|
+
await new Promise(r => setTimeout(r, pause));
|
|
228
|
+
waited += pause;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!skipDelay) await this.addHumanDelay();
|
|
232
|
+
this._active++;
|
|
233
|
+
this._record(ep);
|
|
234
|
+
return () => { this._active = Math.max(0, this._active - 1); };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
getRemainingCooldown(key) {
|
|
238
|
+
const t = this._threadCooldown.get(key) || this._endpointCooldown.get(key);
|
|
239
|
+
return t ? Math.max(0, t - Date.now()) : 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
cleanup() {
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
for (const [k, t] of this._errorLog) if (now - t > this.ERROR_LOG_TTL) this._errorLog.delete(k);
|
|
245
|
+
for (const [k, t] of this._threadCooldown) if (now >= t) this._threadCooldown.delete(k);
|
|
246
|
+
for (const [k, t] of this._endpointCooldown) if (now >= t) this._endpointCooldown.delete(k);
|
|
247
|
+
for (const [k, w] of this._epWindows) { this._pruneWindow(w); if (!w.length) this._epWindows.delete(k); }
|
|
248
|
+
this._pruneWindow(this._window);
|
|
249
|
+
this._cdCache.clear();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
getStats() {
|
|
253
|
+
this._pruneWindow(this._window);
|
|
254
|
+
return {
|
|
255
|
+
active: this._active,
|
|
256
|
+
maxConcurrent: this.MAX_CONCURRENT_REQUESTS,
|
|
257
|
+
maxPerMinute: this.MAX_REQUESTS_PER_MINUTE,
|
|
258
|
+
requestsLastMinute: this._window.length,
|
|
259
|
+
threadCooldowns: this._threadCooldown.size,
|
|
260
|
+
endpointCooldowns: this._endpointCooldown.size,
|
|
261
|
+
suppressedErrors: this._errorLog.size,
|
|
262
|
+
tokenBucketTokens: this._globalBucket.remaining(),
|
|
263
|
+
adaptedErrorRate: (this._errorRate * 100).toFixed(1) + "%",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
destroy() {
|
|
268
|
+
clearInterval(this._adaptTimer);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const globalGate = new TrafficGate();
|
|
273
|
+
setInterval(() => globalGate.cleanup(), 60_000).unref?.();
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
TrafficGate,
|
|
277
|
+
TokenBucket,
|
|
278
|
+
globalRateLimiter: globalGate,
|
|
279
|
+
globalGate,
|
|
280
|
+
configureRateLimiter: (opts) => globalGate.configure(opts),
|
|
281
|
+
getRateLimiterStats: () => globalGate.getStats(),
|
|
282
|
+
};
|