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.
Files changed (4) hide show
  1. package/README.md +84 -12
  2. package/index.js +203 -83
  3. package/index.mjs +4 -0
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # rush-mfa
2
2
 
3
- Ultra-fast Discord MFA token generator with auto-updating headers, TLS fallback and IP rate limit handling.
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
- - 🔄 **Auto-updating Headers** - Fetches latest Discord build numbers automatically
10
- - 🛡️ **TLS Fallback** - Falls back from TLS 1.3auto → TLS 1.2 if Discord fixes
9
+ - 🌐 **HTTP/2 Protocol** - Faster multiplexed connections
10
+ - 🔄 **Host Fallback** - canary.discord.comdiscord.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 15min cooldown on IP rate limit (429)
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
- - `RATE_LIMITED` - MFA endpoint rate limited (auto-retried 3 times)
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. **429 with retry_after > 60s or global** → 15 minute cooldown activated
145
- 3. **Subsequent calls during cooldown** → Immediately rejected with `IP_RATE_LIMITED`
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
- ## TLS Fallback
202
+ The library uses HTTP/2 with automatic host fallback:
148
203
 
149
- The library automatically handles TLS version issues:
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
- 1. First tries **TLS 1.3** (Discord's current requirement)
152
- 2. If 403 or connection error → falls back to **auto** (TLS 1.2-1.3)
153
- 3. If still failing falls back to **TLS 1.2**
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 https = require("node:https");
3
+ const http2 = require("node:http2");
4
4
  const crypto = require("node:crypto");
5
+ const tls = require("node:tls");
5
6
 
6
- const _a = {
7
- a: new https.Agent({ minVersion: 'TLSv1.3', maxVersion: 'TLSv1.3', honorCipherOrder: true, rejectUnauthorized: true, keepAlive: true }),
8
- b: new https.Agent({ minVersion: 'TLSv1.2', maxVersion: 'TLSv1.2', honorCipherOrder: true, rejectUnauthorized: true, keepAlive: true }),
9
- c: new https.Agent({ minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3', honorCipherOrder: true, rejectUnauthorized: true, keepAlive: true })
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 _h = null, _init = false;
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
- const _db = 483853, _dn = 73726, _dv = "1.0.800", _de = "37.6.0", _dc = "138.0.7204.251";
15
- const IP_RATE_COOLDOWN = 15 * 60 * 1000;
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 = 900) => { _ipRateUntil = Date.now() + (seconds * 1000); };
39
+ const setRateLimit = (seconds = 1800) => { _ipRateUntil = Date.now() + (seconds * 1000); };
24
40
 
25
- const _f = () => new Promise((resolve) => {
26
- const r = https.request({ hostname: 'canary.discord.com', port: 443, path: '/app', method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0' }, agent: _a.c, timeout: 5000 }, (rs) => {
27
- let d = '';
28
- rs.on('data', c => d += c);
29
- rs.on('end', () => {
30
- try {
31
- const b = d.match(/build_number["\s:]+(\d+)/i) || d.match(/"buildNumber":(\d+)/i) || d.match(/client_build_number["\s:]+(\d+)/i);
32
- const v = d.match(/discord\/(\d+\.\d+\.\d+)/i) || d.match(/client_version["\s:]+["']?(\d+\.\d+\.\d+)/i);
33
- resolve({ b: b ? parseInt(b[1]) : _db, v: v ? v[1] : _dv });
34
- } catch { resolve({ b: _db, v: _dv }); }
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 _r = async (p, m, b, t, c = 0) => {
53
- const h = await _g(), ao = ['a', 'c', 'b'], ag = _a[ao[Math.min(c, 2)]];
54
- return new Promise((res, rej) => {
55
- const r = https.request({ hostname: "canary.discord.com", port: 443, path: p, method: m, headers: { Authorization: t, ...h }, agent: ag, timeout: 10000 }, rs => {
56
- const d = [];
57
- rs.on("data", x => d.push(x));
58
- rs.on("end", () => {
59
- const raw = Buffer.concat(d).toString();
60
- let j;
61
- try {
62
- j = JSON.parse(raw || "{}");
63
- } catch {
64
- if (rs.statusCode === 429 || raw.includes('rate') || raw.includes('Rate') || raw.includes('error')) {
65
- _ipRateUntil = Date.now() + IP_RATE_COOLDOWN;
66
- return rej(new Error(`IP_RATE_LIMITED:${IP_RATE_COOLDOWN / 1000}s cooldown`));
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
- if (rs.statusCode === 403 && c < 2) return _r(p, m, b, t, c + 1).then(res).catch(rej);
69
- return rej(new Error(`PARSE_ERROR:${rs.statusCode}`));
134
+ return res({ _error: true, _status: status, _host: type, _raw: j._raw });
70
135
  }
71
- j._status = rs.statusCode;
72
- if (rs.statusCode === 403 && c < 2) return _r(p, m, b, t, c + 1).then(res).catch(rej);
73
- if (rs.statusCode === 429) {
74
- const ra = j.retry_after || parseFloat(rs.headers['retry-after']) || 5;
75
- if (ra > 60 || j.global) {
76
- _ipRateUntil = Date.now() + IP_RATE_COOLDOWN;
77
- j._ipRate = true;
78
- j._cooldown = IP_RATE_COOLDOWN / 1000;
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
- res(j);
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
- r.on("error", (e) => { if (c < 2 && (e.code === 'ERR_SSL_WRONG_VERSION_NUMBER' || e.code === 'ECONNRESET')) return _r(p, m, b, t, c + 1).then(res).catch(rej); rej(e); });
86
- r.on("timeout", () => { r.destroy(); if (c < 2) return _r(p, m, b, t, c + 1).then(res).catch(rej); rej(new Error("timeout")); });
87
- r.end(b);
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
- const rem = getRateLimitRemaining();
95
- throw new Error(`IP_RATE_LIMITED:${rem}s remaining`);
96
- }
97
- const tkRes = await _r("/api/v9/guilds/0/vanity-url", "PATCH", '{"code":""}', token);
98
- if (tkRes?._ipRate) throw new Error(`IP_RATE_LIMITED:${tkRes._cooldown}s cooldown`);
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?.retry_after && retry < 3) { await _sl((tkRes.retry_after * 1000) + 100); return get(token, password, undefined, retry + 1); }
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 _r("/api/v9/mfa/finish", "POST", `{"ticket":"${tk}","mfa_type":"password","data":"${password}"}`, token);
108
- if (r?._ipRate) throw new Error(`IP_RATE_LIMITED:${r._cooldown}s cooldown`);
109
- if (r?._status === 429 && r?._retryAfter && retry < 3) {
110
- await _sl((r._retryAfter * 1000) + 100);
111
- return get(token, password, undefined, retry + 1);
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?.retry_after && retry < 3) { await _sl((r.retry_after * 1000) + 100); return get(token, password, undefined, retry + 1); }
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 ? "RATE_LIMITED" : r?.code === 50035 ? "TOKEN_INVALID" : r?.code === 50014 ? "UNAUTHORIZED" : r?.message || "No token");
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.7",
4
- "description": "Ultra-fast Discord MFA token generator with auto-updating headers, TLS fallback and IP rate limit handling",
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
+ }