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