rush-mfa 1.0.7 → 1.0.8
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/README.md +84 -12
- package/index.js +203 -83
- package/index.mjs +4 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# rush-mfa
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Discord MFA token generator with HTTP/2, host fallback and IP rate limit handling.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🚀 **Async/Await & Promise (.then) Support** - Non-blocking API
|
|
8
8
|
- 📦 **ESM & CommonJS Support** - Works with `.mjs`, `.cjs`, `.js`
|
|
9
|
-
-
|
|
10
|
-
-
|
|
9
|
+
- 🌐 **HTTP/2 Protocol** - Faster multiplexed connections
|
|
10
|
+
- 🔄 **Host Fallback** - canary.discord.com → discord.com on rate limit
|
|
11
11
|
- ⚡ **Callback Support** - Traditional Node.js callback style available
|
|
12
12
|
- 🔧 **Zero Config** - Works out of the box
|
|
13
|
-
- ⏱️ **IP Rate Limit Handling** - Auto
|
|
13
|
+
- ⏱️ **IP Rate Limit Handling** - Auto 30min cooldown on IP rate limit (429)
|
|
14
14
|
- 🔁 **Auto Retry** - Retries on rate limit with retry_after parsing
|
|
15
|
+
- 🆔 **X-Installation-ID** - Discord client fingerprint support
|
|
16
|
+
- 🛡️ **Safe JSON Parse** - Handles HTML/Cloudflare responses gracefully
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -34,6 +36,9 @@ if (mfa.isRateLimited()) {
|
|
|
34
36
|
console.log(token);
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
// Set your own installation ID (optional)
|
|
40
|
+
mfa.setInstallationId('1465561582800081062.6ov7tRO-------');
|
|
41
|
+
|
|
37
42
|
// Promise (.then) - Non-blocking
|
|
38
43
|
mfa.get('DISCORD_TOKEN', 'PASSWORD')
|
|
39
44
|
.then(token => console.log(token))
|
|
@@ -88,7 +93,7 @@ Get MFA token for Discord API authentication.
|
|
|
88
93
|
|
|
89
94
|
**Errors:**
|
|
90
95
|
- `IP_RATE_LIMITED:XXXs remaining` - IP is rate limited, wait XXX seconds
|
|
91
|
-
- `
|
|
96
|
+
- `MFA_FAILED:password_wrong_or_token_ratelimited_or_patched` - Password wrong, token rate limited, or MFA patched
|
|
92
97
|
- `UNAUTHORIZED` - Invalid token
|
|
93
98
|
- `TOKEN_INVALID` - Token is invalid
|
|
94
99
|
- `No ticket` - Could not get MFA ticket
|
|
@@ -136,24 +141,91 @@ Get current cached headers object.
|
|
|
136
141
|
const headers = mfa.getHeaders();
|
|
137
142
|
```
|
|
138
143
|
|
|
144
|
+
### `mfa.getInstallationId()`
|
|
145
|
+
|
|
146
|
+
Get the current X-Installation-ID.
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
const installId = mfa.getInstallationId();
|
|
150
|
+
console.log(installId); // "1234567890.abc123xyz..."
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `mfa.setInstallationId(id)`
|
|
154
|
+
|
|
155
|
+
Set a custom X-Installation-ID (from your Discord client).
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
// Use your own Discord client's installation ID
|
|
159
|
+
mfa.setInstallationId('1465561582800081062.6ov7tROCKtZoFslCqgqzvbgeUiA');
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `mfa.generateInstallationId()`
|
|
163
|
+
|
|
164
|
+
Generate a new random X-Installation-ID.
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
const newId = mfa.generateInstallationId();
|
|
168
|
+
console.log(newId); // "1738423456789012345.aB3dEfGhIjKlMnOpQrStUvWxYz0"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Headers Included
|
|
172
|
+
|
|
173
|
+
The library sends only essential Discord client headers:
|
|
174
|
+
|
|
175
|
+
| Header | Description |
|
|
176
|
+
|--------|-------------|
|
|
177
|
+
| `Content-Type` | `application/json` |
|
|
178
|
+
| `Origin` | `https://canary.discord.com` |
|
|
179
|
+
| `Referer` | `https://canary.discord.com/channels/@me` |
|
|
180
|
+
| `Sec-Fetch-Dest` | `empty` |
|
|
181
|
+
| `Sec-Fetch-Mode` | `cors` |
|
|
182
|
+
| `Sec-Fetch-Site` | `same-origin` |
|
|
183
|
+
| `User-Agent` | Discord client UA |
|
|
184
|
+
| `X-Debug-Options` | `bugReporterEnabled` |
|
|
185
|
+
| `X-Discord-Locale` | `tr` |
|
|
186
|
+
| `X-Discord-Timezone` | `Europe/Istanbul` |
|
|
187
|
+
| `X-Installation-Id` | Unique client fingerprint |
|
|
188
|
+
| `X-Super-Properties` | Base64 encoded client info |
|
|
189
|
+
|
|
139
190
|
## Rate Limit Handling
|
|
140
191
|
|
|
141
192
|
The library automatically handles rate limits:
|
|
142
193
|
|
|
143
194
|
1. **429 with retry_after < 60s** → Auto retry after waiting
|
|
144
|
-
2. **
|
|
145
|
-
3. **
|
|
195
|
+
2. **Rate limited on canary** → Fallback to discord.com (stable)
|
|
196
|
+
3. **Rate limited on both hosts** → 30 minute cooldown activated
|
|
197
|
+
4. **Cloudflare/HTML response** → Safe JSON parse, extracts retry_after if available
|
|
198
|
+
5. **Subsequent calls during cooldown** → Immediately rejected with `IP_RATE_LIMITED`
|
|
199
|
+
|
|
200
|
+
## Host Fallback
|
|
146
201
|
|
|
147
|
-
|
|
202
|
+
The library uses HTTP/2 with automatic host fallback:
|
|
148
203
|
|
|
149
|
-
|
|
204
|
+
1. First tries **canary.discord.com** with canary X-Super-Properties
|
|
205
|
+
2. If rate limited → tries **discord.com** with stable X-Super-Properties
|
|
206
|
+
3. If both fail → 30 minute cooldown activated
|
|
150
207
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
208
|
+
### Build Numbers
|
|
209
|
+
|
|
210
|
+
| Host | release_channel | client_version | native_build_number |
|
|
211
|
+
|------|-----------------|----------------|---------------------|
|
|
212
|
+
| canary.discord.com | canary | 1.0.816 | 74605 |
|
|
213
|
+
| discord.com | stable | 1.0.9221 | 74058 |
|
|
154
214
|
|
|
155
215
|
## Changelog
|
|
156
216
|
|
|
217
|
+
### 1.0.8
|
|
218
|
+
- **🚀 HTTP/2 Protocol** - Switched from HTTPS to HTTP/2 for faster connections
|
|
219
|
+
- **🔄 Host Fallback** - canary.discord.com → discord.com on rate limit
|
|
220
|
+
- **🛡️ Safe JSON Parse** - Handles HTML/Cloudflare responses without crashing
|
|
221
|
+
- **📊 Dual X-Super-Properties** - Separate configs for canary and stable
|
|
222
|
+
- Updated build numbers (canary: 492018/74605, stable: 492022/74058)
|
|
223
|
+
- Added `closeSessions()` method to cleanup HTTP/2 connections
|
|
224
|
+
- 30 minute cooldown on IP rate limit
|
|
225
|
+
- Better error messages for 60008 (password wrong/token rate limited/patched)
|
|
226
|
+
- Added `X-Installation-Id` header support (device fingerprint)
|
|
227
|
+
- Added `getInstallationId()`, `setInstallationId()`, `generateInstallationId()` methods
|
|
228
|
+
|
|
157
229
|
### 1.0.6
|
|
158
230
|
- Added IP rate limit handling with 15 minute cooldown
|
|
159
231
|
- Added `isRateLimited()`, `getRateLimitRemaining()`, `clearRateLimit()` methods
|
package/index.js
CHANGED
|
@@ -1,128 +1,244 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const http2 = require("node:http2");
|
|
4
4
|
const crypto = require("node:crypto");
|
|
5
|
+
const tls = require("node:tls");
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
a:
|
|
8
|
-
b:
|
|
9
|
-
c:
|
|
7
|
+
const _tlsOpts = {
|
|
8
|
+
a: { minVersion: 'TLSv1.3', maxVersion: 'TLSv1.3', honorCipherOrder: true, rejectUnauthorized: true },
|
|
9
|
+
b: { minVersion: 'TLSv1.2', maxVersion: 'TLSv1.2', honorCipherOrder: true, rejectUnauthorized: true },
|
|
10
|
+
c: { minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3', honorCipherOrder: true, rejectUnauthorized: true }
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
let
|
|
13
|
+
let _sessions = { canary: null, stable: null };
|
|
14
|
+
let _h = { canary: null, stable: null }, _init = { canary: false, stable: false }, _installId = null;
|
|
13
15
|
let _ipRateUntil = 0;
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
+
|
|
17
|
+
const _builds = {
|
|
18
|
+
canary: { b: 492018, n: 74605, v: "1.0.816", ch: "canary" },
|
|
19
|
+
stable: { b: 492022, n: 74058, v: "1.0.9221", ch: "stable" }
|
|
20
|
+
};
|
|
21
|
+
const _de = "37.6.0", _dc = "138.0.7204.251";
|
|
22
|
+
const IP_RATE_COOLDOWN = 30 * 60 * 1000;
|
|
23
|
+
const HOSTS = { canary: "canary.discord.com", stable: "discord.com" };
|
|
16
24
|
|
|
17
25
|
const _u = () => crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); });
|
|
18
26
|
const _sl = ms => new Promise(r => setTimeout(r, ms));
|
|
19
27
|
|
|
28
|
+
const _genInstallId = () => {
|
|
29
|
+
const ts = BigInt(Date.now() - 1420070400000) << 22n;
|
|
30
|
+
const snowflake = ts | (BigInt(Math.floor(Math.random() * 31)) << 17n) | (BigInt(Math.floor(Math.random() * 31)) << 12n) | BigInt(Math.floor(Math.random() * 4095));
|
|
31
|
+
const rand = crypto.randomBytes(20).toString('base64').replace(/[+/=]/g, c => c === '+' ? 'a' : c === '/' ? 'b' : '').slice(0, 27);
|
|
32
|
+
return `${snowflake}.${rand}`;
|
|
33
|
+
};
|
|
34
|
+
const _getInstallId = () => { if (!_installId) _installId = _genInstallId(); return _installId; };
|
|
35
|
+
|
|
20
36
|
const isRateLimited = () => Date.now() < _ipRateUntil;
|
|
21
37
|
const getRateLimitRemaining = () => Math.max(0, Math.ceil((_ipRateUntil - Date.now()) / 1000));
|
|
22
38
|
const clearRateLimit = () => { _ipRateUntil = 0; };
|
|
23
|
-
const setRateLimit = (seconds =
|
|
39
|
+
const setRateLimit = (seconds = 1800) => { _ipRateUntil = Date.now() + (seconds * 1000); };
|
|
24
40
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
const _safeJson = (raw) => {
|
|
42
|
+
if (!raw || raw.length === 0) return { _empty: true };
|
|
43
|
+
const trimmed = raw.trim();
|
|
44
|
+
if (trimmed.startsWith('<!') || trimmed.startsWith('<html') || trimmed.startsWith('<head') || trimmed.toLowerCase().includes('<!doctype')) {
|
|
45
|
+
const retryMatch = raw.match(/retry[_-]?after[":\s]+(\d+\.?\d*)/i);
|
|
46
|
+
return { _html: true, _raw: raw.slice(0, 200), _retryAfter: retryMatch ? parseFloat(retryMatch[1]) : null };
|
|
47
|
+
}
|
|
48
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
49
|
+
return { _invalid: true, _raw: raw.slice(0, 200) };
|
|
50
|
+
}
|
|
51
|
+
try { return JSON.parse(raw); } catch { return { _parseError: true, _raw: raw.slice(0, 200) }; }
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const _getSession = (type = 'canary', tlsType = 'a') => {
|
|
55
|
+
const key = `${type}_${tlsType}`;
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
if (_sessions[type] && !_sessions[type].destroyed && !_sessions[type].closed) return resolve(_sessions[type]);
|
|
58
|
+
const session = http2.connect(`https://${HOSTS[type]}`, {
|
|
59
|
+
settings: { enablePush: false },
|
|
60
|
+
timeout: 15000,
|
|
61
|
+
createConnection: (url, options) => tls.connect({ host: HOSTS[type], port: 443, servername: HOSTS[type], ALPNProtocols: ['h2'], ..._tlsOpts[tlsType] }),
|
|
62
|
+
..._tlsOpts[tlsType]
|
|
35
63
|
});
|
|
64
|
+
session.on('error', (err) => { _sessions[type] = null; reject(err); });
|
|
65
|
+
session.on('close', () => { _sessions[type] = null; });
|
|
66
|
+
session.on('connect', () => { _sessions[type] = session; resolve(session); });
|
|
67
|
+
session.setTimeout(15000, () => { session.destroy(); _sessions[type] = null; reject(new Error('H2_TIMEOUT')); });
|
|
36
68
|
});
|
|
37
|
-
r.on('error', () => resolve({ b: _db, v: _dv }));
|
|
38
|
-
r.on('timeout', () => { r.destroy(); resolve({ b: _db, v: _dv }); });
|
|
39
|
-
r.end();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const _g = async (force = false) => {
|
|
43
|
-
if (!force && _h && _init) return _h;
|
|
44
|
-
const i = await _f(), l = _u(), s = _u(), g = _u();
|
|
45
|
-
const ua = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) discord/${i.v} Chrome/${_dc} Electron/${_de} Safari/537.36`;
|
|
46
|
-
const sp = { os: "Windows", browser: "Discord Client", release_channel: "canary", client_version: i.v, os_version: "10.0.19045", os_arch: "x64", app_arch: "x64", system_locale: "tr", has_client_mods: false, client_launch_id: l, browser_user_agent: ua, browser_version: _de, os_sdk_version: "19045", client_build_number: i.b, native_build_number: _dn, client_event_source: null, launch_signature: g, client_heartbeat_session_id: s, client_app_state: "focused" };
|
|
47
|
-
_h = { "Content-Type": "application/json", "User-Agent": ua, "X-Super-Properties": Buffer.from(JSON.stringify(sp)).toString('base64') };
|
|
48
|
-
_init = true;
|
|
49
|
-
return _h;
|
|
50
69
|
};
|
|
51
70
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const _closeSessions = () => {
|
|
72
|
+
for (const type of ['canary', 'stable']) {
|
|
73
|
+
if (_sessions[type] && !_sessions[type].destroyed) { _sessions[type].destroy(); _sessions[type] = null; }
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const _g = async (type = 'canary', force = false) => {
|
|
78
|
+
if (!force && _h[type] && _init[type]) return _h[type];
|
|
79
|
+
const build = _builds[type];
|
|
80
|
+
const l = _u(), s = _u(), g = _u();
|
|
81
|
+
const ua = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) discord/${build.v} Chrome/${_dc} Electron/${_de} Safari/537.36`;
|
|
82
|
+
const sp = { os: "Windows", browser: "Discord Client", release_channel: build.ch, client_version: build.v, os_version: "10.0.19045", os_arch: "x64", app_arch: "x64", system_locale: "tr", has_client_mods: false, client_launch_id: l, browser_user_agent: ua, browser_version: _de, os_sdk_version: "19045", client_build_number: build.b, native_build_number: build.n, client_event_source: null, launch_signature: g, client_heartbeat_session_id: s, client_app_state: "focused" };
|
|
83
|
+
_h[type] = {
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
"origin": `https://${HOSTS[type]}`,
|
|
86
|
+
"referer": `https://${HOSTS[type]}/channels/@me`,
|
|
87
|
+
"sec-fetch-dest": "empty",
|
|
88
|
+
"sec-fetch-mode": "cors",
|
|
89
|
+
"sec-fetch-site": "same-origin",
|
|
90
|
+
"user-agent": ua,
|
|
91
|
+
"x-debug-options": "bugReporterEnabled",
|
|
92
|
+
"x-discord-locale": "tr",
|
|
93
|
+
"x-discord-timezone": "Europe/Istanbul",
|
|
94
|
+
"x-installation-id": _getInstallId(),
|
|
95
|
+
"x-super-properties": Buffer.from(JSON.stringify(sp)).toString('base64')
|
|
96
|
+
};
|
|
97
|
+
_init[type] = true;
|
|
98
|
+
return _h[type];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const _r = async (path, method, body, token, type = 'canary', tlsRetry = 0) => {
|
|
102
|
+
const tlsTypes = ['a', 'c', 'b'];
|
|
103
|
+
const tlsType = tlsTypes[Math.min(tlsRetry, 2)];
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const h = await _g(type);
|
|
107
|
+
const session = await _getSession(type, tlsType);
|
|
108
|
+
|
|
109
|
+
if (session.destroyed || session.closed) {
|
|
110
|
+
_sessions[type] = null;
|
|
111
|
+
return _r(path, method, body, token, type, tlsRetry);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return await new Promise((res, rej) => {
|
|
115
|
+
const req = session.request({ ":method": method, ":path": path, ":authority": HOSTS[type], "authorization": token, ...h });
|
|
116
|
+
req.setTimeout(10000, () => { req.destroy(); rej(new Error("H2_REQUEST_TIMEOUT")); });
|
|
117
|
+
const chunks = [];
|
|
118
|
+
let status = 0;
|
|
119
|
+
req.on('response', (headers) => { status = headers[':status']; });
|
|
120
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
121
|
+
req.on('end', () => {
|
|
122
|
+
const raw = Buffer.concat(chunks).toString();
|
|
123
|
+
const j = _safeJson(raw);
|
|
124
|
+
j._status = status;
|
|
125
|
+
j._host = type;
|
|
126
|
+
|
|
127
|
+
if (j._html || j._invalid || j._parseError) {
|
|
128
|
+
const retryAfter = j._retryAfter;
|
|
129
|
+
if (status === 429 || (j._raw && (j._raw.toLowerCase().includes('rate') || j._raw.toLowerCase().includes('cloudflare') || j._raw.toLowerCase().includes('1015')))) {
|
|
130
|
+
const cooldown = retryAfter && retryAfter > 0 ? retryAfter * 1000 : IP_RATE_COOLDOWN;
|
|
131
|
+
_ipRateUntil = Date.now() + cooldown;
|
|
132
|
+
return res({ _rateLimited: true, _retryAfter: retryAfter || (IP_RATE_COOLDOWN / 1000), _cooldown: Math.ceil(cooldown / 1000), _status: status, _host: type, _raw: j._raw });
|
|
67
133
|
}
|
|
68
|
-
|
|
69
|
-
return rej(new Error(`PARSE_ERROR:${rs.statusCode}`));
|
|
134
|
+
return res({ _error: true, _status: status, _host: type, _raw: j._raw });
|
|
70
135
|
}
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
136
|
+
|
|
137
|
+
if (j.code === 1015 || (j.message && j.message.includes('1015'))) {
|
|
138
|
+
const retryAfter = j.retry_after || j._retryAfter;
|
|
139
|
+
const cooldown = retryAfter && retryAfter > 0 ? retryAfter * 1000 : IP_RATE_COOLDOWN;
|
|
140
|
+
_ipRateUntil = Date.now() + cooldown;
|
|
141
|
+
return res({ _rateLimited: true, _retryAfter: retryAfter || (IP_RATE_COOLDOWN / 1000), _cooldown: Math.ceil(cooldown / 1000), _status: status, _host: type });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (status === 429) {
|
|
145
|
+
const retryAfter = j.retry_after || 5;
|
|
146
|
+
j._rateLimited = true;
|
|
147
|
+
j._retryAfter = retryAfter;
|
|
148
|
+
if (retryAfter > 60 || j.global) {
|
|
149
|
+
const cooldown = retryAfter * 1000;
|
|
150
|
+
_ipRateUntil = Date.now() + cooldown;
|
|
151
|
+
j._cooldown = Math.ceil(cooldown / 1000);
|
|
79
152
|
}
|
|
80
|
-
j._retryAfter = ra;
|
|
81
153
|
}
|
|
82
|
-
|
|
154
|
+
|
|
155
|
+
if (status === 403 && tlsRetry < 2) {
|
|
156
|
+
if (_sessions[type]) { _sessions[type].destroy(); _sessions[type] = null; }
|
|
157
|
+
return _r(path, method, body, token, type, tlsRetry + 1).then(res).catch(rej);
|
|
158
|
+
}
|
|
159
|
+
res(j);
|
|
83
160
|
});
|
|
161
|
+
req.on('error', (e) => rej(e));
|
|
162
|
+
if (body) req.end(body); else req.end();
|
|
84
163
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err.code === 'ERR_HTTP2_INVALID_SESSION' || err.code === 'ERR_HTTP2_STREAM_CANCEL' || err.code === 'ERR_HTTP2_GOAWAY_SESSION' || err.message.includes('session') || err.message.includes('closed')) {
|
|
166
|
+
if (_sessions[type]) { _sessions[type].destroy(); _sessions[type] = null; }
|
|
167
|
+
if (tlsRetry < 3) return _r(path, method, body, token, type, tlsRetry);
|
|
168
|
+
}
|
|
169
|
+
if (tlsRetry < 2 && (err.code === 'ERR_SSL_WRONG_VERSION_NUMBER' || err.code === 'ECONNRESET' || err.code === 'ERR_HTTP2_ERROR' || err.message.includes('TLS'))) {
|
|
170
|
+
if (_sessions[type]) { _sessions[type].destroy(); _sessions[type] = null; }
|
|
171
|
+
return _r(path, method, body, token, type, tlsRetry + 1);
|
|
172
|
+
}
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const _request = async (path, method, body, token, retry = 0) => {
|
|
178
|
+
try {
|
|
179
|
+
const res = await _r(path, method, body, token, 'canary');
|
|
180
|
+
if (res._rateLimited && retry < 1) {
|
|
181
|
+
if (_sessions.canary) { _sessions.canary.destroy(); _sessions.canary = null; }
|
|
182
|
+
try {
|
|
183
|
+
const stableRes = await _r(path, method, body, token, 'stable');
|
|
184
|
+
if (stableRes._rateLimited) {
|
|
185
|
+
const cooldown = stableRes._retryAfter && stableRes._retryAfter > 0 ? stableRes._retryAfter * 1000 : IP_RATE_COOLDOWN;
|
|
186
|
+
_ipRateUntil = Date.now() + cooldown;
|
|
187
|
+
stableRes._cooldown = Math.ceil(cooldown / 1000);
|
|
188
|
+
return stableRes;
|
|
189
|
+
}
|
|
190
|
+
return stableRes;
|
|
191
|
+
} catch (stableErr) {
|
|
192
|
+
_ipRateUntil = Date.now() + IP_RATE_COOLDOWN;
|
|
193
|
+
return { _rateLimited: true, _error: true, _cooldown: IP_RATE_COOLDOWN / 1000, message: stableErr.message };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return res;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (retry < 1) {
|
|
199
|
+
if (_sessions.canary) { _sessions.canary.destroy(); _sessions.canary = null; }
|
|
200
|
+
try { return await _r(path, method, body, token, 'stable'); } catch (stableErr) { throw stableErr; }
|
|
201
|
+
}
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
89
204
|
};
|
|
90
205
|
|
|
91
206
|
const get = (token, password, cb, retry = 0) => {
|
|
92
207
|
const p = (async () => {
|
|
93
|
-
if (isRateLimited()) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (tkRes?._status === 429 && tkRes?._retryAfter && retry < 3) {
|
|
100
|
-
await _sl((tkRes._retryAfter * 1000) + 100);
|
|
101
|
-
return get(token, password, undefined, retry + 1);
|
|
208
|
+
if (isRateLimited()) { const rem = getRateLimitRemaining(); throw new Error(`IP_RATE_LIMITED:${rem}s remaining`); }
|
|
209
|
+
const tkRes = await _request("/api/v9/guilds/0/vanity-url", "PATCH", '{"code":""}', token);
|
|
210
|
+
if (tkRes?._rateLimited) {
|
|
211
|
+
const cooldown = tkRes._cooldown || (tkRes._retryAfter && tkRes._retryAfter > 0 ? tkRes._retryAfter : IP_RATE_COOLDOWN / 1000);
|
|
212
|
+
if (tkRes._retryAfter && tkRes._retryAfter < 60 && retry < 3) { await _sl((tkRes._retryAfter * 1000) + 100); return get(token, password, undefined, retry + 1); }
|
|
213
|
+
throw new Error(`IP_RATE_LIMITED:${Math.ceil(cooldown)}s cooldown`);
|
|
102
214
|
}
|
|
103
|
-
if (tkRes?.
|
|
215
|
+
if (tkRes?._error) throw new Error(`REQUEST_ERROR:${tkRes._status}:${tkRes._raw || 'unknown'}`);
|
|
104
216
|
if (tkRes?.code === 0 || tkRes?.message === "401: Unauthorized") throw new Error("UNAUTHORIZED");
|
|
105
217
|
const tk = tkRes?.mfa?.ticket;
|
|
106
218
|
if (!tk) throw new Error(tkRes?.message || "No ticket");
|
|
107
|
-
const r = await
|
|
108
|
-
if (r?.
|
|
109
|
-
|
|
110
|
-
await _sl((r._retryAfter * 1000) + 100);
|
|
111
|
-
|
|
219
|
+
const r = await _request("/api/v9/mfa/finish", "POST", `{"ticket":"${tk}","mfa_type":"password","data":"${password}"}`, token);
|
|
220
|
+
if (r?._rateLimited) {
|
|
221
|
+
const cooldown = r._cooldown || (r._retryAfter && r._retryAfter > 0 ? r._retryAfter : IP_RATE_COOLDOWN / 1000);
|
|
222
|
+
if (r._retryAfter && r._retryAfter < 60 && retry < 3) { await _sl((r._retryAfter * 1000) + 100); return get(token, password, undefined, retry + 1); }
|
|
223
|
+
throw new Error(`IP_RATE_LIMITED:${Math.ceil(cooldown)}s cooldown`);
|
|
112
224
|
}
|
|
113
|
-
if (r?.
|
|
225
|
+
if (r?._error) throw new Error(`REQUEST_ERROR:${r._status}:${r._raw || 'unknown'}`);
|
|
114
226
|
if (r?.code === 60008 && retry < 3) { await _sl(5000); return get(token, password, undefined, retry + 1); }
|
|
115
|
-
if (!r?.token) throw new Error(r?.code === 60008 ? "
|
|
227
|
+
if (!r?.token) throw new Error(r?.code === 60008 ? "MFA_FAILED:password_wrong_or_token_ratelimited_or_patched" : r?.code === 50035 ? "TOKEN_INVALID" : r?.code === 50014 ? "UNAUTHORIZED" : r?.message || "No token");
|
|
116
228
|
return r.token;
|
|
117
229
|
})();
|
|
118
230
|
if (typeof cb === 'function') { p.then(t => cb(null, t)).catch(e => cb(e, null)); return; }
|
|
119
231
|
return p;
|
|
120
232
|
};
|
|
121
233
|
|
|
122
|
-
const refreshHeaders = () => _g(true);
|
|
123
|
-
const getHeaders = () => _h;
|
|
234
|
+
const refreshHeaders = (type = 'canary') => _g(type, true);
|
|
235
|
+
const getHeaders = (type = 'canary') => _h[type];
|
|
236
|
+
const getInstallationId = () => _getInstallId();
|
|
237
|
+
const setInstallationId = (id) => { _installId = id; if (_h.canary) _h.canary["x-installation-id"] = id; if (_h.stable) _h.stable["x-installation-id"] = id; };
|
|
238
|
+
const generateInstallationId = () => _genInstallId();
|
|
239
|
+
const closeSessions = _closeSessions;
|
|
124
240
|
|
|
125
|
-
module.exports = { get, refreshHeaders, getHeaders, isRateLimited, getRateLimitRemaining, clearRateLimit, setRateLimit };
|
|
241
|
+
module.exports = { get, refreshHeaders, getHeaders, isRateLimited, getRateLimitRemaining, clearRateLimit, setRateLimit, getInstallationId, setInstallationId, generateInstallationId, closeSessions };
|
|
126
242
|
module.exports.default = module.exports;
|
|
127
243
|
module.exports.get = get;
|
|
128
244
|
module.exports.refreshHeaders = refreshHeaders;
|
|
@@ -131,3 +247,7 @@ module.exports.isRateLimited = isRateLimited;
|
|
|
131
247
|
module.exports.getRateLimitRemaining = getRateLimitRemaining;
|
|
132
248
|
module.exports.clearRateLimit = clearRateLimit;
|
|
133
249
|
module.exports.setRateLimit = setRateLimit;
|
|
250
|
+
module.exports.getInstallationId = getInstallationId;
|
|
251
|
+
module.exports.setInstallationId = setInstallationId;
|
|
252
|
+
module.exports.generateInstallationId = generateInstallationId;
|
|
253
|
+
module.exports.closeSessions = closeSessions;
|
package/index.mjs
CHANGED
|
@@ -6,4 +6,8 @@ export const isRateLimited = mfa.isRateLimited;
|
|
|
6
6
|
export const getRateLimitRemaining = mfa.getRateLimitRemaining;
|
|
7
7
|
export const clearRateLimit = mfa.clearRateLimit;
|
|
8
8
|
export const setRateLimit = mfa.setRateLimit;
|
|
9
|
+
export const getInstallationId = mfa.getInstallationId;
|
|
10
|
+
export const setInstallationId = mfa.setInstallationId;
|
|
11
|
+
export const generateInstallationId = mfa.generateInstallationId;
|
|
12
|
+
export const closeSessions = mfa.closeSessions;
|
|
9
13
|
export default mfa;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rush-mfa",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Discord MFA token generator with auto-updating headers, TLS fallback and IP rate limit handling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
7
7
|
"exports": {
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
"node": ">=14.0.0"
|
|
19
19
|
},
|
|
20
20
|
"files": ["index.js", "index.mjs", "README.md", "LICENSE"]
|
|
21
|
-
}
|
|
21
|
+
}
|