@the-convocation/twitter-scraper 0.21.1 → 0.22.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.
@@ -49,13 +49,13 @@ class AuthenticationError extends Error {
49
49
  }
50
50
  }
51
51
 
52
- const log$6 = debug("twitter-scraper:rate-limit");
52
+ const log$8 = debug("twitter-scraper:rate-limit");
53
53
  class WaitingRateLimitStrategy {
54
54
  async onRateLimit({ response: res }) {
55
55
  const xRateLimitLimit = res.headers.get("x-rate-limit-limit");
56
56
  const xRateLimitRemaining = res.headers.get("x-rate-limit-remaining");
57
57
  const xRateLimitReset = res.headers.get("x-rate-limit-reset");
58
- log$6(
58
+ log$8(
59
59
  `Rate limit event: limit=${xRateLimitLimit}, remaining=${xRateLimitRemaining}, reset=${xRateLimitReset}`
60
60
  );
61
61
  if (xRateLimitRemaining == "0" && xRateLimitReset) {
@@ -71,7 +71,794 @@ class ErrorRateLimitStrategy {
71
71
  }
72
72
  }
73
73
 
74
- const log$5 = debug("twitter-scraper:requests");
74
+ const log$7 = debug("twitter-scraper:castle");
75
+ var FieldEncoding = /* @__PURE__ */ ((FieldEncoding2) => {
76
+ FieldEncoding2[FieldEncoding2["Empty"] = -1] = "Empty";
77
+ FieldEncoding2[FieldEncoding2["Marker"] = 1] = "Marker";
78
+ FieldEncoding2[FieldEncoding2["Byte"] = 3] = "Byte";
79
+ FieldEncoding2[FieldEncoding2["EncryptedBytes"] = 4] = "EncryptedBytes";
80
+ FieldEncoding2[FieldEncoding2["CompactInt"] = 5] = "CompactInt";
81
+ FieldEncoding2[FieldEncoding2["RoundedByte"] = 6] = "RoundedByte";
82
+ FieldEncoding2[FieldEncoding2["RawAppend"] = 7] = "RawAppend";
83
+ return FieldEncoding2;
84
+ })(FieldEncoding || {});
85
+ const TWITTER_CASTLE_PK = "AvRa79bHyJSYSQHnRpcVtzyxetSvFerx";
86
+ const XXTEA_KEY = [1164413191, 3891440048, 185273099, 2746598870];
87
+ const PER_FIELD_KEY_TAIL = [
88
+ 16373134,
89
+ 643144773,
90
+ 1762804430,
91
+ 1186572681,
92
+ 1164413191
93
+ ];
94
+ const TS_EPOCH = 1535e6;
95
+ const SDK_VERSION = 27008;
96
+ const TOKEN_VERSION = 11;
97
+ const FP_PART = {
98
+ DEVICE: 0,
99
+ // Part 1: hardware/OS/rendering fingerprint
100
+ BROWSER: 4,
101
+ // Part 2: browser environment fingerprint
102
+ TIMING: 7
103
+ // Part 3: timing-based fingerprint
104
+ };
105
+ const DEFAULT_PROFILE = {
106
+ locale: "en-US",
107
+ language: "en",
108
+ timezone: "America/New_York",
109
+ screenWidth: 1920,
110
+ screenHeight: 1080,
111
+ availableWidth: 1920,
112
+ availableHeight: 1032,
113
+ // 1080 minus Windows taskbar (~48px)
114
+ gpuRenderer: "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti Direct3D11 vs_5_0 ps_5_0, D3D11)",
115
+ deviceMemoryGB: 8,
116
+ hardwareConcurrency: 24,
117
+ colorDepth: 24,
118
+ devicePixelRatio: 1
119
+ };
120
+ const SCREEN_RESOLUTIONS = [
121
+ { w: 1920, h: 1080, ah: 1032 },
122
+ { w: 2560, h: 1440, ah: 1392 },
123
+ { w: 1366, h: 768, ah: 720 },
124
+ { w: 1536, h: 864, ah: 816 },
125
+ { w: 1440, h: 900, ah: 852 },
126
+ { w: 1680, h: 1050, ah: 1002 },
127
+ { w: 3840, h: 2160, ah: 2112 }
128
+ ];
129
+ const DEVICE_MEMORY_VALUES = [4, 8, 8, 16];
130
+ const HARDWARE_CONCURRENCY_VALUES = [4, 8, 8, 12, 16, 24];
131
+ function randomizeBrowserProfile() {
132
+ const screen = SCREEN_RESOLUTIONS[randInt(0, SCREEN_RESOLUTIONS.length - 1)];
133
+ return {
134
+ ...DEFAULT_PROFILE,
135
+ screenWidth: screen.w,
136
+ screenHeight: screen.h,
137
+ availableWidth: screen.w,
138
+ availableHeight: screen.ah,
139
+ // gpuRenderer intentionally NOT randomized — see JSDoc above
140
+ deviceMemoryGB: DEVICE_MEMORY_VALUES[randInt(0, DEVICE_MEMORY_VALUES.length - 1)],
141
+ hardwareConcurrency: HARDWARE_CONCURRENCY_VALUES[randInt(0, HARDWARE_CONCURRENCY_VALUES.length - 1)]
142
+ };
143
+ }
144
+ function getRandomBytes(n) {
145
+ const buf = new Uint8Array(n);
146
+ if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.getRandomValues) {
147
+ globalThis.crypto.getRandomValues(buf);
148
+ } else {
149
+ for (let i = 0; i < n; i++) buf[i] = Math.floor(Math.random() * 256);
150
+ }
151
+ return buf;
152
+ }
153
+ function randInt(min, max) {
154
+ return min + Math.floor(Math.random() * (max - min + 1));
155
+ }
156
+ function randFloat(min, max) {
157
+ return min + Math.random() * (max - min);
158
+ }
159
+ function concat(...arrays) {
160
+ const len = arrays.reduce((s, a) => s + a.length, 0);
161
+ const out = new Uint8Array(len);
162
+ let off = 0;
163
+ for (const a of arrays) {
164
+ out.set(a, off);
165
+ off += a.length;
166
+ }
167
+ return out;
168
+ }
169
+ function toHex(input) {
170
+ return Array.from(input).map((b) => b.toString(16).padStart(2, "0")).join("");
171
+ }
172
+ function fromHex(hex) {
173
+ const out = new Uint8Array(hex.length / 2);
174
+ for (let i = 0; i < hex.length; i += 2)
175
+ out[i / 2] = parseInt(hex.substring(i, i + 2), 16);
176
+ return out;
177
+ }
178
+ function textEnc(s) {
179
+ return new TextEncoder().encode(s);
180
+ }
181
+ function u8(...vals) {
182
+ return new Uint8Array(vals);
183
+ }
184
+ function be16(v) {
185
+ return u8(v >>> 8 & 255, v & 255);
186
+ }
187
+ function be32(v) {
188
+ return u8(v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255);
189
+ }
190
+ function xorBytes(data, key) {
191
+ const out = new Uint8Array(data.length);
192
+ for (let i = 0; i < data.length; i++) out[i] = data[i] ^ key[i % key.length];
193
+ return out;
194
+ }
195
+ function xorNibbles(nibbles, keyNibble) {
196
+ const k = parseInt(keyNibble, 16);
197
+ return nibbles.split("").map((n) => (parseInt(n, 16) ^ k).toString(16)).join("");
198
+ }
199
+ function base64url(data) {
200
+ if (typeof Buffer !== "undefined") {
201
+ return Buffer.from(data).toString("base64url");
202
+ }
203
+ let bin = "";
204
+ for (let i = 0; i < data.length; i++) bin += String.fromCharCode(data[i]);
205
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
206
+ }
207
+ function xxteaEncrypt(data, key) {
208
+ const padLen = Math.ceil(data.length / 4) * 4;
209
+ const padded = new Uint8Array(padLen);
210
+ padded.set(data);
211
+ const n = padLen / 4;
212
+ const v = new Uint32Array(n);
213
+ for (let i = 0; i < n; i++) {
214
+ v[i] = (padded[i * 4] | padded[i * 4 + 1] << 8 | padded[i * 4 + 2] << 16 | padded[i * 4 + 3] << 24) >>> 0;
215
+ }
216
+ if (n <= 1) return padded;
217
+ const k = new Uint32Array(key.map((x) => x >>> 0));
218
+ const DELTA = 2654435769;
219
+ const u = n - 1;
220
+ let sum = 0;
221
+ let z = v[u];
222
+ let y;
223
+ let rounds = 6 + Math.floor(52 / (u + 1));
224
+ while (rounds-- > 0) {
225
+ sum = sum + DELTA >>> 0;
226
+ const e = sum >>> 2 & 3;
227
+ for (let p = 0; p < u; p++) {
228
+ y = v[p + 1];
229
+ const mx2 = ((z >>> 5 ^ y << 2) >>> 0) + ((y >>> 3 ^ z << 4) >>> 0) ^ ((sum ^ y) >>> 0) + ((k[p & 3 ^ e] ^ z) >>> 0);
230
+ v[p] = v[p] + mx2 >>> 0;
231
+ z = v[p];
232
+ }
233
+ y = v[0];
234
+ const mx = ((z >>> 5 ^ y << 2) >>> 0) + ((y >>> 3 ^ z << 4) >>> 0) ^ ((sum ^ y) >>> 0) + ((k[u & 3 ^ e] ^ z) >>> 0);
235
+ v[u] = v[u] + mx >>> 0;
236
+ z = v[u];
237
+ }
238
+ const out = new Uint8Array(n * 4);
239
+ for (let i = 0; i < n; i++) {
240
+ out[i * 4] = v[i] & 255;
241
+ out[i * 4 + 1] = v[i] >>> 8 & 255;
242
+ out[i * 4 + 2] = v[i] >>> 16 & 255;
243
+ out[i * 4 + 3] = v[i] >>> 24 & 255;
244
+ }
245
+ return out;
246
+ }
247
+ function fieldEncrypt(data, fieldIndex, initTime) {
248
+ return xxteaEncrypt(data, [
249
+ fieldIndex,
250
+ Math.floor(initTime),
251
+ ...PER_FIELD_KEY_TAIL
252
+ ]);
253
+ }
254
+ function encodeTimestampBytes(ms) {
255
+ let t = Math.floor(ms / 1e3 - TS_EPOCH);
256
+ t = Math.max(Math.min(t, 268435455), 0);
257
+ return be32(t);
258
+ }
259
+ function xorAndAppendKey(buf, key) {
260
+ const hex = toHex(buf);
261
+ const keyNib = (key & 15).toString(16);
262
+ return xorNibbles(hex.substring(1), keyNib) + keyNib;
263
+ }
264
+ function encodeTimestampEncrypted(ms) {
265
+ const tsBytes = encodeTimestampBytes(ms);
266
+ const slice = Math.floor(ms) % 1e3;
267
+ const sliceBytes = be16(slice);
268
+ const k = randInt(0, 15);
269
+ return xorAndAppendKey(tsBytes, k) + xorAndAppendKey(sliceBytes, k);
270
+ }
271
+ function deriveAndXor(keyHex, sliceLen, rotChar, data) {
272
+ const sub = keyHex.substring(0, sliceLen).split("");
273
+ if (sub.length === 0) return data;
274
+ const rot = parseInt(rotChar, 16) % sub.length;
275
+ const rotated = sub.slice(rot).concat(sub.slice(0, rot)).join("");
276
+ return xorBytes(data, fromHex(rotated));
277
+ }
278
+ function customFloatEncode(expBits, manBits, value) {
279
+ if (value === 0) return 0;
280
+ let n = Math.abs(value);
281
+ let exp = 0;
282
+ while (2 <= n) {
283
+ n /= 2;
284
+ exp++;
285
+ }
286
+ while (n < 1 && n > 0) {
287
+ n *= 2;
288
+ exp--;
289
+ }
290
+ exp = Math.min(exp, (1 << expBits) - 1);
291
+ const frac = n - Math.floor(n);
292
+ let mantissa = 0;
293
+ if (frac > 0) {
294
+ let pos = 1;
295
+ let tmp = frac;
296
+ while (tmp !== 0 && pos <= manBits) {
297
+ tmp *= 2;
298
+ const bit = Math.floor(tmp);
299
+ mantissa |= bit << manBits - pos;
300
+ tmp -= bit;
301
+ pos++;
302
+ }
303
+ }
304
+ return exp << manBits | mantissa;
305
+ }
306
+ function encodeFloatVal(v) {
307
+ const n = Math.max(v, 0);
308
+ if (n <= 15) return 64 | customFloatEncode(2, 4, n + 1);
309
+ return 128 | customFloatEncode(4, 3, n - 14);
310
+ }
311
+ function encodeField(index, encoding, val, initTime) {
312
+ const hdr = u8((31 & index) << 3 | 7 & encoding);
313
+ if (encoding === -1 /* Empty */ || encoding === 1 /* Marker */)
314
+ return hdr;
315
+ let body;
316
+ switch (encoding) {
317
+ case 3 /* Byte */:
318
+ body = u8(val);
319
+ break;
320
+ case 6 /* RoundedByte */:
321
+ body = u8(Math.round(val));
322
+ break;
323
+ case 5 /* CompactInt */: {
324
+ const v = val;
325
+ body = v <= 127 ? u8(v) : be16(1 << 15 | 32767 & v);
326
+ break;
327
+ }
328
+ case 4 /* EncryptedBytes */: {
329
+ if (initTime == null) {
330
+ throw new Error("initTime is required for EncryptedBytes encoding");
331
+ }
332
+ const enc = fieldEncrypt(val, index, initTime);
333
+ body = concat(u8(enc.length), enc);
334
+ break;
335
+ }
336
+ case 7 /* RawAppend */:
337
+ body = val instanceof Uint8Array ? val : u8(val);
338
+ break;
339
+ default:
340
+ body = new Uint8Array(0);
341
+ }
342
+ return concat(hdr, body);
343
+ }
344
+ function encodeBits(bits, byteSize) {
345
+ const numBytes = byteSize / 8;
346
+ const arr = new Uint8Array(numBytes);
347
+ for (const bit of bits) {
348
+ const bi = numBytes - 1 - Math.floor(bit / 8);
349
+ if (bi >= 0 && bi < numBytes) arr[bi] |= 1 << bit % 8;
350
+ }
351
+ return arr;
352
+ }
353
+ function screenDimBytes(screen, avail) {
354
+ const r = 32767 & screen;
355
+ const e = 65535 & avail;
356
+ return r === e ? be16(32768 | r) : concat(be16(r), be16(e));
357
+ }
358
+ function boolsToBin(arr, totalBits) {
359
+ const e = arr.length > totalBits ? arr.slice(0, totalBits) : arr;
360
+ const c = e.length;
361
+ let r = 0;
362
+ for (let i = c - 1; i >= 0; i--) {
363
+ if (e[i]) r |= 1 << c - i - 1;
364
+ }
365
+ if (c < totalBits) r <<= totalBits - c;
366
+ return r;
367
+ }
368
+ function encodeCodecPlayability() {
369
+ const codecs = {
370
+ webm: 2,
371
+ // VP8/VP9
372
+ mp4: 2,
373
+ // H.264
374
+ ogg: 0,
375
+ // Theora (Chrome dropped support)
376
+ aac: 2,
377
+ // AAC audio
378
+ xm4a: 1,
379
+ // M4A container
380
+ wav: 2,
381
+ // PCM audio
382
+ mpeg: 2,
383
+ // MP3 audio
384
+ ogg2: 2
385
+ // Vorbis audio
386
+ };
387
+ const bits = Object.values(codecs).map((c) => c.toString(2).padStart(2, "0")).join("");
388
+ return be16(parseInt(bits, 2));
389
+ }
390
+ const TIMEZONE_ENUM = {
391
+ "America/New_York": 0,
392
+ "America/Sao_Paulo": 1,
393
+ "America/Chicago": 2,
394
+ "America/Los_Angeles": 3,
395
+ "America/Mexico_City": 4,
396
+ "Asia/Shanghai": 5
397
+ };
398
+ function getTimezoneInfo(tz) {
399
+ const knownOffsets = {
400
+ "America/New_York": { offset: 20, dstDiff: 4 },
401
+ "America/Chicago": { offset: 24, dstDiff: 4 },
402
+ "America/Los_Angeles": { offset: 32, dstDiff: 4 },
403
+ "America/Denver": { offset: 28, dstDiff: 4 },
404
+ "America/Sao_Paulo": { offset: 12, dstDiff: 4 },
405
+ "America/Mexico_City": { offset: 24, dstDiff: 4 },
406
+ "Asia/Shanghai": { offset: 246, dstDiff: 0 },
407
+ "Asia/Tokyo": { offset: 220, dstDiff: 0 },
408
+ "Europe/London": { offset: 0, dstDiff: 4 },
409
+ "Europe/Berlin": { offset: 252, dstDiff: 4 },
410
+ UTC: { offset: 0, dstDiff: 0 }
411
+ };
412
+ try {
413
+ const now = /* @__PURE__ */ new Date();
414
+ const jan = new Date(now.getFullYear(), 0, 1);
415
+ const jul = new Date(now.getFullYear(), 6, 1);
416
+ const getOffset = (date, zone) => {
417
+ const utc = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
418
+ const local = new Date(date.toLocaleString("en-US", { timeZone: zone }));
419
+ return (utc.getTime() - local.getTime()) / 6e4;
420
+ };
421
+ const currentOffset = getOffset(now, tz);
422
+ const janOffset = getOffset(jan, tz);
423
+ const julOffset = getOffset(jul, tz);
424
+ const dstDifference = Math.abs(janOffset - julOffset);
425
+ return {
426
+ offset: Math.floor(currentOffset / 15) & 255,
427
+ dstDiff: Math.floor(dstDifference / 15) & 255
428
+ };
429
+ } catch {
430
+ return knownOffsets[tz] || { offset: 20, dstDiff: 4 };
431
+ }
432
+ }
433
+ function buildDeviceFingerprint(initTime, profile, userAgent) {
434
+ const tz = getTimezoneInfo(profile.timezone);
435
+ const { Byte, EncryptedBytes, CompactInt, RoundedByte, RawAppend } = FieldEncoding;
436
+ const encryptedUA = fieldEncrypt(textEnc(userAgent), 12, initTime);
437
+ const uaPayload = concat(u8(1), u8(encryptedUA.length), encryptedUA);
438
+ const fields = [
439
+ encodeField(0, Byte, 1),
440
+ // Platform: Win32
441
+ encodeField(1, Byte, 0),
442
+ // Vendor: Google Inc.
443
+ encodeField(2, EncryptedBytes, textEnc(profile.locale), initTime),
444
+ // Locale
445
+ encodeField(3, RoundedByte, profile.deviceMemoryGB * 10),
446
+ // Device memory (GB * 10)
447
+ encodeField(
448
+ 4,
449
+ RawAppend,
450
+ concat(
451
+ // Screen dimensions (width + height)
452
+ screenDimBytes(profile.screenWidth, profile.availableWidth),
453
+ screenDimBytes(profile.screenHeight, profile.availableHeight)
454
+ )
455
+ ),
456
+ encodeField(5, CompactInt, profile.colorDepth),
457
+ // Screen color depth
458
+ encodeField(6, CompactInt, profile.hardwareConcurrency),
459
+ // CPU logical cores
460
+ encodeField(7, RoundedByte, profile.devicePixelRatio * 10),
461
+ // Pixel ratio (* 10)
462
+ encodeField(8, RawAppend, u8(tz.offset, tz.dstDiff)),
463
+ // Timezone offset info
464
+ // MIME type hash — captured from Chrome 144 on Windows 10.
465
+ // Source: yubie-re/castleio-gen (Python SDK, MIT license).
466
+ encodeField(9, RawAppend, u8(2, 125, 95, 201, 167)),
467
+ // Browser plugins hash — Chrome no longer exposes plugins to navigator.plugins,
468
+ // so this is a fixed hash. Source: yubie-re/castleio-gen (Python SDK, MIT license).
469
+ encodeField(10, RawAppend, u8(5, 114, 147, 2, 8)),
470
+ encodeField(
471
+ 11,
472
+ RawAppend,
473
+ // Browser feature flags
474
+ concat(u8(12), encodeBits([0, 1, 2, 3, 4, 5, 6], 16))
475
+ ),
476
+ encodeField(12, RawAppend, uaPayload),
477
+ // User agent (encrypted)
478
+ // Canvas font rendering hash — generated by Castle.io SDK's canvas fingerprinting (text rendering).
479
+ // Captured from Chrome 144 on Windows 10. Source: yubie-re/castleio-gen (Python SDK, MIT license).
480
+ encodeField(13, EncryptedBytes, textEnc("54b4b5cf"), initTime),
481
+ encodeField(
482
+ 14,
483
+ RawAppend,
484
+ // Media input devices
485
+ concat(u8(3), encodeBits([0, 1, 2], 8))
486
+ ),
487
+ // Fields 15 (DoNotTrack) and 16 (JavaEnabled) intentionally omitted
488
+ encodeField(17, Byte, 0),
489
+ // productSub type
490
+ // Canvas circle rendering hash — generated by Castle.io SDK's canvas fingerprinting (arc drawing).
491
+ // Captured from Chrome 144 on Windows 10. Source: yubie-re/castleio-gen (Python SDK, MIT license).
492
+ encodeField(18, EncryptedBytes, textEnc("c6749e76"), initTime),
493
+ encodeField(19, EncryptedBytes, textEnc(profile.gpuRenderer), initTime),
494
+ // WebGL renderer
495
+ encodeField(
496
+ 20,
497
+ EncryptedBytes,
498
+ // Epoch locale string
499
+ textEnc("12/31/1969, 7:00:00 PM"),
500
+ initTime
501
+ ),
502
+ encodeField(
503
+ 21,
504
+ RawAppend,
505
+ // WebDriver flags (none set)
506
+ concat(u8(8), encodeBits([], 8))
507
+ ),
508
+ encodeField(22, CompactInt, 33),
509
+ // eval.toString() length
510
+ // Field 23 (navigator.buildID) intentionally omitted (Chrome doesn't have it)
511
+ encodeField(24, CompactInt, 12549),
512
+ // Max recursion depth
513
+ encodeField(25, Byte, 0),
514
+ // Recursion error message type
515
+ encodeField(26, Byte, 1),
516
+ // Recursion error name type
517
+ encodeField(27, CompactInt, 4644),
518
+ // Stack trace string length
519
+ encodeField(28, RawAppend, u8(0)),
520
+ // Touch support metric
521
+ encodeField(29, Byte, 3),
522
+ // Undefined call error type
523
+ // Navigator properties hash — hash of enumerable navigator property names.
524
+ // Captured from Chrome 144 on Windows 10. Source: yubie-re/castleio-gen (Python SDK, MIT license).
525
+ encodeField(30, RawAppend, u8(93, 197, 171, 181, 136)),
526
+ encodeField(31, RawAppend, encodeCodecPlayability())
527
+ // Codec playability
528
+ ];
529
+ const data = concat(...fields);
530
+ const sizeIdx = (7 & FP_PART.DEVICE) << 5 | 31 & fields.length;
531
+ return concat(u8(sizeIdx), data);
532
+ }
533
+ function buildBrowserFingerprint(profile, initTime) {
534
+ const { Byte, EncryptedBytes, CompactInt, Marker, RawAppend } = FieldEncoding;
535
+ const timezoneField = profile.timezone in TIMEZONE_ENUM ? encodeField(1, Byte, TIMEZONE_ENUM[profile.timezone]) : encodeField(1, EncryptedBytes, textEnc(profile.timezone), initTime);
536
+ const fields = [
537
+ encodeField(0, Byte, 0),
538
+ // Constant marker
539
+ timezoneField,
540
+ // Timezone
541
+ encodeField(
542
+ 2,
543
+ EncryptedBytes,
544
+ // Language list
545
+ textEnc(`${profile.locale},${profile.language}`),
546
+ initTime
547
+ ),
548
+ encodeField(6, CompactInt, 0),
549
+ // Expected property count
550
+ encodeField(
551
+ 10,
552
+ RawAppend,
553
+ // Castle data bitfield
554
+ concat(u8(4), encodeBits([1, 2, 3], 8))
555
+ ),
556
+ encodeField(12, CompactInt, 80),
557
+ // Negative error string length
558
+ encodeField(13, RawAppend, u8(9, 0, 0)),
559
+ // Driver check values
560
+ encodeField(
561
+ 17,
562
+ RawAppend,
563
+ // Chrome feature flags
564
+ concat(u8(13), encodeBits([1, 5, 8, 9, 10], 16))
565
+ ),
566
+ encodeField(18, Marker, 0),
567
+ // Device logic expected
568
+ encodeField(21, RawAppend, u8(0, 0, 0, 0)),
569
+ // Class properties count
570
+ encodeField(22, EncryptedBytes, textEnc(profile.locale), initTime),
571
+ // User locale (secondary)
572
+ encodeField(
573
+ 23,
574
+ RawAppend,
575
+ // Worker capabilities
576
+ concat(u8(2), encodeBits([0], 8))
577
+ ),
578
+ encodeField(
579
+ 24,
580
+ RawAppend,
581
+ // Inner/outer dimension diff
582
+ concat(be16(0), be16(randInt(10, 30)))
583
+ )
584
+ ];
585
+ const data = concat(...fields);
586
+ const sizeIdx = (7 & FP_PART.BROWSER) << 5 | 31 & fields.length;
587
+ return concat(u8(sizeIdx), data);
588
+ }
589
+ function buildTimingFingerprint(initTime) {
590
+ const minute = new Date(initTime).getUTCMinutes();
591
+ const fields = [
592
+ encodeField(3, 5 /* CompactInt */, 1),
593
+ // Time since window.open (ms)
594
+ encodeField(4, 5 /* CompactInt */, minute)
595
+ // Castle init time (minutes)
596
+ ];
597
+ const data = concat(...fields);
598
+ const sizeIdx = (7 & FP_PART.TIMING) << 5 | 31 & fields.length;
599
+ return concat(u8(sizeIdx), data);
600
+ }
601
+ const EventType = {
602
+ CLICK: 0,
603
+ FOCUS: 5,
604
+ BLUR: 6,
605
+ ANIMATIONSTART: 18,
606
+ MOUSEMOVE: 21,
607
+ MOUSELEAVE: 25,
608
+ MOUSEENTER: 26,
609
+ RESIZE: 27
610
+ };
611
+ const HAS_TARGET_FLAG = 128;
612
+ const TARGET_UNKNOWN = 63;
613
+ function generateEventLog() {
614
+ const simpleEvents = [
615
+ EventType.MOUSEMOVE,
616
+ EventType.ANIMATIONSTART,
617
+ EventType.MOUSELEAVE,
618
+ EventType.MOUSEENTER,
619
+ EventType.RESIZE
620
+ ];
621
+ const targetedEvents = [
622
+ EventType.CLICK,
623
+ EventType.BLUR,
624
+ EventType.FOCUS
625
+ ];
626
+ const allEvents = [...simpleEvents, ...targetedEvents];
627
+ const count = randInt(30, 70);
628
+ const eventBytes = [];
629
+ for (let i = 0; i < count; i++) {
630
+ const eventId = allEvents[randInt(0, allEvents.length - 1)];
631
+ if (targetedEvents.includes(eventId)) {
632
+ eventBytes.push(eventId | HAS_TARGET_FLAG);
633
+ eventBytes.push(TARGET_UNKNOWN);
634
+ } else {
635
+ eventBytes.push(eventId);
636
+ }
637
+ }
638
+ const inner = concat(u8(0), be16(count), new Uint8Array(eventBytes));
639
+ return concat(be16(inner.length), inner);
640
+ }
641
+ function buildBehavioralBitfield() {
642
+ const flags = new Array(15).fill(false);
643
+ flags[2] = true;
644
+ flags[3] = true;
645
+ flags[5] = true;
646
+ flags[6] = true;
647
+ flags[9] = true;
648
+ flags[11] = true;
649
+ flags[12] = true;
650
+ const packedBits = boolsToBin(flags, 16);
651
+ const encoded = 6 << 20 | 2 << 16 | 65535 & packedBits;
652
+ return u8(encoded >>> 16 & 255, encoded >>> 8 & 255, encoded & 255);
653
+ }
654
+ const NO_DATA = -1;
655
+ function buildFloatMetrics() {
656
+ const metrics = [
657
+ // ── Mouse & key timing ──
658
+ randFloat(40, 50),
659
+ // 0: Mouse angle vector mean
660
+ NO_DATA,
661
+ // 1: Touch angle vector (no touch device)
662
+ randFloat(70, 80),
663
+ // 2: Key same-time difference
664
+ NO_DATA,
665
+ // 3: (unused)
666
+ randFloat(60, 70),
667
+ // 4: Mouse down-to-up time mean
668
+ NO_DATA,
669
+ // 5: (unused)
670
+ 0,
671
+ // 6: (zero placeholder)
672
+ 0,
673
+ // 7: Mouse click time difference
674
+ // ── Duration distributions ──
675
+ randFloat(60, 80),
676
+ // 8: Mouse down-up duration median
677
+ randFloat(5, 10),
678
+ // 9: Mouse down-up duration std deviation
679
+ randFloat(30, 40),
680
+ // 10: Key press duration median
681
+ randFloat(2, 5),
682
+ // 11: Key press duration std deviation
683
+ // ── Touch metrics (all disabled for desktop) ──
684
+ NO_DATA,
685
+ NO_DATA,
686
+ NO_DATA,
687
+ NO_DATA,
688
+ // 12-15
689
+ NO_DATA,
690
+ NO_DATA,
691
+ NO_DATA,
692
+ NO_DATA,
693
+ // 16-19
694
+ // ── Mouse trajectory analysis ──
695
+ randFloat(150, 180),
696
+ // 20: Mouse movement angle mean
697
+ randFloat(3, 6),
698
+ // 21: Mouse movement angle std deviation
699
+ randFloat(150, 180),
700
+ // 22: Mouse movement angle mean (500ms window)
701
+ randFloat(3, 6),
702
+ // 23: Mouse movement angle std (500ms window)
703
+ randFloat(0, 2),
704
+ // 24: Mouse position deviation X
705
+ randFloat(0, 2),
706
+ // 25: Mouse position deviation Y
707
+ 0,
708
+ 0,
709
+ // 26-27: (zero placeholders)
710
+ // ── Touch sequential/gesture metrics (disabled) ──
711
+ NO_DATA,
712
+ NO_DATA,
713
+ // 28-29
714
+ NO_DATA,
715
+ NO_DATA,
716
+ // 30-31
717
+ // ── Key pattern analysis ──
718
+ 0,
719
+ 0,
720
+ // 32-33: Letter-digit transition ratio
721
+ 0,
722
+ 0,
723
+ // 34-35: Digit-invalid transition ratio
724
+ 0,
725
+ 0,
726
+ // 36-37: Double-invalid transition ratio
727
+ // ── Mouse vector differences ──
728
+ 1,
729
+ 0,
730
+ // 38-39: Mouse vector diff (mean, std)
731
+ 1,
732
+ 0,
733
+ // 40-41: Mouse vector diff 2 (mean, std)
734
+ randFloat(0, 4),
735
+ // 42: Mouse vector diff (500ms mean)
736
+ randFloat(0, 3),
737
+ // 43: Mouse vector diff (500ms std)
738
+ // ── Rounded movement metrics ──
739
+ randFloat(25, 50),
740
+ // 44: Mouse time diff (rounded mean)
741
+ randFloat(25, 50),
742
+ // 45: Mouse time diff (rounded std)
743
+ randFloat(25, 50),
744
+ // 46: Mouse vector diff (rounded mean)
745
+ randFloat(25, 30),
746
+ // 47: Mouse vector diff (rounded std)
747
+ // ── Speed change analysis ──
748
+ randFloat(0, 2),
749
+ // 48: Mouse speed change mean
750
+ randFloat(0, 1),
751
+ // 49: Mouse speed change std
752
+ randFloat(0, 1),
753
+ // 50: Mouse vector 500ms aggregate
754
+ // ── Trailing ──
755
+ 1,
756
+ // 51: Universal flag
757
+ 0
758
+ // 52: Terminator
759
+ ];
760
+ const out = new Uint8Array(metrics.length);
761
+ for (let i = 0; i < metrics.length; i++) {
762
+ out[i] = metrics[i] === NO_DATA ? 0 : encodeFloatVal(metrics[i]);
763
+ }
764
+ return out;
765
+ }
766
+ function buildEventCounts() {
767
+ const counts = [
768
+ randInt(100, 200),
769
+ // 0: mousemove events
770
+ randInt(1, 5),
771
+ // 1: keyup events
772
+ randInt(1, 5),
773
+ // 2: click events
774
+ 0,
775
+ // 3: touchstart events (none on desktop)
776
+ randInt(0, 5),
777
+ // 4: keydown events
778
+ 0,
779
+ // 5: touchmove events (none)
780
+ 0,
781
+ // 6: mousedown-mouseup pairs
782
+ 0,
783
+ // 7: vector diff samples
784
+ randInt(0, 5),
785
+ // 8: wheel events
786
+ randInt(0, 11),
787
+ // 9: (internal counter)
788
+ randInt(0, 1)
789
+ // 10: (internal counter)
790
+ ];
791
+ return concat(new Uint8Array(counts), u8(counts.length));
792
+ }
793
+ function buildBehavioralData() {
794
+ return concat(
795
+ buildBehavioralBitfield(),
796
+ buildFloatMetrics(),
797
+ buildEventCounts()
798
+ );
799
+ }
800
+ function buildTokenHeader(uuid, publisherKey, initTime) {
801
+ const timestamp = fromHex(encodeTimestampEncrypted(initTime));
802
+ const version = be16(SDK_VERSION);
803
+ const pkBytes = textEnc(publisherKey);
804
+ const uuidBytes = fromHex(uuid);
805
+ return concat(timestamp, version, pkBytes, uuidBytes);
806
+ }
807
+ function generateLocalCastleToken(userAgent, profileOverride) {
808
+ const now = Date.now();
809
+ const profile = { ...DEFAULT_PROFILE, ...profileOverride };
810
+ const initTime = now - randFloat(2 * 60 * 1e3, 30 * 60 * 1e3);
811
+ log$7("Generating local Castle.io v11 token");
812
+ const deviceFp = buildDeviceFingerprint(initTime, profile, userAgent);
813
+ const browserFp = buildBrowserFingerprint(profile, initTime);
814
+ const timingFp = buildTimingFingerprint(initTime);
815
+ const eventLog = generateEventLog();
816
+ const behavioral = buildBehavioralData();
817
+ const fingerprintData = concat(
818
+ deviceFp,
819
+ browserFp,
820
+ timingFp,
821
+ eventLog,
822
+ behavioral,
823
+ u8(255)
824
+ );
825
+ const sendTime = Date.now();
826
+ const timestampKey = encodeTimestampEncrypted(sendTime);
827
+ const xorPass1 = deriveAndXor(
828
+ timestampKey,
829
+ 4,
830
+ timestampKey[3],
831
+ fingerprintData
832
+ );
833
+ const tokenUuid = toHex(getRandomBytes(16));
834
+ const withTimestampPrefix = concat(fromHex(timestampKey), xorPass1);
835
+ const xorPass2 = deriveAndXor(
836
+ tokenUuid,
837
+ 8,
838
+ tokenUuid[9],
839
+ withTimestampPrefix
840
+ );
841
+ const header = buildTokenHeader(tokenUuid, TWITTER_CASTLE_PK, initTime);
842
+ const plaintext = concat(header, xorPass2);
843
+ const encrypted = xxteaEncrypt(plaintext, XXTEA_KEY);
844
+ const paddingBytes = encrypted.length - plaintext.length;
845
+ const versioned = concat(u8(TOKEN_VERSION, paddingBytes), encrypted);
846
+ const randomByte = getRandomBytes(1)[0];
847
+ const checksum = versioned.length * 2 & 255;
848
+ const withChecksum = concat(versioned, u8(checksum));
849
+ const xored = xorBytes(withChecksum, u8(randomByte));
850
+ const finalPayload = concat(u8(randomByte), xored);
851
+ const token = base64url(finalPayload);
852
+ log$7(
853
+ `Generated castle token: ${token.length} chars, cuid: ${tokenUuid.substring(
854
+ 0,
855
+ 6
856
+ )}...`
857
+ );
858
+ return { token, cuid: tokenUuid };
859
+ }
860
+
861
+ const log$6 = debug("twitter-scraper:requests");
75
862
  async function updateCookieJar(cookieJar, headers) {
76
863
  let setCookieHeaders = [];
77
864
  if (typeof headers.getSetCookie === "function") {
@@ -86,12 +873,12 @@ async function updateCookieJar(cookieJar, headers) {
86
873
  for (const cookieStr of setCookieHeaders) {
87
874
  const cookie = Cookie.parse(cookieStr);
88
875
  if (!cookie) {
89
- log$5(`Failed to parse cookie: ${cookieStr.substring(0, 100)}`);
876
+ log$6(`Failed to parse cookie: ${cookieStr.substring(0, 100)}`);
90
877
  continue;
91
878
  }
92
879
  if (cookie.maxAge === 0 || cookie.expires && cookie.expires < /* @__PURE__ */ new Date()) {
93
880
  if (cookie.key === "ct0") {
94
- log$5(`Skipping deletion of ct0 cookie (Max-Age=0)`);
881
+ log$6(`Skipping deletion of ct0 cookie (Max-Age=0)`);
95
882
  }
96
883
  continue;
97
884
  }
@@ -99,7 +886,7 @@ async function updateCookieJar(cookieJar, headers) {
99
886
  const url = `${cookie.secure ? "https" : "http"}://${cookie.domain}${cookie.path}`;
100
887
  await cookieJar.setCookie(cookie, url);
101
888
  if (cookie.key === "ct0") {
102
- log$5(
889
+ log$6(
103
890
  `Successfully set ct0 cookie with value: ${cookie.value.substring(
104
891
  0,
105
892
  20
@@ -107,9 +894,9 @@ async function updateCookieJar(cookieJar, headers) {
107
894
  );
108
895
  }
109
896
  } catch (err) {
110
- log$5(`Failed to set cookie ${cookie.key}: ${err}`);
897
+ log$6(`Failed to set cookie ${cookie.key}: ${err}`);
111
898
  if (cookie.key === "ct0") {
112
- log$5(`FAILED to set ct0 cookie! Error: ${err}`);
899
+ log$6(`FAILED to set ct0 cookie! Error: ${err}`);
113
900
  }
114
901
  }
115
902
  }
@@ -123,14 +910,14 @@ async function updateCookieJar(cookieJar, headers) {
123
910
  }
124
911
  }
125
912
 
126
- const log$4 = debug("twitter-scraper:xpff");
913
+ const log$5 = debug("twitter-scraper:xpff");
127
914
  let isoCrypto = null;
128
915
  async function getCrypto() {
129
916
  if (isoCrypto != null) {
130
917
  return isoCrypto;
131
918
  }
132
919
  if (typeof crypto === "undefined") {
133
- log$4("Global crypto is undefined, importing from crypto module...");
920
+ log$5("Global crypto is undefined, importing from crypto module...");
134
921
  const { webcrypto } = await import('crypto');
135
922
  isoCrypto = webcrypto;
136
923
  return webcrypto;
@@ -157,7 +944,7 @@ class XPFFHeaderGenerator {
157
944
  return result;
158
945
  }
159
946
  async generateHeader(plaintext, guestId) {
160
- log$4(`Generating XPFF key for guest ID: ${guestId}`);
947
+ log$5(`Generating XPFF key for guest ID: ${guestId}`);
161
948
  const key = await this.deriveKey(guestId);
162
949
  const crypto2 = await getCrypto();
163
950
  const nonce = crypto2.getRandomValues(new Uint8Array(12));
@@ -180,7 +967,7 @@ class XPFFHeaderGenerator {
180
967
  combined.set(nonce);
181
968
  combined.set(new Uint8Array(encrypted), nonce.length);
182
969
  const result = buf2hex(combined.buffer);
183
- log$4(`XPFF header generated for guest ID ${guestId}: ${result}`);
970
+ log$5(`XPFF header generated for guest ID ${guestId}: ${result}`);
184
971
  return result;
185
972
  }
186
973
  }
@@ -202,7 +989,7 @@ async function generateXPFFHeader(guestId) {
202
989
  return generator.generateHeader(plaintext, guestId);
203
990
  }
204
991
 
205
- const log$3 = debug("twitter-scraper:auth");
992
+ const log$4 = debug("twitter-scraper:auth");
206
993
  function withTransform(fetchFn, transform) {
207
994
  return async (input, init) => {
208
995
  const fetchArgs = await transform?.request?.(input, init) ?? [
@@ -252,8 +1039,14 @@ class TwitterGuestAuth {
252
1039
  }
253
1040
  return new Date(this.guestCreatedAt);
254
1041
  }
255
- async installTo(headers, _url, bearerTokenOverride) {
1042
+ /**
1043
+ * Install only authentication credentials (bearer token, guest token, cookies)
1044
+ * without browser fingerprint or platform headers. Useful for callers that
1045
+ * build their own header set (e.g. the login flow).
1046
+ */
1047
+ async installAuthCredentials(headers, bearerTokenOverride) {
256
1048
  const tokenToUse = bearerTokenOverride ?? this.bearerToken;
1049
+ headers.set("authorization", `Bearer ${tokenToUse}`);
257
1050
  if (!bearerTokenOverride) {
258
1051
  if (this.shouldUpdate()) {
259
1052
  await this.updateGuestToken();
@@ -262,11 +1055,27 @@ class TwitterGuestAuth {
262
1055
  headers.set("x-guest-token", this.guestToken);
263
1056
  }
264
1057
  }
265
- headers.set("authorization", `Bearer ${tokenToUse}`);
266
- headers.set(
267
- "user-agent",
268
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
269
- );
1058
+ headers.set("cookie", await this.getCookieString());
1059
+ }
1060
+ async installTo(headers, url, bearerTokenOverride) {
1061
+ await this.installAuthCredentials(headers, bearerTokenOverride);
1062
+ headers.set("user-agent", CHROME_USER_AGENT);
1063
+ if (!headers.has("accept")) {
1064
+ headers.set("accept", "*/*");
1065
+ }
1066
+ headers.set("accept-language", "en-US,en;q=0.9");
1067
+ headers.set("sec-ch-ua", CHROME_SEC_CH_UA);
1068
+ headers.set("sec-ch-ua-mobile", "?0");
1069
+ headers.set("sec-ch-ua-platform", '"Windows"');
1070
+ headers.set("referer", "https://x.com/");
1071
+ headers.set("origin", "https://x.com");
1072
+ headers.set("sec-fetch-site", "same-site");
1073
+ headers.set("sec-fetch-mode", "cors");
1074
+ headers.set("sec-fetch-dest", "empty");
1075
+ headers.set("priority", "u=1, i");
1076
+ if (!headers.has("content-type") && (url.includes("api.x.com/graphql/") || url.includes("x.com/i/api/graphql/"))) {
1077
+ headers.set("content-type", "application/json");
1078
+ }
270
1079
  await this.installCsrfToken(headers);
271
1080
  if (this.options?.experimental?.xpff) {
272
1081
  const guestId = await this.guestId();
@@ -275,7 +1084,6 @@ class TwitterGuestAuth {
275
1084
  headers.set("x-xp-forwarded-for", xpffHeader);
276
1085
  }
277
1086
  }
278
- headers.set("cookie", await this.getCookieString());
279
1087
  }
280
1088
  async installCsrfToken(headers) {
281
1089
  const cookies = await this.getCookies();
@@ -306,7 +1114,7 @@ class TwitterGuestAuth {
306
1114
  const cookies = await this.jar.getCookies(this.getCookieJarUrl());
307
1115
  for (const cookie of cookies) {
308
1116
  if (!cookie.domain || !cookie.path) continue;
309
- store.removeCookie(cookie.domain, cookie.path, key);
1117
+ await store.removeCookie(cookie.domain, cookie.path, key);
310
1118
  if (typeof document !== "undefined") {
311
1119
  document.cookie = `${cookie.key}=; Max-Age=0; path=${cookie.path}; domain=${cookie.domain}`;
312
1120
  }
@@ -327,20 +1135,31 @@ class TwitterGuestAuth {
327
1135
  try {
328
1136
  await this.updateGuestTokenCore();
329
1137
  } catch (err) {
330
- log$3("Failed to update guest token; this may cause issues:", err);
1138
+ log$4("Failed to update guest token; this may cause issues:", err);
331
1139
  }
332
1140
  }
333
1141
  async updateGuestTokenCore() {
334
1142
  const guestActivateUrl = "https://api.x.com/1.1/guest/activate.json";
335
1143
  const headers = new Headers({
336
- Authorization: `Bearer ${this.bearerToken}`,
337
- Cookie: await this.getCookieString()
1144
+ authorization: `Bearer ${this.bearerToken}`,
1145
+ "user-agent": CHROME_USER_AGENT,
1146
+ accept: "*/*",
1147
+ "accept-language": "en-US,en;q=0.9",
1148
+ "content-type": "application/x-www-form-urlencoded",
1149
+ "sec-ch-ua": CHROME_SEC_CH_UA,
1150
+ "sec-ch-ua-mobile": "?0",
1151
+ "sec-ch-ua-platform": '"Windows"',
1152
+ origin: "https://x.com",
1153
+ referer: "https://x.com/",
1154
+ "sec-fetch-site": "same-site",
1155
+ "sec-fetch-mode": "cors",
1156
+ "sec-fetch-dest": "empty",
1157
+ cookie: await this.getCookieString()
338
1158
  });
339
- log$3(`Making POST request to ${guestActivateUrl}`);
1159
+ log$4(`Making POST request to ${guestActivateUrl}`);
340
1160
  const res = await this.fetch(guestActivateUrl, {
341
1161
  method: "POST",
342
- headers,
343
- referrerPolicy: "no-referrer"
1162
+ headers
344
1163
  });
345
1164
  await updateCookieJar(this.jar, res.headers);
346
1165
  if (!res.ok) {
@@ -357,7 +1176,7 @@ class TwitterGuestAuth {
357
1176
  this.guestToken = newGuestToken;
358
1177
  this.guestCreatedAt = /* @__PURE__ */ new Date();
359
1178
  await this.setCookie("gt", newGuestToken);
360
- log$3(`Updated guest token: ${newGuestToken}`);
1179
+ log$4(`Updated guest token (length: ${newGuestToken.length})`);
361
1180
  }
362
1181
  /**
363
1182
  * Returns if the authentication token needs to be updated or not.
@@ -384,7 +1203,7 @@ class Platform {
384
1203
  }
385
1204
  }
386
1205
 
387
- const log$2 = debug("twitter-scraper:xctxid");
1206
+ const log$3 = debug("twitter-scraper:xctxid");
388
1207
  let linkedom = null;
389
1208
  async function linkedomImport() {
390
1209
  if (!linkedom) {
@@ -413,7 +1232,7 @@ async function handleXMigration(fetchFn) {
413
1232
  "cache-control": "no-cache",
414
1233
  pragma: "no-cache",
415
1234
  priority: "u=0, i",
416
- "sec-ch-ua": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
1235
+ "sec-ch-ua": CHROME_SEC_CH_UA,
417
1236
  "sec-ch-ua-mobile": "?0",
418
1237
  "sec-ch-ua-platform": '"Windows"',
419
1238
  "sec-fetch-dest": "document",
@@ -421,7 +1240,7 @@ async function handleXMigration(fetchFn) {
421
1240
  "sec-fetch-site": "none",
422
1241
  "sec-fetch-user": "?1",
423
1242
  "upgrade-insecure-requests": "1",
424
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
1243
+ "user-agent": CHROME_USER_AGENT
425
1244
  };
426
1245
  const response = await fetchFn("https://x.com", {
427
1246
  headers
@@ -440,7 +1259,7 @@ async function handleXMigration(fetchFn) {
440
1259
  const metaContent = metaRefresh ? metaRefresh.getAttribute("content") || "" : "";
441
1260
  const migrationRedirectionUrl = migrationRedirectionRegex.exec(metaContent) || migrationRedirectionRegex.exec(htmlText);
442
1261
  if (migrationRedirectionUrl) {
443
- const redirectResponse = await fetch(migrationRedirectionUrl[0]);
1262
+ const redirectResponse = await fetchFn(migrationRedirectionUrl[0]);
444
1263
  if (!redirectResponse.ok) {
445
1264
  throw new Error(
446
1265
  `Failed to follow migration redirection: ${redirectResponse.statusText}`
@@ -463,7 +1282,7 @@ async function handleXMigration(fetchFn) {
463
1282
  requestPayload.append(name, value);
464
1283
  }
465
1284
  }
466
- const formResponse = await fetch(url, {
1285
+ const formResponse = await fetchFn(url, {
467
1286
  method,
468
1287
  body: requestPayload,
469
1288
  headers
@@ -479,6 +1298,23 @@ async function handleXMigration(fetchFn) {
479
1298
  }
480
1299
  return document;
481
1300
  }
1301
+ let cachedDocumentPromise = null;
1302
+ let cachedDocumentTimestamp = 0;
1303
+ const DOCUMENT_CACHE_TTL = 5 * 60 * 1e3;
1304
+ async function getCachedDocument(fetchFn) {
1305
+ const now = Date.now();
1306
+ if (!cachedDocumentPromise || now - cachedDocumentTimestamp > DOCUMENT_CACHE_TTL) {
1307
+ log$3("Fetching fresh x.com document for transaction ID generation");
1308
+ cachedDocumentTimestamp = now;
1309
+ cachedDocumentPromise = handleXMigration(fetchFn).catch((err) => {
1310
+ cachedDocumentPromise = null;
1311
+ throw err;
1312
+ });
1313
+ } else {
1314
+ log$3("Using cached x.com document for transaction ID generation");
1315
+ }
1316
+ return cachedDocumentPromise;
1317
+ }
482
1318
  let ClientTransaction = null;
483
1319
  async function clientTransaction() {
484
1320
  if (!ClientTransaction) {
@@ -491,16 +1327,19 @@ async function clientTransaction() {
491
1327
  async function generateTransactionId(url, fetchFn, method) {
492
1328
  const parsedUrl = new URL(url);
493
1329
  const path = parsedUrl.pathname;
494
- log$2(`Generating transaction ID for ${method} ${path}`);
495
- const document = await handleXMigration(fetchFn);
1330
+ log$3(`Generating transaction ID for ${method} ${path}`);
1331
+ const document = await getCachedDocument(fetchFn);
496
1332
  const ClientTransactionClass = await clientTransaction();
497
1333
  const transaction = await ClientTransactionClass.create(document);
498
1334
  const transactionId = await transaction.generateTransactionId(method, path);
499
- log$2(`Transaction ID: ${transactionId}`);
1335
+ log$3(`Transaction ID: ${transactionId}`);
500
1336
  return transactionId;
501
1337
  }
502
1338
 
503
- const log$1 = debug("twitter-scraper:api");
1339
+ const CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36";
1340
+ const CHROME_SEC_CH_UA = '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"';
1341
+
1342
+ const log$2 = debug("twitter-scraper:api");
504
1343
  const bearerToken = "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF";
505
1344
  const bearerToken2 = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
506
1345
  async function jitter(maxMs) {
@@ -508,7 +1347,7 @@ async function jitter(maxMs) {
508
1347
  await new Promise((resolve) => setTimeout(resolve, jitter2));
509
1348
  }
510
1349
  async function requestApi(url, auth, method = "GET", platform = new Platform(), headers = new Headers(), bearerTokenOverride) {
511
- log$1(`Making ${method} request to ${url}`);
1350
+ log$2(`Making ${method} request to ${url}`);
512
1351
  await auth.installTo(headers, url, bearerTokenOverride);
513
1352
  await platform.randomizeCiphers();
514
1353
  if (auth instanceof TwitterGuestAuth && auth.options?.experimental?.xClientTransactionId) {
@@ -537,12 +1376,12 @@ async function requestApi(url, auth, method = "GET", platform = new Platform(),
537
1376
  }
538
1377
  return {
539
1378
  success: false,
540
- err: new Error("Failed to perform request.")
1379
+ err
541
1380
  };
542
1381
  }
543
1382
  await updateCookieJar(auth.cookieJar(), res.headers);
544
1383
  if (res.status === 429) {
545
- log$1("Rate limit hit, waiting for retry...");
1384
+ log$2("Rate limit hit, waiting for retry...");
546
1385
  await auth.onRateLimit({
547
1386
  fetchParameters,
548
1387
  response: res
@@ -567,9 +1406,9 @@ async function flexParseJson(res) {
567
1406
  try {
568
1407
  return await res.json();
569
1408
  } catch {
570
- log$1("Failed to parse response as JSON, trying text parse...");
1409
+ log$2("Failed to parse response as JSON, trying text parse...");
571
1410
  const text = await res.text();
572
- log$1("Response text:", text);
1411
+ log$2("Response text:", text);
573
1412
  return JSON.parse(text);
574
1413
  }
575
1414
  }
@@ -643,12 +1482,12 @@ function addApiParams(params, includeTweetReplies) {
643
1482
  return params;
644
1483
  }
645
1484
 
646
- const log = debug("twitter-scraper:auth-user");
1485
+ const log$1 = debug("twitter-scraper:auth-user");
647
1486
  const TwitterUserAuthSubtask = Type.Object({
648
1487
  subtask_id: Type.String(),
649
1488
  enter_text: Type.Optional(Type.Object({}))
650
1489
  });
651
- class TwitterUserAuth extends TwitterGuestAuth {
1490
+ const _TwitterUserAuth = class _TwitterUserAuth extends TwitterGuestAuth {
652
1491
  constructor(bearerToken, options) {
653
1492
  super(bearerToken, options);
654
1493
  this.subtaskHandlers = /* @__PURE__ */ new Map();
@@ -694,11 +1533,14 @@ class TwitterUserAuth extends TwitterGuestAuth {
694
1533
  );
695
1534
  }
696
1535
  async isLoggedIn() {
697
- const cookie = await this.getCookieString();
698
- return cookie.includes("ct0=");
1536
+ const cookies = await this.getCookies();
1537
+ return cookies.some((c) => c.key === "ct0") && cookies.some((c) => c.key === "auth_token");
699
1538
  }
700
1539
  async login(username, password, email, twoFactorSecret) {
701
- await this.updateGuestToken();
1540
+ await this.preflight();
1541
+ if (!this.guestToken) {
1542
+ await this.updateGuestToken();
1543
+ }
702
1544
  const credentials = {
703
1545
  username,
704
1546
  password,
@@ -712,6 +1554,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
712
1554
  throw new Error("flow_token not found.");
713
1555
  }
714
1556
  const subtaskId = next.response.subtasks[0].subtask_id;
1557
+ const configuredDelay = this.options?.experimental?.flowStepDelay;
1558
+ const delay = configuredDelay !== void 0 ? configuredDelay : 1e3 + Math.floor(Math.random() * 2e3);
1559
+ if (delay > 0) {
1560
+ log$1(`Waiting ${delay}ms before handling subtask: ${subtaskId}`);
1561
+ await new Promise((resolve) => setTimeout(resolve, delay));
1562
+ }
715
1563
  const handler = this.subtaskHandlers.get(subtaskId);
716
1564
  if (handler) {
717
1565
  next = await handler(subtaskId, next.response, credentials, {
@@ -726,65 +1574,92 @@ class TwitterUserAuth extends TwitterGuestAuth {
726
1574
  throw next.err;
727
1575
  }
728
1576
  }
1577
+ /**
1578
+ * Pre-flight request to establish Cloudflare cookies and session context.
1579
+ * Mimics a real browser visiting x.com before starting the login API flow.
1580
+ */
1581
+ async preflight() {
1582
+ try {
1583
+ const headers = new Headers({
1584
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
1585
+ "accept-language": "en-US,en;q=0.9",
1586
+ "sec-ch-ua": CHROME_SEC_CH_UA,
1587
+ "sec-ch-ua-mobile": "?0",
1588
+ "sec-ch-ua-platform": '"Windows"',
1589
+ "sec-fetch-dest": "document",
1590
+ "sec-fetch-mode": "navigate",
1591
+ "sec-fetch-site": "none",
1592
+ "sec-fetch-user": "?1",
1593
+ "upgrade-insecure-requests": "1",
1594
+ "user-agent": CHROME_USER_AGENT
1595
+ });
1596
+ log$1("Pre-flight: fetching https://x.com/i/flow/login");
1597
+ const res = await this.fetch("https://x.com/i/flow/login", {
1598
+ redirect: "follow",
1599
+ headers
1600
+ });
1601
+ await updateCookieJar(this.jar, res.headers);
1602
+ log$1(`Pre-flight response: ${res.status}`);
1603
+ try {
1604
+ const html = await res.text();
1605
+ const gtMatch = html.match(/document\.cookie="gt=(\d+)/);
1606
+ if (gtMatch) {
1607
+ this.guestToken = gtMatch[1];
1608
+ this.guestCreatedAt = /* @__PURE__ */ new Date();
1609
+ await this.setCookie("gt", gtMatch[1]);
1610
+ log$1(`Extracted guest token from HTML (length: ${gtMatch[1].length})`);
1611
+ }
1612
+ } catch (err) {
1613
+ log$1("Failed to extract guest token from HTML (non-fatal):", err);
1614
+ }
1615
+ } catch (err) {
1616
+ log$1("Pre-flight request failed (non-fatal):", err);
1617
+ }
1618
+ }
729
1619
  async logout() {
730
1620
  if (!this.hasToken()) {
731
1621
  return;
732
1622
  }
733
1623
  try {
734
- await requestApi(
735
- "https://api.x.com/1.1/account/logout.json",
736
- this,
737
- "POST"
738
- );
1624
+ const logoutUrl = "https://api.x.com/1.1/account/logout.json";
1625
+ const headers = new Headers();
1626
+ await this.installTo(headers, logoutUrl);
1627
+ await this.fetch(logoutUrl, {
1628
+ method: "POST",
1629
+ headers
1630
+ });
739
1631
  } catch (error) {
740
- console.warn("Error during logout:", error);
1632
+ log$1("Error during logout:", error);
741
1633
  } finally {
742
1634
  this.deleteToken();
743
1635
  this.jar = new CookieJar();
744
1636
  }
745
1637
  }
746
- async installTo(headers, _url, bearerTokenOverride) {
747
- const tokenToUse = bearerTokenOverride ?? this.bearerToken;
748
- headers.set("authorization", `Bearer ${tokenToUse}`);
749
- headers.set(
750
- "user-agent",
751
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
752
- );
753
- if (this.guestToken) {
754
- headers.set("x-guest-token", this.guestToken);
755
- }
756
- await this.installCsrfToken(headers);
757
- if (this.options?.experimental?.xpff) {
758
- const guestId = await this.guestId();
759
- if (guestId != null) {
760
- const xpffHeader = await generateXPFFHeader(guestId);
761
- headers.set("x-xp-forwarded-for", xpffHeader);
762
- }
763
- }
764
- const cookie = await this.getCookieString();
765
- headers.set("cookie", cookie);
1638
+ async installTo(headers, url, bearerTokenOverride) {
1639
+ await super.installTo(headers, url, bearerTokenOverride);
1640
+ headers.set("x-twitter-auth-type", "OAuth2Session");
1641
+ headers.set("x-twitter-active-user", "yes");
1642
+ headers.set("x-twitter-client-language", "en");
766
1643
  }
767
1644
  async initLogin() {
768
- this.removeCookie("twitter_ads_id=");
769
- this.removeCookie("ads_prefs=");
770
- this.removeCookie("_twitter_sess=");
771
- this.removeCookie("zipbox_forms_auth_token=");
772
- this.removeCookie("lang=");
773
- this.removeCookie("bouncer_reset_cookie=");
774
- this.removeCookie("twid=");
775
- this.removeCookie("twitter_ads_idb=");
776
- this.removeCookie("email_uid=");
777
- this.removeCookie("external_referer=");
778
- this.removeCookie("ct0=");
779
- this.removeCookie("aa_u=");
780
- this.removeCookie("__cf_bm=");
1645
+ await this.removeCookie("twitter_ads_id");
1646
+ await this.removeCookie("ads_prefs");
1647
+ await this.removeCookie("_twitter_sess");
1648
+ await this.removeCookie("zipbox_forms_auth_token");
1649
+ await this.removeCookie("lang");
1650
+ await this.removeCookie("bouncer_reset_cookie");
1651
+ await this.removeCookie("twid");
1652
+ await this.removeCookie("twitter_ads_idb");
1653
+ await this.removeCookie("email_uid");
1654
+ await this.removeCookie("external_referer");
1655
+ await this.removeCookie("aa_u");
781
1656
  return await this.executeFlowTask({
782
1657
  flow_name: "login",
783
1658
  input_flow_data: {
784
1659
  flow_context: {
785
1660
  debug_overrides: {},
786
1661
  start_location: {
787
- location: "unknown"
1662
+ location: "manual_link"
788
1663
  }
789
1664
  }
790
1665
  },
@@ -833,20 +1708,157 @@ class TwitterUserAuth extends TwitterGuestAuth {
833
1708
  }
834
1709
  });
835
1710
  }
836
- async handleJsInstrumentationSubtask(subtaskId, _prev, _credentials, api) {
1711
+ async handleJsInstrumentationSubtask(subtaskId, prev, _credentials, api) {
1712
+ const subtasks = prev.subtasks;
1713
+ const jsSubtask = subtasks?.find((s) => s.subtask_id === subtaskId);
1714
+ const jsUrl = jsSubtask?.js_instrumentation?.url;
1715
+ let metricsResponse = "{}";
1716
+ if (jsUrl) {
1717
+ try {
1718
+ metricsResponse = await this.executeJsInstrumentation(jsUrl);
1719
+ log$1(
1720
+ `JS instrumentation executed successfully, response length: ${metricsResponse.length}`
1721
+ );
1722
+ } catch (err) {
1723
+ log$1("Failed to execute JS instrumentation (falling back to {})", err);
1724
+ }
1725
+ }
837
1726
  return await api.sendFlowRequest({
838
1727
  flow_token: api.getFlowToken(),
839
1728
  subtask_inputs: [
840
1729
  {
841
1730
  subtask_id: subtaskId,
842
1731
  js_instrumentation: {
843
- response: "{}",
1732
+ response: metricsResponse,
844
1733
  link: "next_link"
845
1734
  }
846
1735
  }
847
1736
  ]
848
1737
  });
849
1738
  }
1739
+ // 512KB
1740
+ /**
1741
+ * Fetches and executes the JS instrumentation script to generate browser
1742
+ * fingerprinting data. The result is written to an input element named
1743
+ * 'ui_metrics'.
1744
+ *
1745
+ * In browser environments, uses a hidden iframe with native DOM APIs.
1746
+ * In Node.js, uses linkedom (for DOM) and the vm module for execution.
1747
+ *
1748
+ * @security This method executes **remote JavaScript** fetched from Twitter's servers.
1749
+ * - In browsers, execution is isolated in a disposable iframe.
1750
+ * - In Node.js, `vm.runInContext` is used for convenience, NOT for security.
1751
+ * Node's `vm` module provides NO security sandbox — a malicious script can
1752
+ * trivially escape the context (e.g., via `this.constructor.constructor('return process')()`).
1753
+ * The only real trust boundary is that scripts are fetched from Twitter's known CDN URLs.
1754
+ * Setting `process: undefined` etc. in the sandbox context is cosmetic and does not
1755
+ * prevent escape.
1756
+ * - A maximum script size limit (512KB) and a 5-second timeout provide basic sanity checks.
1757
+ */
1758
+ async executeJsInstrumentation(url) {
1759
+ log$1(`Fetching JS instrumentation from: ${url}`);
1760
+ const response = await this.fetch(url);
1761
+ const scriptContent = await response.text();
1762
+ log$1(`JS instrumentation script fetched, length: ${scriptContent.length}`);
1763
+ if (scriptContent.length > _TwitterUserAuth.JS_INSTRUMENTATION_MAX_SIZE) {
1764
+ log$1(
1765
+ `WARNING: JS instrumentation script exceeds size limit (${scriptContent.length} > ${_TwitterUserAuth.JS_INSTRUMENTATION_MAX_SIZE}), skipping execution`
1766
+ );
1767
+ return "{}";
1768
+ }
1769
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
1770
+ return this.executeJsInstrumentationBrowser(scriptContent);
1771
+ }
1772
+ return this.executeJsInstrumentationNode(scriptContent);
1773
+ }
1774
+ /**
1775
+ * Execute JS instrumentation in a browser environment using a hidden iframe.
1776
+ * The iframe provides natural isolation — the script gets its own document
1777
+ * and window, and we can override setTimeout without affecting the host page.
1778
+ */
1779
+ async executeJsInstrumentationBrowser(scriptContent) {
1780
+ const iframe = document.createElement("iframe");
1781
+ iframe.style.display = "none";
1782
+ document.body.appendChild(iframe);
1783
+ try {
1784
+ const iframeWin = iframe.contentWindow;
1785
+ const iframeDoc = iframe.contentDocument;
1786
+ if (!iframeWin || !iframeDoc) {
1787
+ log$1("WARNING: Could not access iframe document/window");
1788
+ return "{}";
1789
+ }
1790
+ const input = iframeDoc.createElement("input");
1791
+ input.name = "ui_metrics";
1792
+ input.type = "hidden";
1793
+ iframeDoc.body.appendChild(input);
1794
+ iframeWin.setTimeout = (fn) => fn();
1795
+ const script = iframeDoc.createElement("script");
1796
+ script.textContent = scriptContent;
1797
+ iframeDoc.body.appendChild(script);
1798
+ const value = input.value;
1799
+ if (value) {
1800
+ log$1(`JS instrumentation result extracted, length: ${value.length}`);
1801
+ return value;
1802
+ }
1803
+ log$1("WARNING: No ui_metrics value found after script execution");
1804
+ return "{}";
1805
+ } finally {
1806
+ document.body.removeChild(iframe);
1807
+ }
1808
+ }
1809
+ /**
1810
+ * Execute JS instrumentation in Node.js using linkedom for DOM emulation
1811
+ * and the vm module for sandboxed script execution.
1812
+ *
1813
+ * @security Node's `vm` module does NOT provide a security sandbox. A
1814
+ * malicious script can trivially escape the context. The only real trust
1815
+ * boundary is that scripts come from Twitter's CDN. The undefined globals
1816
+ * (process, require, etc.) are cosmetic — they do not prevent sandbox escape.
1817
+ */
1818
+ async executeJsInstrumentationNode(scriptContent) {
1819
+ const { parseHTML } = await import('linkedom');
1820
+ const { document: doc, window: win } = parseHTML(
1821
+ '<html><head></head><body><input name="ui_metrics" type="hidden" value="" /></body></html>'
1822
+ );
1823
+ if (typeof doc.getElementsByName !== "function") {
1824
+ doc.getElementsByName = (name) => doc.querySelectorAll(`[name="${name}"]`);
1825
+ }
1826
+ const vm = await import('vm');
1827
+ const origSetTimeout = win.setTimeout;
1828
+ win.setTimeout = (fn) => fn();
1829
+ try {
1830
+ Object.defineProperty(doc, "readyState", {
1831
+ value: "complete",
1832
+ writable: true,
1833
+ configurable: true
1834
+ });
1835
+ } catch {
1836
+ }
1837
+ const sandbox = {
1838
+ document: doc,
1839
+ window: win,
1840
+ Date,
1841
+ JSON,
1842
+ parseInt,
1843
+ // Deny access to Node.js internals to limit sandbox escape surface
1844
+ process: void 0,
1845
+ require: void 0,
1846
+ global: void 0,
1847
+ globalThis: void 0
1848
+ };
1849
+ vm.runInNewContext(scriptContent, sandbox, { timeout: 5e3 });
1850
+ win.setTimeout = origSetTimeout;
1851
+ const inputs = doc.getElementsByName("ui_metrics");
1852
+ if (inputs && inputs.length > 0) {
1853
+ const value = inputs[0].value || inputs[0].getAttribute("value");
1854
+ if (value) {
1855
+ log$1(`JS instrumentation result extracted, length: ${value.length}`);
1856
+ return value;
1857
+ }
1858
+ }
1859
+ log$1("WARNING: No ui_metrics value found after script execution");
1860
+ return "{}";
1861
+ }
850
1862
  async handleEnterAlternateIdentifierSubtask(subtaskId, _prev, credentials, api) {
851
1863
  return await this.executeFlowTask({
852
1864
  flow_token: api.getFlowToken(),
@@ -862,36 +1874,76 @@ class TwitterUserAuth extends TwitterGuestAuth {
862
1874
  });
863
1875
  }
864
1876
  async handleEnterUserIdentifierSSO(subtaskId, _prev, credentials, api) {
1877
+ let castleToken;
1878
+ try {
1879
+ castleToken = await this.generateCastleToken();
1880
+ log$1(`Castle token generated, length: ${castleToken.length}`);
1881
+ } catch (err) {
1882
+ log$1("Failed to generate castle token (continuing without it):", err);
1883
+ }
1884
+ const settingsList = {
1885
+ setting_responses: [
1886
+ {
1887
+ key: "user_identifier",
1888
+ response_data: {
1889
+ text_data: { result: credentials.username }
1890
+ }
1891
+ }
1892
+ ],
1893
+ link: "next_link"
1894
+ };
1895
+ if (castleToken) {
1896
+ settingsList.castle_token = castleToken;
1897
+ }
865
1898
  return await this.executeFlowTask({
866
1899
  flow_token: api.getFlowToken(),
867
1900
  subtask_inputs: [
868
1901
  {
869
1902
  subtask_id: subtaskId,
870
- settings_list: {
871
- setting_responses: [
872
- {
873
- key: "user_identifier",
874
- response_data: {
875
- text_data: { result: credentials.username }
876
- }
877
- }
878
- ],
879
- link: "next_link"
880
- }
1903
+ settings_list: settingsList
881
1904
  }
882
1905
  ]
883
1906
  });
884
1907
  }
1908
+ /**
1909
+ * Generates a Castle.io device fingerprint token for the login flow.
1910
+ * Uses local token generation (Castle.io v11 format) to avoid external
1911
+ * API dependencies and rate limits.
1912
+ */
1913
+ async generateCastleToken() {
1914
+ const userAgent = CHROME_USER_AGENT;
1915
+ const browserProfile = this.options?.experimental?.browserProfile;
1916
+ const { token, cuid } = generateLocalCastleToken(userAgent, browserProfile);
1917
+ await this.setCookie("__cuid", cuid);
1918
+ log$1(
1919
+ `Castle token generated locally, length: ${token.length}, cuid: ${cuid.substring(0, 6)}...`
1920
+ );
1921
+ return token;
1922
+ }
885
1923
  async handleEnterPassword(subtaskId, _prev, credentials, api) {
1924
+ let castleToken;
1925
+ try {
1926
+ castleToken = await this.generateCastleToken();
1927
+ log$1(`Castle token for password step, length: ${castleToken.length}`);
1928
+ } catch (err) {
1929
+ log$1(
1930
+ "Failed to generate castle token for password (continuing without):",
1931
+ err
1932
+ );
1933
+ }
1934
+ const enterPassword = {
1935
+ password: credentials.password,
1936
+ link: "next_link"
1937
+ };
1938
+ if (castleToken) {
1939
+ enterPassword.castle_token = castleToken;
1940
+ }
886
1941
  return await this.executeFlowTask({
887
1942
  flow_token: api.getFlowToken(),
888
1943
  subtask_inputs: [
889
1944
  {
890
1945
  subtask_id: subtaskId,
891
- enter_password: {
892
- password: credentials.password,
893
- link: "next_link"
894
- }
1946
+ enter_password: enterPassword
895
1947
  }
896
1948
  ]
897
1949
  });
@@ -919,27 +1971,28 @@ class TwitterUserAuth extends TwitterGuestAuth {
919
1971
  };
920
1972
  }
921
1973
  const totp = new OTPAuth.TOTP({ secret: credentials.twoFactorSecret });
922
- let error;
1974
+ let lastResult;
923
1975
  for (let attempts = 1; attempts < 4; attempts += 1) {
924
- try {
925
- return await api.sendFlowRequest({
926
- flow_token: api.getFlowToken(),
927
- subtask_inputs: [
928
- {
929
- subtask_id: subtaskId,
930
- enter_text: {
931
- link: "next_link",
932
- text: totp.generate()
933
- }
1976
+ const result = await api.sendFlowRequest({
1977
+ flow_token: api.getFlowToken(),
1978
+ subtask_inputs: [
1979
+ {
1980
+ subtask_id: subtaskId,
1981
+ enter_text: {
1982
+ link: "next_link",
1983
+ text: totp.generate()
934
1984
  }
935
- ]
936
- });
937
- } catch (err) {
938
- error = err;
939
- await new Promise((resolve) => setTimeout(resolve, 2e3 * attempts));
1985
+ }
1986
+ ]
1987
+ });
1988
+ if (result.status === "success") {
1989
+ return result;
940
1990
  }
1991
+ lastResult = result;
1992
+ log$1(`2FA attempt ${attempts} failed: ${result.err.message}`);
1993
+ await new Promise((resolve) => setTimeout(resolve, 2e3 * attempts));
941
1994
  }
942
- throw error;
1995
+ return lastResult;
943
1996
  }
944
1997
  async handleAcid(subtaskId, _prev, credentials, api) {
945
1998
  return await this.executeFlowTask({
@@ -956,7 +2009,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
956
2009
  });
957
2010
  }
958
2011
  async handleSuccessSubtask() {
959
- log("Successfully logged in with user credentials.");
2012
+ log$1("Successfully logged in with user credentials.");
960
2013
  return {
961
2014
  status: "success",
962
2015
  response: {}
@@ -967,28 +2020,33 @@ class TwitterUserAuth extends TwitterGuestAuth {
967
2020
  if ("flow_name" in data) {
968
2021
  onboardingTaskUrl = `https://api.x.com/1.1/onboarding/task.json?flow_name=${data.flow_name}`;
969
2022
  }
970
- log(`Making POST request to ${onboardingTaskUrl}`);
2023
+ log$1(`Making POST request to ${onboardingTaskUrl}`);
2024
+ log$1(
2025
+ "Request data:",
2026
+ JSON.stringify(
2027
+ data,
2028
+ (key, value) => key === "password" ? "[REDACTED]" : value,
2029
+ 2
2030
+ )
2031
+ );
971
2032
  const headers = new Headers({
972
2033
  accept: "*/*",
973
2034
  "accept-language": "en-US,en;q=0.9",
974
2035
  "content-type": "application/json",
975
- "cache-control": "no-cache",
976
2036
  origin: "https://x.com",
977
- pragma: "no-cache",
978
2037
  priority: "u=1, i",
979
2038
  referer: "https://x.com/",
980
- "sec-ch-ua": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
2039
+ "sec-ch-ua": CHROME_SEC_CH_UA,
981
2040
  "sec-ch-ua-mobile": "?0",
982
2041
  "sec-ch-ua-platform": '"Windows"',
983
2042
  "sec-fetch-dest": "empty",
984
2043
  "sec-fetch-mode": "cors",
985
- "sec-fetch-site": "same-origin",
986
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
987
- "x-twitter-auth-type": "OAuth2Client",
2044
+ "sec-fetch-site": "same-site",
2045
+ "user-agent": CHROME_USER_AGENT,
988
2046
  "x-twitter-active-user": "yes",
989
2047
  "x-twitter-client-language": "en"
990
2048
  });
991
- await this.installTo(headers, onboardingTaskUrl);
2049
+ await this.installAuthCredentials(headers);
992
2050
  if (this.options?.experimental?.xClientTransactionId) {
993
2051
  const transactionId = await generateTransactionId(
994
2052
  onboardingTaskUrl,
@@ -997,6 +2055,10 @@ class TwitterUserAuth extends TwitterGuestAuth {
997
2055
  );
998
2056
  headers.set("x-client-transaction-id", transactionId);
999
2057
  }
2058
+ const bodyData = { ...data };
2059
+ if ("flow_name" in bodyData) {
2060
+ delete bodyData.flow_name;
2061
+ }
1000
2062
  let res;
1001
2063
  do {
1002
2064
  const fetchParameters = [
@@ -1005,7 +2067,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
1005
2067
  credentials: "include",
1006
2068
  method: "POST",
1007
2069
  headers,
1008
- body: JSON.stringify(data)
2070
+ body: JSON.stringify(bodyData)
1009
2071
  }
1010
2072
  ];
1011
2073
  try {
@@ -1020,25 +2082,58 @@ class TwitterUserAuth extends TwitterGuestAuth {
1020
2082
  };
1021
2083
  }
1022
2084
  await updateCookieJar(this.jar, res.headers);
2085
+ log$1(`Response status: ${res.status}`);
1023
2086
  if (res.status === 429) {
1024
- log("Rate limit hit, waiting before retrying...");
2087
+ log$1("Rate limit hit, waiting before retrying...");
1025
2088
  await this.onRateLimit({
1026
2089
  fetchParameters,
1027
2090
  response: res
1028
2091
  });
1029
2092
  }
1030
2093
  } while (res.status === 429);
1031
- if (!res.ok) {
1032
- return { status: "error", err: await ApiError.fromResponse(res) };
1033
- }
1034
- const flow = await flexParseJson(res);
1035
- if (flow?.flow_token == null) {
2094
+ let flow;
2095
+ try {
2096
+ flow = await flexParseJson(res);
2097
+ } catch {
2098
+ if (!res.ok) {
2099
+ return {
2100
+ status: "error",
2101
+ err: new ApiError(res, "Failed to parse response body")
2102
+ };
2103
+ }
1036
2104
  return {
1037
2105
  status: "error",
1038
- err: new AuthenticationError("flow_token not found.")
2106
+ err: new AuthenticationError("Failed to parse flow response.")
1039
2107
  };
1040
2108
  }
2109
+ log$1(
2110
+ "Flow response: status=%s subtasks=%s",
2111
+ flow.status,
2112
+ flow.subtasks?.map((s) => s.subtask_id).join(", ")
2113
+ );
1041
2114
  if (flow.errors?.length) {
2115
+ log$1("Twitter auth flow errors:", JSON.stringify(flow.errors, null, 2));
2116
+ if (flow.errors[0].code === 399) {
2117
+ const message = flow.errors[0].message || "";
2118
+ const challengeMatch = message.match(/g;[^:]+:[^:]+:[0-9]+/);
2119
+ if (challengeMatch) {
2120
+ log$1("Twitter challenge token detected:", challengeMatch[0]);
2121
+ }
2122
+ return {
2123
+ status: "error",
2124
+ err: new AuthenticationError(
2125
+ `Twitter blocked this login attempt due to suspicious activity (error 399). This is not an issue with your credentials - Twitter requires additional authentication.
2126
+
2127
+ Solutions:
2128
+ 1. Use cookie-based authentication (RECOMMENDED): Export cookies from your browser and use scraper.setCookies() - see README for details
2129
+ 2. Enable Two-Factor Authentication (2FA) on your account and provide totp_secret
2130
+ 3. Wait 15 minutes before retrying (Twitter rate limit for suspicious logins)
2131
+ 4. Login via browser first to establish device trust
2132
+
2133
+ Original error: ${message}`
2134
+ )
2135
+ };
2136
+ }
1042
2137
  return {
1043
2138
  status: "error",
1044
2139
  err: new AuthenticationError(
@@ -1046,6 +2141,15 @@ class TwitterUserAuth extends TwitterGuestAuth {
1046
2141
  )
1047
2142
  };
1048
2143
  }
2144
+ if (!res.ok) {
2145
+ return { status: "error", err: new ApiError(res, flow) };
2146
+ }
2147
+ if (flow?.flow_token == null) {
2148
+ return {
2149
+ status: "error",
2150
+ err: new AuthenticationError("flow_token not found.")
2151
+ };
2152
+ }
1049
2153
  if (typeof flow.flow_token !== "string") {
1050
2154
  return {
1051
2155
  status: "error",
@@ -1053,7 +2157,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
1053
2157
  };
1054
2158
  }
1055
2159
  const subtask = flow.subtasks?.length ? flow.subtasks[0] : void 0;
1056
- Check(TwitterUserAuthSubtask, subtask);
2160
+ if (subtask && !Check(TwitterUserAuthSubtask, subtask)) {
2161
+ log$1(
2162
+ "WARNING: Subtask failed schema validation: %s",
2163
+ subtask.subtask_id ?? "unknown"
2164
+ );
2165
+ }
1057
2166
  if (subtask && subtask.subtask_id === "DenyLoginSubtask") {
1058
2167
  return {
1059
2168
  status: "error",
@@ -1065,19 +2174,26 @@ class TwitterUserAuth extends TwitterGuestAuth {
1065
2174
  response: flow
1066
2175
  };
1067
2176
  }
1068
- }
2177
+ };
2178
+ /**
2179
+ * Maximum allowed size (in bytes) for the JS instrumentation script.
2180
+ * Twitter's scripts are typically ~50-100KB. Anything significantly larger
2181
+ * may indicate tampering or an unexpected response.
2182
+ */
2183
+ _TwitterUserAuth.JS_INSTRUMENTATION_MAX_SIZE = 512 * 1024;
2184
+ let TwitterUserAuth = _TwitterUserAuth;
1069
2185
 
1070
2186
  const endpoints = {
1071
- UserTweets: "https://x.com/i/api/graphql/oRJs8SLCRNRbQzuZG93_oA/UserTweets?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22payments_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
1072
- UserTweetsAndReplies: "https://x.com/i/api/graphql/Hk4KlJ-ONjlJsucqR55P7g/UserTweetsAndReplies?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
1073
- UserLikedTweets: "https://x.com/i/api/graphql/XHTMjDbiTGLQ9cP1em-aqQ/Likes?variables=%7B%22userId%22%3A%222244196397%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Afalse%2C%22withClientEventToken%22%3Afalse%2C%22withBirdwatchNotes%22%3Afalse%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
1074
- UserByScreenName: "https://api.x.com/graphql/-oaLodhGbbnzJBACb1kk2Q/UserByScreenName?variables=%7B%22screen_name%22%3A%22geminiapp%22%2C%22withGrokTranslatedBio%22%3Afalse%7D&features=%7B%22hidden_profile_subscriptions_enabled%22%3Atrue%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22subscriptions_verification_info_is_identity_verified_enabled%22%3Atrue%2C%22subscriptions_verification_info_verified_since_enabled%22%3Atrue%2C%22highlights_tweets_tab_ui_enabled%22%3Atrue%2C%22responsive_web_twitter_article_notes_tab_enabled%22%3Atrue%2C%22subscriptions_feature_can_gift_premium%22%3Atrue%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%7D&fieldToggles=%7B%22withPayments%22%3Afalse%2C%22withAuxiliaryUserLabels%22%3Atrue%7D",
1075
- TweetDetail: "https://x.com/i/api/graphql/YVyS4SfwYW7Uw5qwy0mQCA/TweetDetail?variables=%7B%22focalTweetId%22%3A%221985465713096794294%22%2C%22referrer%22%3A%22profile%22%2C%22with_rux_injections%22%3Afalse%2C%22rankingMode%22%3A%22Relevance%22%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withBirdwatchNotes%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22payments_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticleRichContentState%22%3Atrue%2C%22withArticlePlainText%22%3Afalse%2C%22withGrokAnalyze%22%3Afalse%2C%22withDisallowedReplyControls%22%3Afalse%7D",
1076
- TweetResultByRestId: "https://api.x.com/graphql/tCVRZ3WCvoj0BVO7BKnL-Q/TweetResultByRestId?variables=%7B%22tweetId%22%3A%221985465713096794294%22%2C%22withCommunity%22%3Afalse%2C%22includePromotedContent%22%3Afalse%2C%22withVoice%22%3Afalse%7D&features=%7B%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Afalse%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22payments_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticleRichContentState%22%3Atrue%2C%22withArticlePlainText%22%3Afalse%2C%22withGrokAnalyze%22%3Afalse%2C%22withDisallowedReplyControls%22%3Afalse%7D",
1077
- ListTweets: "https://x.com/i/api/graphql/S1Sm3_mNJwa-fnY9htcaAQ/ListLatestTweetsTimeline?variables=%7B%22listId%22%3A%221736495155002106192%22%2C%22count%22%3A20%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D",
1078
- SearchTimeline: "https://x.com/i/api/graphql/nK1dw4oV3k4w5TdtcAdSww/SearchTimeline?variables=%7B%22rawQuery%22%3A%22twitter%22%2C%22count%22%3A20%2C%22querySource%22%3A%22typed_query%22%2C%22product%22%3A%22Top%22%2C%22withGrokTranslatedBio%22%3Afalse%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_media_download_video_enabled%22%3Afalse%2C%22rweb_lists_timeline_redesign_enabled%22%3Atrue%7D",
1079
- Followers: "https://x.com/i/api/graphql/SCu9fVIlCUm-BM8-tL5pkQ/Followers?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Afalse%2C%22withGrokTranslatedBio%22%3Afalse%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D",
1080
- Following: "https://x.com/i/api/graphql/S5xUN9s2v4xk50KWGGvyvQ/Following?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Afalse%2C%22withGrokTranslatedBio%22%3Afalse%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D"
2187
+ UserTweets: "https://api.x.com/graphql/eApPT8jppbYXlweF_ByTyA/UserTweets?variables=%7B%22userId%22%3A%2244196397%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22responsive_web_grok_annotations_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22post_ctas_fetch_enabled%22%3Atrue%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
2188
+ UserTweetsAndReplies: "https://api.x.com/graphql/aDl2OEiH_EFH10mA_ewZ9A/UserTweetsAndReplies?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22responsive_web_grok_annotations_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22post_ctas_fetch_enabled%22%3Atrue%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
2189
+ UserLikedTweets: "https://api.x.com/graphql/JPxbOQGc_tXQ0Y29mvHKSw/Likes?variables=%7B%22userId%22%3A%222244196397%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Afalse%2C%22withClientEventToken%22%3Afalse%2C%22withBirdwatchNotes%22%3Afalse%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22responsive_web_grok_annotations_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22post_ctas_fetch_enabled%22%3Atrue%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
2190
+ UserByScreenName: "https://api.x.com/graphql/AWbeRIdkLtqTRN7yL_H8yw/UserByScreenName?variables=%7B%22screen_name%22%3A%22elonmusk%22%2C%22withGrokTranslatedBio%22%3Atrue%7D&features=%7B%22hidden_profile_subscriptions_enabled%22%3Atrue%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22subscriptions_verification_info_is_identity_verified_enabled%22%3Atrue%2C%22subscriptions_verification_info_verified_since_enabled%22%3Atrue%2C%22highlights_tweets_tab_ui_enabled%22%3Atrue%2C%22responsive_web_twitter_article_notes_tab_enabled%22%3Atrue%2C%22subscriptions_feature_can_gift_premium%22%3Atrue%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%7D&fieldToggles=%7B%22withPayments%22%3Afalse%2C%22withAuxiliaryUserLabels%22%3Atrue%7D",
2191
+ TweetDetail: "https://api.x.com/graphql/ooUbmy0T2DmvwfjgARktiQ/TweetDetail?variables=%7B%22focalTweetId%22%3A%221985465713096794294%22%2C%22with_rux_injections%22%3Afalse%2C%22rankingMode%22%3A%22Relevance%22%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withBirdwatchNotes%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22responsive_web_grok_annotations_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22post_ctas_fetch_enabled%22%3Atrue%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticleRichContentState%22%3Atrue%2C%22withArticlePlainText%22%3Afalse%2C%22withGrokAnalyze%22%3Afalse%2C%22withDisallowedReplyControls%22%3Afalse%7D",
2192
+ TweetResultByRestId: "https://api.x.com/graphql/d6YKjvQ920F-D4Y1PruO-A/TweetResultByRestId?variables=%7B%22tweetId%22%3A%221985465713096794294%22%2C%22includePromotedContent%22%3Atrue%2C%22withBirdwatchNotes%22%3Atrue%2C%22withVoice%22%3Atrue%2C%22withCommunity%22%3Atrue%7D&features=%7B%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22responsive_web_grok_annotations_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22post_ctas_fetch_enabled%22%3Atrue%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticleRichContentState%22%3Atrue%2C%22withArticlePlainText%22%3Afalse%7D",
2193
+ ListTweets: "https://api.x.com/graphql/aJxgBm1YveGJCRiWJFx5WA/ListLatestTweetsTimeline?variables=%7B%22listId%22%3A%221736495155002106192%22%2C%22count%22%3A20%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22responsive_web_grok_annotations_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22post_ctas_fetch_enabled%22%3Atrue%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D",
2194
+ SearchTimeline: "https://api.x.com/graphql/nK1dw4oV3k4w5TdtcAdSww/SearchTimeline?variables=%7B%22rawQuery%22%3A%22twitter%22%2C%22count%22%3A20%2C%22querySource%22%3A%22typed_query%22%2C%22product%22%3A%22Top%22%2C%22withGrokTranslatedBio%22%3Afalse%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_media_download_video_enabled%22%3Afalse%2C%22rweb_lists_timeline_redesign_enabled%22%3Atrue%7D",
2195
+ Followers: "https://api.x.com/graphql/efNzdTpE-mkUcLARCd3RPQ/Followers?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Afalse%2C%22withGrokTranslatedBio%22%3Afalse%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22responsive_web_grok_annotations_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22post_ctas_fetch_enabled%22%3Atrue%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D",
2196
+ Following: "https://api.x.com/graphql/M3LO-sJg6BCWdEliN_C2fQ/Following?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Afalse%2C%22withGrokTranslatedBio%22%3Afalse%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Atrue%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22responsive_web_grok_annotations_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Atrue%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22post_ctas_fetch_enabled%22%3Atrue%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D"
1081
2197
  };
1082
2198
  class ApiRequest {
1083
2199
  constructor(info) {
@@ -1876,6 +2992,15 @@ function parseRelationshipTimeline(timeline) {
1876
2992
  if (!profile.userId) {
1877
2993
  profile.userId = userResultRaw.rest_id;
1878
2994
  }
2995
+ if (!profile.username && userResultRaw.core?.screen_name) {
2996
+ profile.username = userResultRaw.core.screen_name;
2997
+ profile.url = `https://x.com/${profile.username}`;
2998
+ }
2999
+ if (!profile.joined && userResultRaw.core?.created_at) {
3000
+ profile.joined = new Date(
3001
+ Date.parse(userResultRaw.core.created_at)
3002
+ );
3003
+ }
1879
3004
  profiles.push(profile);
1880
3005
  }
1881
3006
  } else if (entry.content?.cursorType === "Bottom") {
@@ -2428,6 +3553,7 @@ function findDmConversationsByUserId(inbox, userId) {
2428
3553
  return conversations;
2429
3554
  }
2430
3555
 
3556
+ const log = debug("twitter-scraper:scraper");
2431
3557
  const twUrl = "https://x.com";
2432
3558
  class Scraper {
2433
3559
  /**
@@ -2437,6 +3563,7 @@ class Scraper {
2437
3563
  */
2438
3564
  constructor(options) {
2439
3565
  this.options = options;
3566
+ this.subtaskHandlers = /* @__PURE__ */ new Map();
2440
3567
  this.token = bearerToken;
2441
3568
  this.useGuestAuth();
2442
3569
  }
@@ -2447,6 +3574,7 @@ class Scraper {
2447
3574
  * @param subtaskHandler The handler function to register.
2448
3575
  */
2449
3576
  registerAuthSubtaskHandler(subtaskId, subtaskHandler) {
3577
+ this.subtaskHandlers.set(subtaskId, subtaskHandler);
2450
3578
  if (this.auth instanceof TwitterUserAuth) {
2451
3579
  this.auth.registerSubtaskHandler(subtaskId, subtaskHandler);
2452
3580
  }
@@ -2454,6 +3582,15 @@ class Scraper {
2454
3582
  this.authTrends.registerSubtaskHandler(subtaskId, subtaskHandler);
2455
3583
  }
2456
3584
  }
3585
+ /**
3586
+ * Applies all stored subtask handlers to the given auth instance.
3587
+ * @internal
3588
+ */
3589
+ applySubtaskHandlers(auth) {
3590
+ for (const [subtaskId, handler] of this.subtaskHandlers) {
3591
+ auth.registerSubtaskHandler(subtaskId, handler);
3592
+ }
3593
+ }
2457
3594
  /**
2458
3595
  * Initializes auth properties using a guest token.
2459
3596
  * Used when creating a new instance of this class, and when logging out.
@@ -2754,7 +3891,8 @@ class Scraper {
2754
3891
  * @param twoFactorSecret The secret to generate two factor authentication tokens with, if you have two factor authentication enabled.
2755
3892
  */
2756
3893
  async login(username, password, email, twoFactorSecret) {
2757
- const userAuth = new TwitterUserAuth(this.token, this.getAuthOptions());
3894
+ const userAuth = new TwitterUserAuth(bearerToken2, this.getAuthOptions());
3895
+ this.applySubtaskHandlers(userAuth);
2758
3896
  await userAuth.login(username, password, email, twoFactorSecret);
2759
3897
  this.auth = userAuth;
2760
3898
  this.authTrends = userAuth;
@@ -2781,12 +3919,37 @@ class Scraper {
2781
3919
  * @param cookies The cookies to set for the current session.
2782
3920
  */
2783
3921
  async setCookies(cookies) {
2784
- const userAuth = new TwitterUserAuth(this.token, this.getAuthOptions());
3922
+ const userAuth = new TwitterUserAuth(bearerToken2, this.getAuthOptions());
3923
+ this.applySubtaskHandlers(userAuth);
2785
3924
  for (const cookie of cookies) {
2786
- await userAuth.cookieJar().setCookie(cookie, twUrl);
3925
+ if (cookie == null) continue;
3926
+ if (typeof cookie === "string") {
3927
+ try {
3928
+ await userAuth.cookieJar().setCookie(cookie, "https://x.com");
3929
+ } catch (err) {
3930
+ log(`Failed to parse cookie string: ${err.message}`);
3931
+ }
3932
+ } else {
3933
+ if (cookie.domain && cookie.domain.startsWith(".")) {
3934
+ cookie.domain = cookie.domain.substring(1);
3935
+ cookie.hostOnly = false;
3936
+ }
3937
+ const cookieDomain = cookie.domain || "x.com";
3938
+ const cookieUrl = `https://${cookieDomain}`;
3939
+ await userAuth.cookieJar().setCookie(cookie, cookieUrl);
3940
+ }
2787
3941
  }
2788
3942
  this.auth = userAuth;
2789
3943
  this.authTrends = userAuth;
3944
+ const isLoggedIn = await userAuth.isLoggedIn();
3945
+ if (!isLoggedIn) {
3946
+ const cookieString = await userAuth.cookieJar().getCookies(twUrl).then((c) => c.map((cookie) => cookie.key));
3947
+ if (cookieString.includes("ct0") && !cookieString.includes("auth_token")) {
3948
+ log(
3949
+ "auth_token cookie is missing. This is required for authenticated API access. The auth_token is an HttpOnly cookie that cannot be accessed via document.cookie. Export it using a browser extension (e.g., EditThisCookie) or DevTools Application tab."
3950
+ );
3951
+ }
3952
+ }
2790
3953
  }
2791
3954
  /**
2792
3955
  * Clear all cookies for the current session.
@@ -2828,7 +3991,9 @@ class Scraper {
2828
3991
  rateLimitStrategy: this.options?.rateLimitStrategy,
2829
3992
  experimental: {
2830
3993
  xClientTransactionId: this.options?.experimental?.xClientTransactionId,
2831
- xpff: this.options?.experimental?.xpff
3994
+ xpff: this.options?.experimental?.xpff,
3995
+ flowStepDelay: this.options?.experimental?.flowStepDelay,
3996
+ browserProfile: this.options?.experimental?.browserProfile
2832
3997
  }
2833
3998
  };
2834
3999
  }
@@ -2840,5 +4005,5 @@ class Scraper {
2840
4005
  }
2841
4006
  }
2842
4007
 
2843
- export { ApiError, AuthenticationError, ErrorRateLimitStrategy, Scraper, SearchMode, WaitingRateLimitStrategy };
4008
+ export { ApiError, AuthenticationError, ErrorRateLimitStrategy, Scraper, SearchMode, WaitingRateLimitStrategy, randomizeBrowserProfile };
2844
4009
  //# sourceMappingURL=index.mjs.map