UNPKG

@the-convocation/twitter-scraper

Version:
1,525 lines (1,511 loc) 158 kB
'use strict'; var debug = require('debug'); var toughCookie = require('tough-cookie'); var setCookie = require('set-cookie-parser'); var headersPolyfill = require('headers-polyfill'); var fetch = require('cross-fetch'); var typebox = require('@sinclair/typebox'); var value = require('@sinclair/typebox/value'); var OTPAuth = require('otpauth'); var stringify = require('json-stable-stringify'); var tls = require('node:tls'); var node_crypto = require('node:crypto'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var OTPAuth__namespace = /*#__PURE__*/_interopNamespaceDefault(OTPAuth); class ApiError extends Error { constructor(response, data) { super( `Response status: ${response.status} | headers: ${JSON.stringify( headersToString(response.headers) )} | data: ${typeof data === "string" ? data : JSON.stringify(data)}` ); this.response = response; this.data = data; } static async fromResponse(response) { let data = void 0; try { if (response.headers.get("content-type")?.includes("application/json")) { data = await response.json(); } else { data = await response.text(); } } catch { try { data = await response.text(); } catch { } } return new ApiError(response, data); } } function headersToString(headers) { const result = []; headers.forEach((value, key) => { result.push(`${key}: ${value}`); }); return result.join("\n"); } class AuthenticationError extends Error { constructor(message) { super(message || "Authentication failed"); this.name = "AuthenticationError"; } } const log$8 = debug("twitter-scraper:rate-limit"); class WaitingRateLimitStrategy { async onRateLimit({ response: res }) { const xRateLimitLimit = res.headers.get("x-rate-limit-limit"); const xRateLimitRemaining = res.headers.get("x-rate-limit-remaining"); const xRateLimitReset = res.headers.get("x-rate-limit-reset"); log$8( `Rate limit event: limit=${xRateLimitLimit}, remaining=${xRateLimitRemaining}, reset=${xRateLimitReset}` ); if (xRateLimitRemaining == "0" && xRateLimitReset) { const currentTime = (/* @__PURE__ */ new Date()).valueOf() / 1e3; const timeDeltaMs = 1e3 * (parseInt(xRateLimitReset) - currentTime); await new Promise((resolve) => setTimeout(resolve, timeDeltaMs)); } } } class ErrorRateLimitStrategy { async onRateLimit({ response: res }) { throw await ApiError.fromResponse(res); } } const log$7 = debug("twitter-scraper:castle"); var FieldEncoding = /* @__PURE__ */ ((FieldEncoding2) => { FieldEncoding2[FieldEncoding2["Empty"] = -1] = "Empty"; FieldEncoding2[FieldEncoding2["Marker"] = 1] = "Marker"; FieldEncoding2[FieldEncoding2["Byte"] = 3] = "Byte"; FieldEncoding2[FieldEncoding2["EncryptedBytes"] = 4] = "EncryptedBytes"; FieldEncoding2[FieldEncoding2["CompactInt"] = 5] = "CompactInt"; FieldEncoding2[FieldEncoding2["RoundedByte"] = 6] = "RoundedByte"; FieldEncoding2[FieldEncoding2["RawAppend"] = 7] = "RawAppend"; return FieldEncoding2; })(FieldEncoding || {}); const TWITTER_CASTLE_PK = "AvRa79bHyJSYSQHnRpcVtzyxetSvFerx"; const XXTEA_KEY = [1164413191, 3891440048, 185273099, 2746598870]; const PER_FIELD_KEY_TAIL = [ 16373134, 643144773, 1762804430, 1186572681, 1164413191 ]; const TS_EPOCH = 1535e6; const SDK_VERSION = 27008; const TOKEN_VERSION = 11; const FP_PART = { DEVICE: 0, // Part 1: hardware/OS/rendering fingerprint BROWSER: 4, // Part 2: browser environment fingerprint TIMING: 7 // Part 3: timing-based fingerprint }; const DEFAULT_PROFILE = { locale: "en-US", language: "en", timezone: "America/New_York", screenWidth: 1920, screenHeight: 1080, availableWidth: 1920, availableHeight: 1032, // 1080 minus Windows taskbar (~48px) gpuRenderer: "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti Direct3D11 vs_5_0 ps_5_0, D3D11)", deviceMemoryGB: 8, hardwareConcurrency: 24, colorDepth: 24, devicePixelRatio: 1 }; const SCREEN_RESOLUTIONS = [ { w: 1920, h: 1080, ah: 1032 }, { w: 2560, h: 1440, ah: 1392 }, { w: 1366, h: 768, ah: 720 }, { w: 1536, h: 864, ah: 816 }, { w: 1440, h: 900, ah: 852 }, { w: 1680, h: 1050, ah: 1002 }, { w: 3840, h: 2160, ah: 2112 } ]; const DEVICE_MEMORY_VALUES = [4, 8, 8, 16]; const HARDWARE_CONCURRENCY_VALUES = [4, 8, 8, 12, 16, 24]; function randomizeBrowserProfile() { const screen = SCREEN_RESOLUTIONS[randInt(0, SCREEN_RESOLUTIONS.length - 1)]; return { ...DEFAULT_PROFILE, screenWidth: screen.w, screenHeight: screen.h, availableWidth: screen.w, availableHeight: screen.ah, // gpuRenderer intentionally NOT randomized — see JSDoc above deviceMemoryGB: DEVICE_MEMORY_VALUES[randInt(0, DEVICE_MEMORY_VALUES.length - 1)], hardwareConcurrency: HARDWARE_CONCURRENCY_VALUES[randInt(0, HARDWARE_CONCURRENCY_VALUES.length - 1)] }; } function getRandomBytes(n) { const buf = new Uint8Array(n); if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.getRandomValues) { globalThis.crypto.getRandomValues(buf); } else { for (let i = 0; i < n; i++) buf[i] = Math.floor(Math.random() * 256); } return buf; } function randInt(min, max) { return min + Math.floor(Math.random() * (max - min + 1)); } function randFloat(min, max) { return min + Math.random() * (max - min); } function concat(...arrays) { const len = arrays.reduce((s, a) => s + a.length, 0); const out = new Uint8Array(len); let off = 0; for (const a of arrays) { out.set(a, off); off += a.length; } return out; } function toHex(input) { return Array.from(input).map((b) => b.toString(16).padStart(2, "0")).join(""); } function fromHex(hex) { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) out[i / 2] = parseInt(hex.substring(i, i + 2), 16); return out; } function textEnc(s) { return new TextEncoder().encode(s); } function u8(...vals) { return new Uint8Array(vals); } function be16(v) { return u8(v >>> 8 & 255, v & 255); } function be32(v) { return u8(v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255); } function xorBytes(data, key) { const out = new Uint8Array(data.length); for (let i = 0; i < data.length; i++) out[i] = data[i] ^ key[i % key.length]; return out; } function xorNibbles(nibbles, keyNibble) { const k = parseInt(keyNibble, 16); return nibbles.split("").map((n) => (parseInt(n, 16) ^ k).toString(16)).join(""); } function base64url(data) { if (typeof Buffer !== "undefined") { return Buffer.from(data).toString("base64url"); } let bin = ""; for (let i = 0; i < data.length; i++) bin += String.fromCharCode(data[i]); return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function xxteaEncrypt(data, key) { const padLen = Math.ceil(data.length / 4) * 4; const padded = new Uint8Array(padLen); padded.set(data); const n = padLen / 4; const v = new Uint32Array(n); for (let i = 0; i < n; i++) { v[i] = (padded[i * 4] | padded[i * 4 + 1] << 8 | padded[i * 4 + 2] << 16 | padded[i * 4 + 3] << 24) >>> 0; } if (n <= 1) return padded; const k = new Uint32Array(key.map((x) => x >>> 0)); const DELTA = 2654435769; const u = n - 1; let sum = 0; let z = v[u]; let y; let rounds = 6 + Math.floor(52 / (u + 1)); while (rounds-- > 0) { sum = sum + DELTA >>> 0; const e = sum >>> 2 & 3; for (let p = 0; p < u; p++) { y = v[p + 1]; const mx2 = ((z >>> 5 ^ y << 2) >>> 0) + ((y >>> 3 ^ z << 4) >>> 0) ^ ((sum ^ y) >>> 0) + ((k[p & 3 ^ e] ^ z) >>> 0); v[p] = v[p] + mx2 >>> 0; z = v[p]; } y = v[0]; const mx = ((z >>> 5 ^ y << 2) >>> 0) + ((y >>> 3 ^ z << 4) >>> 0) ^ ((sum ^ y) >>> 0) + ((k[u & 3 ^ e] ^ z) >>> 0); v[u] = v[u] + mx >>> 0; z = v[u]; } const out = new Uint8Array(n * 4); for (let i = 0; i < n; i++) { out[i * 4] = v[i] & 255; out[i * 4 + 1] = v[i] >>> 8 & 255; out[i * 4 + 2] = v[i] >>> 16 & 255; out[i * 4 + 3] = v[i] >>> 24 & 255; } return out; } function fieldEncrypt(data, fieldIndex, initTime) { return xxteaEncrypt(data, [ fieldIndex, Math.floor(initTime), ...PER_FIELD_KEY_TAIL ]); } function encodeTimestampBytes(ms) { let t = Math.floor(ms / 1e3 - TS_EPOCH); t = Math.max(Math.min(t, 268435455), 0); return be32(t); } function xorAndAppendKey(buf, key) { const hex = toHex(buf); const keyNib = (key & 15).toString(16); return xorNibbles(hex.substring(1), keyNib) + keyNib; } function encodeTimestampEncrypted(ms) { const tsBytes = encodeTimestampBytes(ms); const slice = Math.floor(ms) % 1e3; const sliceBytes = be16(slice); const k = randInt(0, 15); return xorAndAppendKey(tsBytes, k) + xorAndAppendKey(sliceBytes, k); } function deriveAndXor(keyHex, sliceLen, rotChar, data) { const sub = keyHex.substring(0, sliceLen).split(""); if (sub.length === 0) return data; const rot = parseInt(rotChar, 16) % sub.length; const rotated = sub.slice(rot).concat(sub.slice(0, rot)).join(""); return xorBytes(data, fromHex(rotated)); } function customFloatEncode(expBits, manBits, value) { if (value === 0) return 0; let n = Math.abs(value); let exp = 0; while (2 <= n) { n /= 2; exp++; } while (n < 1 && n > 0) { n *= 2; exp--; } exp = Math.min(exp, (1 << expBits) - 1); const frac = n - Math.floor(n); let mantissa = 0; if (frac > 0) { let pos = 1; let tmp = frac; while (tmp !== 0 && pos <= manBits) { tmp *= 2; const bit = Math.floor(tmp); mantissa |= bit << manBits - pos; tmp -= bit; pos++; } } return exp << manBits | mantissa; } function encodeFloatVal(v) { const n = Math.max(v, 0); if (n <= 15) return 64 | customFloatEncode(2, 4, n + 1); return 128 | customFloatEncode(4, 3, n - 14); } function encodeField(index, encoding, val, initTime) { const hdr = u8((31 & index) << 3 | 7 & encoding); if (encoding === -1 /* Empty */ || encoding === 1 /* Marker */) return hdr; let body; switch (encoding) { case 3 /* Byte */: body = u8(val); break; case 6 /* RoundedByte */: body = u8(Math.round(val)); break; case 5 /* CompactInt */: { const v = val; body = v <= 127 ? u8(v) : be16(1 << 15 | 32767 & v); break; } case 4 /* EncryptedBytes */: { if (initTime == null) { throw new Error("initTime is required for EncryptedBytes encoding"); } const enc = fieldEncrypt(val, index, initTime); body = concat(u8(enc.length), enc); break; } case 7 /* RawAppend */: body = val instanceof Uint8Array ? val : u8(val); break; default: body = new Uint8Array(0); } return concat(hdr, body); } function encodeBits(bits, byteSize) { const numBytes = byteSize / 8; const arr = new Uint8Array(numBytes); for (const bit of bits) { const bi = numBytes - 1 - Math.floor(bit / 8); if (bi >= 0 && bi < numBytes) arr[bi] |= 1 << bit % 8; } return arr; } function screenDimBytes(screen, avail) { const r = 32767 & screen; const e = 65535 & avail; return r === e ? be16(32768 | r) : concat(be16(r), be16(e)); } function boolsToBin(arr, totalBits) { const e = arr.length > totalBits ? arr.slice(0, totalBits) : arr; const c = e.length; let r = 0; for (let i = c - 1; i >= 0; i--) { if (e[i]) r |= 1 << c - i - 1; } if (c < totalBits) r <<= totalBits - c; return r; } function encodeCodecPlayability() { const codecs = { webm: 2, // VP8/VP9 mp4: 2, // H.264 ogg: 0, // Theora (Chrome dropped support) aac: 2, // AAC audio xm4a: 1, // M4A container wav: 2, // PCM audio mpeg: 2, // MP3 audio ogg2: 2 // Vorbis audio }; const bits = Object.values(codecs).map((c) => c.toString(2).padStart(2, "0")).join(""); return be16(parseInt(bits, 2)); } const TIMEZONE_ENUM = { "America/New_York": 0, "America/Sao_Paulo": 1, "America/Chicago": 2, "America/Los_Angeles": 3, "America/Mexico_City": 4, "Asia/Shanghai": 5 }; function getTimezoneInfo(tz) { const knownOffsets = { "America/New_York": { offset: 20, dstDiff: 4 }, "America/Chicago": { offset: 24, dstDiff: 4 }, "America/Los_Angeles": { offset: 32, dstDiff: 4 }, "America/Denver": { offset: 28, dstDiff: 4 }, "America/Sao_Paulo": { offset: 12, dstDiff: 4 }, "America/Mexico_City": { offset: 24, dstDiff: 4 }, "Asia/Shanghai": { offset: 246, dstDiff: 0 }, "Asia/Tokyo": { offset: 220, dstDiff: 0 }, "Europe/London": { offset: 0, dstDiff: 4 }, "Europe/Berlin": { offset: 252, dstDiff: 4 }, UTC: { offset: 0, dstDiff: 0 } }; try { const now = /* @__PURE__ */ new Date(); const jan = new Date(now.getFullYear(), 0, 1); const jul = new Date(now.getFullYear(), 6, 1); const getOffset = (date, zone) => { const utc = new Date(date.toLocaleString("en-US", { timeZone: "UTC" })); const local = new Date(date.toLocaleString("en-US", { timeZone: zone })); return (utc.getTime() - local.getTime()) / 6e4; }; const currentOffset = getOffset(now, tz); const janOffset = getOffset(jan, tz); const julOffset = getOffset(jul, tz); const dstDifference = Math.abs(janOffset - julOffset); return { offset: Math.floor(currentOffset / 15) & 255, dstDiff: Math.floor(dstDifference / 15) & 255 }; } catch { return knownOffsets[tz] || { offset: 20, dstDiff: 4 }; } } function buildDeviceFingerprint(initTime, profile, userAgent) { const tz = getTimezoneInfo(profile.timezone); const { Byte, EncryptedBytes, CompactInt, RoundedByte, RawAppend } = FieldEncoding; const encryptedUA = fieldEncrypt(textEnc(userAgent), 12, initTime); const uaPayload = concat(u8(1), u8(encryptedUA.length), encryptedUA); const fields = [ encodeField(0, Byte, 1), // Platform: Win32 encodeField(1, Byte, 0), // Vendor: Google Inc. encodeField(2, EncryptedBytes, textEnc(profile.locale), initTime), // Locale encodeField(3, RoundedByte, profile.deviceMemoryGB * 10), // Device memory (GB * 10) encodeField( 4, RawAppend, concat( // Screen dimensions (width + height) screenDimBytes(profile.screenWidth, profile.availableWidth), screenDimBytes(profile.screenHeight, profile.availableHeight) ) ), encodeField(5, CompactInt, profile.colorDepth), // Screen color depth encodeField(6, CompactInt, profile.hardwareConcurrency), // CPU logical cores encodeField(7, RoundedByte, profile.devicePixelRatio * 10), // Pixel ratio (* 10) encodeField(8, RawAppend, u8(tz.offset, tz.dstDiff)), // Timezone offset info // MIME type hash — captured from Chrome 144 on Windows 10. // Source: yubie-re/castleio-gen (Python SDK, MIT license). encodeField(9, RawAppend, u8(2, 125, 95, 201, 167)), // Browser plugins hash — Chrome no longer exposes plugins to navigator.plugins, // so this is a fixed hash. Source: yubie-re/castleio-gen (Python SDK, MIT license). encodeField(10, RawAppend, u8(5, 114, 147, 2, 8)), encodeField( 11, RawAppend, // Browser feature flags concat(u8(12), encodeBits([0, 1, 2, 3, 4, 5, 6], 16)) ), encodeField(12, RawAppend, uaPayload), // User agent (encrypted) // Canvas font rendering hash — generated by Castle.io SDK's canvas fingerprinting (text rendering). // Captured from Chrome 144 on Windows 10. Source: yubie-re/castleio-gen (Python SDK, MIT license). encodeField(13, EncryptedBytes, textEnc("54b4b5cf"), initTime), encodeField( 14, RawAppend, // Media input devices concat(u8(3), encodeBits([0, 1, 2], 8)) ), // Fields 15 (DoNotTrack) and 16 (JavaEnabled) intentionally omitted encodeField(17, Byte, 0), // productSub type // Canvas circle rendering hash — generated by Castle.io SDK's canvas fingerprinting (arc drawing). // Captured from Chrome 144 on Windows 10. Source: yubie-re/castleio-gen (Python SDK, MIT license). encodeField(18, EncryptedBytes, textEnc("c6749e76"), initTime), encodeField(19, EncryptedBytes, textEnc(profile.gpuRenderer), initTime), // WebGL renderer encodeField( 20, EncryptedBytes, // Epoch locale string textEnc("12/31/1969, 7:00:00 PM"), initTime ), encodeField( 21, RawAppend, // WebDriver flags (none set) concat(u8(8), encodeBits([], 8)) ), encodeField(22, CompactInt, 33), // eval.toString() length // Field 23 (navigator.buildID) intentionally omitted (Chrome doesn't have it) encodeField(24, CompactInt, 12549), // Max recursion depth encodeField(25, Byte, 0), // Recursion error message type encodeField(26, Byte, 1), // Recursion error name type encodeField(27, CompactInt, 4644), // Stack trace string length encodeField(28, RawAppend, u8(0)), // Touch support metric encodeField(29, Byte, 3), // Undefined call error type // Navigator properties hash — hash of enumerable navigator property names. // Captured from Chrome 144 on Windows 10. Source: yubie-re/castleio-gen (Python SDK, MIT license). encodeField(30, RawAppend, u8(93, 197, 171, 181, 136)), encodeField(31, RawAppend, encodeCodecPlayability()) // Codec playability ]; const data = concat(...fields); const sizeIdx = (7 & FP_PART.DEVICE) << 5 | 31 & fields.length; return concat(u8(sizeIdx), data); } function buildBrowserFingerprint(profile, initTime) { const { Byte, EncryptedBytes, CompactInt, Marker, RawAppend } = FieldEncoding; const timezoneField = profile.timezone in TIMEZONE_ENUM ? encodeField(1, Byte, TIMEZONE_ENUM[profile.timezone]) : encodeField(1, EncryptedBytes, textEnc(profile.timezone), initTime); const fields = [ encodeField(0, Byte, 0), // Constant marker timezoneField, // Timezone encodeField( 2, EncryptedBytes, // Language list textEnc(`${profile.locale},${profile.language}`), initTime ), encodeField(6, CompactInt, 0), // Expected property count encodeField( 10, RawAppend, // Castle data bitfield concat(u8(4), encodeBits([1, 2, 3], 8)) ), encodeField(12, CompactInt, 80), // Negative error string length encodeField(13, RawAppend, u8(9, 0, 0)), // Driver check values encodeField( 17, RawAppend, // Chrome feature flags concat(u8(13), encodeBits([1, 5, 8, 9, 10], 16)) ), encodeField(18, Marker, 0), // Device logic expected encodeField(21, RawAppend, u8(0, 0, 0, 0)), // Class properties count encodeField(22, EncryptedBytes, textEnc(profile.locale), initTime), // User locale (secondary) encodeField( 23, RawAppend, // Worker capabilities concat(u8(2), encodeBits([0], 8)) ), encodeField( 24, RawAppend, // Inner/outer dimension diff concat(be16(0), be16(randInt(10, 30))) ) ]; const data = concat(...fields); const sizeIdx = (7 & FP_PART.BROWSER) << 5 | 31 & fields.length; return concat(u8(sizeIdx), data); } function buildTimingFingerprint(initTime) { const minute = new Date(initTime).getUTCMinutes(); const fields = [ encodeField(3, 5 /* CompactInt */, 1), // Time since window.open (ms) encodeField(4, 5 /* CompactInt */, minute) // Castle init time (minutes) ]; const data = concat(...fields); const sizeIdx = (7 & FP_PART.TIMING) << 5 | 31 & fields.length; return concat(u8(sizeIdx), data); } const EventType = { CLICK: 0, FOCUS: 5, BLUR: 6, ANIMATIONSTART: 18, MOUSEMOVE: 21, MOUSELEAVE: 25, MOUSEENTER: 26, RESIZE: 27 }; const HAS_TARGET_FLAG = 128; const TARGET_UNKNOWN = 63; function generateEventLog() { const simpleEvents = [ EventType.MOUSEMOVE, EventType.ANIMATIONSTART, EventType.MOUSELEAVE, EventType.MOUSEENTER, EventType.RESIZE ]; const targetedEvents = [ EventType.CLICK, EventType.BLUR, EventType.FOCUS ]; const allEvents = [...simpleEvents, ...targetedEvents]; const count = randInt(30, 70); const eventBytes = []; for (let i = 0; i < count; i++) { const eventId = allEvents[randInt(0, allEvents.length - 1)]; if (targetedEvents.includes(eventId)) { eventBytes.push(eventId | HAS_TARGET_FLAG); eventBytes.push(TARGET_UNKNOWN); } else { eventBytes.push(eventId); } } const inner = concat(u8(0), be16(count), new Uint8Array(eventBytes)); return concat(be16(inner.length), inner); } function buildBehavioralBitfield() { const flags = new Array(15).fill(false); flags[2] = true; flags[3] = true; flags[5] = true; flags[6] = true; flags[9] = true; flags[11] = true; flags[12] = true; const packedBits = boolsToBin(flags, 16); const encoded = 6 << 20 | 2 << 16 | 65535 & packedBits; return u8(encoded >>> 16 & 255, encoded >>> 8 & 255, encoded & 255); } const NO_DATA = -1; function buildFloatMetrics() { const metrics = [ // ── Mouse & key timing ── randFloat(40, 50), // 0: Mouse angle vector mean NO_DATA, // 1: Touch angle vector (no touch device) randFloat(70, 80), // 2: Key same-time difference NO_DATA, // 3: (unused) randFloat(60, 70), // 4: Mouse down-to-up time mean NO_DATA, // 5: (unused) 0, // 6: (zero placeholder) 0, // 7: Mouse click time difference // ── Duration distributions ── randFloat(60, 80), // 8: Mouse down-up duration median randFloat(5, 10), // 9: Mouse down-up duration std deviation randFloat(30, 40), // 10: Key press duration median randFloat(2, 5), // 11: Key press duration std deviation // ── Touch metrics (all disabled for desktop) ── NO_DATA, NO_DATA, NO_DATA, NO_DATA, // 12-15 NO_DATA, NO_DATA, NO_DATA, NO_DATA, // 16-19 // ── Mouse trajectory analysis ── randFloat(150, 180), // 20: Mouse movement angle mean randFloat(3, 6), // 21: Mouse movement angle std deviation randFloat(150, 180), // 22: Mouse movement angle mean (500ms window) randFloat(3, 6), // 23: Mouse movement angle std (500ms window) randFloat(0, 2), // 24: Mouse position deviation X randFloat(0, 2), // 25: Mouse position deviation Y 0, 0, // 26-27: (zero placeholders) // ── Touch sequential/gesture metrics (disabled) ── NO_DATA, NO_DATA, // 28-29 NO_DATA, NO_DATA, // 30-31 // ── Key pattern analysis ── 0, 0, // 32-33: Letter-digit transition ratio 0, 0, // 34-35: Digit-invalid transition ratio 0, 0, // 36-37: Double-invalid transition ratio // ── Mouse vector differences ── 1, 0, // 38-39: Mouse vector diff (mean, std) 1, 0, // 40-41: Mouse vector diff 2 (mean, std) randFloat(0, 4), // 42: Mouse vector diff (500ms mean) randFloat(0, 3), // 43: Mouse vector diff (500ms std) // ── Rounded movement metrics ── randFloat(25, 50), // 44: Mouse time diff (rounded mean) randFloat(25, 50), // 45: Mouse time diff (rounded std) randFloat(25, 50), // 46: Mouse vector diff (rounded mean) randFloat(25, 30), // 47: Mouse vector diff (rounded std) // ── Speed change analysis ── randFloat(0, 2), // 48: Mouse speed change mean randFloat(0, 1), // 49: Mouse speed change std randFloat(0, 1), // 50: Mouse vector 500ms aggregate // ── Trailing ── 1, // 51: Universal flag 0 // 52: Terminator ]; const out = new Uint8Array(metrics.length); for (let i = 0; i < metrics.length; i++) { out[i] = metrics[i] === NO_DATA ? 0 : encodeFloatVal(metrics[i]); } return out; } function buildEventCounts() { const counts = [ randInt(100, 200), // 0: mousemove events randInt(1, 5), // 1: keyup events randInt(1, 5), // 2: click events 0, // 3: touchstart events (none on desktop) randInt(0, 5), // 4: keydown events 0, // 5: touchmove events (none) 0, // 6: mousedown-mouseup pairs 0, // 7: vector diff samples randInt(0, 5), // 8: wheel events randInt(0, 11), // 9: (internal counter) randInt(0, 1) // 10: (internal counter) ]; return concat(new Uint8Array(counts), u8(counts.length)); } function buildBehavioralData() { return concat( buildBehavioralBitfield(), buildFloatMetrics(), buildEventCounts() ); } function buildTokenHeader(uuid, publisherKey, initTime) { const timestamp = fromHex(encodeTimestampEncrypted(initTime)); const version = be16(SDK_VERSION); const pkBytes = textEnc(publisherKey); const uuidBytes = fromHex(uuid); return concat(timestamp, version, pkBytes, uuidBytes); } function generateLocalCastleToken(userAgent, profileOverride) { const now = Date.now(); const profile = { ...DEFAULT_PROFILE, ...profileOverride }; const initTime = now - randFloat(2 * 60 * 1e3, 30 * 60 * 1e3); log$7("Generating local Castle.io v11 token"); const deviceFp = buildDeviceFingerprint(initTime, profile, userAgent); const browserFp = buildBrowserFingerprint(profile, initTime); const timingFp = buildTimingFingerprint(initTime); const eventLog = generateEventLog(); const behavioral = buildBehavioralData(); const fingerprintData = concat( deviceFp, browserFp, timingFp, eventLog, behavioral, u8(255) ); const sendTime = Date.now(); const timestampKey = encodeTimestampEncrypted(sendTime); const xorPass1 = deriveAndXor( timestampKey, 4, timestampKey[3], fingerprintData ); const tokenUuid = toHex(getRandomBytes(16)); const withTimestampPrefix = concat(fromHex(timestampKey), xorPass1); const xorPass2 = deriveAndXor( tokenUuid, 8, tokenUuid[9], withTimestampPrefix ); const header = buildTokenHeader(tokenUuid, TWITTER_CASTLE_PK, initTime); const plaintext = concat(header, xorPass2); const encrypted = xxteaEncrypt(plaintext, XXTEA_KEY); const paddingBytes = encrypted.length - plaintext.length; const versioned = concat(u8(TOKEN_VERSION, paddingBytes), encrypted); const randomByte = getRandomBytes(1)[0]; const checksum = versioned.length * 2 & 255; const withChecksum = concat(versioned, u8(checksum)); const xored = xorBytes(withChecksum, u8(randomByte)); const finalPayload = concat(u8(randomByte), xored); const token = base64url(finalPayload); log$7( `Generated castle token: ${token.length} chars, cuid: ${tokenUuid.substring( 0, 6 )}...` ); return { token, cuid: tokenUuid }; } const log$6 = debug("twitter-scraper:requests"); async function updateCookieJar(cookieJar, headers) { let setCookieHeaders = []; if (typeof headers.getSetCookie === "function") { setCookieHeaders = headers.getSetCookie(); } else { const setCookieHeader = headers.get("set-cookie"); if (setCookieHeader) { setCookieHeaders = setCookie.splitCookiesString(setCookieHeader); } } if (setCookieHeaders.length > 0) { for (const cookieStr of setCookieHeaders) { const cookie = toughCookie.Cookie.parse(cookieStr); if (!cookie) { log$6(`Failed to parse cookie: ${cookieStr.substring(0, 100)}`); continue; } if (cookie.maxAge === 0 || cookie.expires && cookie.expires < /* @__PURE__ */ new Date()) { if (cookie.key === "ct0") { log$6(`Skipping deletion of ct0 cookie (Max-Age=0)`); } continue; } try { const url = `${cookie.secure ? "https" : "http"}://${cookie.domain}${cookie.path}`; await cookieJar.setCookie(cookie, url); if (cookie.key === "ct0") { log$6( `Successfully set ct0 cookie with value: ${cookie.value.substring( 0, 20 )}...` ); } } catch (err) { log$6(`Failed to set cookie ${cookie.key}: ${err}`); if (cookie.key === "ct0") { log$6(`FAILED to set ct0 cookie! Error: ${err}`); } } } } else if (typeof document !== "undefined") { for (const cookie of document.cookie.split(";")) { const hardCookie = toughCookie.Cookie.parse(cookie); if (hardCookie) { await cookieJar.setCookie(hardCookie, document.location.toString()); } } } } const log$5 = debug("twitter-scraper:xpff"); let isoCrypto = null; async function getCrypto() { if (isoCrypto != null) { return isoCrypto; } if (typeof crypto === "undefined") { log$5("Global crypto is undefined, importing from crypto module..."); const { webcrypto } = await import('crypto'); isoCrypto = webcrypto; return webcrypto; } isoCrypto = crypto; return crypto; } async function sha256(message) { const msgBuffer = new TextEncoder().encode(message); const crypto2 = await getCrypto(); const hashBuffer = await crypto2.subtle.digest("SHA-256", msgBuffer); return new Uint8Array(hashBuffer); } function buf2hex(buffer) { return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join(""); } class XPFFHeaderGenerator { constructor(seed) { this.seed = seed; } async deriveKey(guestId) { const combined = `${this.seed}${guestId}`; const result = await sha256(combined); return result; } async generateHeader(plaintext, guestId) { log$5(`Generating XPFF key for guest ID: ${guestId}`); const key = await this.deriveKey(guestId); const crypto2 = await getCrypto(); const nonce = crypto2.getRandomValues(new Uint8Array(12)); const cipher = await crypto2.subtle.importKey( "raw", key, { name: "AES-GCM" }, false, ["encrypt"] ); const encrypted = await crypto2.subtle.encrypt( { name: "AES-GCM", iv: nonce }, cipher, new TextEncoder().encode(plaintext) ); const combined = new Uint8Array(nonce.length + encrypted.byteLength); combined.set(nonce); combined.set(new Uint8Array(encrypted), nonce.length); const result = buf2hex(combined.buffer); log$5(`XPFF header generated for guest ID ${guestId}: ${result}`); return result; } } const xpffBaseKey = "0e6be1f1e21ffc33590b888fd4dc81b19713e570e805d4e5df80a493c9571a05"; function xpffPlain() { const timestamp = Date.now(); return JSON.stringify({ navigator_properties: { hasBeenActive: "true", userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", webdriver: "false" }, created_at: timestamp }); } async function generateXPFFHeader(guestId) { const generator = new XPFFHeaderGenerator(xpffBaseKey); const plaintext = xpffPlain(); return generator.generateHeader(plaintext, guestId); } const log$4 = debug("twitter-scraper:auth"); function withTransform(fetchFn, transform) { return async (input, init) => { const fetchArgs = await transform?.request?.(input, init) ?? [ input, init ]; const res = await fetchFn(...fetchArgs); return await transform?.response?.(res) ?? res; }; } class TwitterGuestAuth { constructor(bearerToken, options) { this.options = options; this.fetch = withTransform(options?.fetch ?? fetch, options?.transform); this.rateLimitStrategy = options?.rateLimitStrategy ?? new WaitingRateLimitStrategy(); this.bearerToken = bearerToken; this.jar = new toughCookie.CookieJar(); } async onRateLimit(event) { await this.rateLimitStrategy.onRateLimit(event); } cookieJar() { return this.jar; } isLoggedIn() { return Promise.resolve(false); } // eslint-disable-next-line @typescript-eslint/no-unused-vars login(_username, _password, _email) { return this.updateGuestToken(); } logout() { this.deleteToken(); this.jar = new toughCookie.CookieJar(); return Promise.resolve(); } deleteToken() { delete this.guestToken; delete this.guestCreatedAt; } hasToken() { return this.guestToken != null; } authenticatedAt() { if (this.guestCreatedAt == null) { return null; } return new Date(this.guestCreatedAt); } /** * Install only authentication credentials (bearer token, guest token, cookies) * without browser fingerprint or platform headers. Useful for callers that * build their own header set (e.g. the login flow). */ async installAuthCredentials(headers, bearerTokenOverride) { const tokenToUse = bearerTokenOverride ?? this.bearerToken; headers.set("authorization", `Bearer ${tokenToUse}`); if (!bearerTokenOverride) { if (this.shouldUpdate()) { await this.updateGuestToken(); } if (this.guestToken) { headers.set("x-guest-token", this.guestToken); } } headers.set("cookie", await this.getCookieString()); } async installTo(headers, url, bearerTokenOverride) { await this.installAuthCredentials(headers, bearerTokenOverride); headers.set("user-agent", CHROME_USER_AGENT); if (!headers.has("accept")) { headers.set("accept", "*/*"); } headers.set("accept-language", "en-US,en;q=0.9"); headers.set("sec-ch-ua", CHROME_SEC_CH_UA); headers.set("sec-ch-ua-mobile", "?0"); headers.set("sec-ch-ua-platform", '"Windows"'); headers.set("referer", "https://x.com/"); headers.set("origin", "https://x.com"); headers.set("sec-fetch-site", "same-site"); headers.set("sec-fetch-mode", "cors"); headers.set("sec-fetch-dest", "empty"); headers.set("priority", "u=1, i"); if (!headers.has("content-type") && (url.includes("api.x.com/graphql/") || url.includes("x.com/i/api/graphql/"))) { headers.set("content-type", "application/json"); } await this.installCsrfToken(headers); if (this.options?.experimental?.xpff) { const guestId = await this.guestId(); if (guestId != null) { const xpffHeader = await generateXPFFHeader(guestId); headers.set("x-xp-forwarded-for", xpffHeader); } } } async installCsrfToken(headers) { const cookies = await this.getCookies(); const xCsrfToken = cookies.find((cookie) => cookie.key === "ct0"); if (xCsrfToken) { headers.set("x-csrf-token", xCsrfToken.value); } } async setCookie(key, value) { const cookie = toughCookie.Cookie.parse(`${key}=${value}`); if (!cookie) { throw new Error("Failed to parse cookie."); } await this.jar.setCookie(cookie, this.getCookieJarUrl()); if (typeof document !== "undefined") { document.cookie = cookie.toString(); } } async getCookies() { return this.jar.getCookies(this.getCookieJarUrl()); } async getCookieString() { const cookies = await this.getCookies(); return cookies.map((cookie) => `${cookie.key}=${cookie.value}`).join("; "); } async removeCookie(key) { const store = this.jar.store; const cookies = await this.jar.getCookies(this.getCookieJarUrl()); for (const cookie of cookies) { if (!cookie.domain || !cookie.path) continue; await store.removeCookie(cookie.domain, cookie.path, key); if (typeof document !== "undefined") { document.cookie = `${cookie.key}=; Max-Age=0; path=${cookie.path}; domain=${cookie.domain}`; } } } getCookieJarUrl() { return typeof document !== "undefined" ? document.location.toString() : "https://x.com"; } async guestId() { const cookies = await this.getCookies(); const guestIdCookie = cookies.find((cookie) => cookie.key === "guest_id"); return guestIdCookie ? guestIdCookie.value : null; } /** * Updates the authentication state with a new guest token from the Twitter API. */ async updateGuestToken() { try { await this.updateGuestTokenCore(); } catch (err) { log$4("Failed to update guest token; this may cause issues:", err); } } async updateGuestTokenCore() { const guestActivateUrl = "https://api.x.com/1.1/guest/activate.json"; const headers = new headersPolyfill.Headers({ authorization: `Bearer ${this.bearerToken}`, "user-agent": CHROME_USER_AGENT, accept: "*/*", "accept-language": "en-US,en;q=0.9", "content-type": "application/x-www-form-urlencoded", "sec-ch-ua": CHROME_SEC_CH_UA, "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', origin: "https://x.com", referer: "https://x.com/", "sec-fetch-site": "same-site", "sec-fetch-mode": "cors", "sec-fetch-dest": "empty", cookie: await this.getCookieString() }); log$4(`Making POST request to ${guestActivateUrl}`); const res = await this.fetch(guestActivateUrl, { method: "POST", headers }); await updateCookieJar(this.jar, res.headers); if (!res.ok) { throw new AuthenticationError(await res.text()); } const o = await flexParseJson(res); if (o == null || o["guest_token"] == null) { throw new AuthenticationError("guest_token not found."); } const newGuestToken = o["guest_token"]; if (typeof newGuestToken !== "string") { throw new AuthenticationError("guest_token was not a string."); } this.guestToken = newGuestToken; this.guestCreatedAt = /* @__PURE__ */ new Date(); await this.setCookie("gt", newGuestToken); log$4(`Updated guest token (length: ${newGuestToken.length})`); } /** * Returns if the authentication token needs to be updated or not. * @returns `true` if the token needs to be updated; `false` otherwise. */ shouldUpdate() { return !this.hasToken() || this.guestCreatedAt != null && this.guestCreatedAt < new Date((/* @__PURE__ */ new Date()).valueOf() - 3 * 60 * 60 * 1e3); } } class Platform { async randomizeCiphers() { const platform = await Platform.importPlatform(); await platform?.randomizeCiphers(); } static async importPlatform() { { const { platform } = await Promise.resolve().then(function () { return index; }); return platform; } } } const log$3 = debug("twitter-scraper:xctxid"); let linkedom = null; async function linkedomImport() { if (!linkedom) { const mod = await import('linkedom'); linkedom = mod; return mod; } return linkedom; } async function parseHTML(html) { if (typeof window !== "undefined") { const { defaultView } = new DOMParser().parseFromString(html, "text/html"); if (!defaultView) { throw new Error("Failed to get defaultView from parsed HTML."); } return defaultView; } else { const { DOMParser: DOMParser2 } = await linkedomImport(); return new DOMParser2().parseFromString(html, "text/html").defaultView; } } async function handleXMigration(fetchFn) { const headers = { 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", "accept-language": "ja", "cache-control": "no-cache", pragma: "no-cache", priority: "u=0, i", "sec-ch-ua": CHROME_SEC_CH_UA, "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "none", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", "user-agent": CHROME_USER_AGENT }; const response = await fetchFn("https://x.com", { headers }); if (!response.ok) { throw new Error(`Failed to fetch X homepage: ${response.statusText}`); } const htmlText = await response.text(); let dom = await parseHTML(htmlText); let document = dom.window.document; const migrationRedirectionRegex = new RegExp( "(http(?:s)?://(?:www\\.)?(twitter|x){1}\\.com(/x)?/migrate([/?])?tok=[a-zA-Z0-9%\\-_]+)+", "i" ); const metaRefresh = document.querySelector("meta[http-equiv='refresh']"); const metaContent = metaRefresh ? metaRefresh.getAttribute("content") || "" : ""; const migrationRedirectionUrl = migrationRedirectionRegex.exec(metaContent) || migrationRedirectionRegex.exec(htmlText); if (migrationRedirectionUrl) { const redirectResponse = await fetchFn(migrationRedirectionUrl[0]); if (!redirectResponse.ok) { throw new Error( `Failed to follow migration redirection: ${redirectResponse.statusText}` ); } const redirectHtml = await redirectResponse.text(); dom = await parseHTML(redirectHtml); document = dom.window.document; } const migrationForm = document.querySelector("form[name='f']") || document.querySelector("form[action='https://x.com/x/migrate']"); if (migrationForm) { const url = migrationForm.getAttribute("action") || "https://x.com/x/migrate"; const method = migrationForm.getAttribute("method") || "POST"; const requestPayload = new FormData(); const inputFields = migrationForm.querySelectorAll("input"); for (const element of Array.from(inputFields)) { const name = element.getAttribute("name"); const value = element.getAttribute("value"); if (name && value) { requestPayload.append(name, value); } } const formResponse = await fetchFn(url, { method, body: requestPayload, headers }); if (!formResponse.ok) { throw new Error( `Failed to submit migration form: ${formResponse.statusText}` ); } const formHtml = await formResponse.text(); dom = await parseHTML(formHtml); document = dom.window.document; } return document; } let cachedDocumentPromise = null; let cachedDocumentTimestamp = 0; const DOCUMENT_CACHE_TTL = 5 * 60 * 1e3; async function getCachedDocument(fetchFn) { const now = Date.now(); if (!cachedDocumentPromise || now - cachedDocumentTimestamp > DOCUMENT_CACHE_TTL) { log$3("Fetching fresh x.com document for transaction ID generation"); cachedDocumentTimestamp = now; cachedDocumentPromise = handleXMigration(fetchFn).catch((err) => { cachedDocumentPromise = null; throw err; }); } else { log$3("Using cached x.com document for transaction ID generation"); } return cachedDocumentPromise; } let ClientTransaction = null; async function clientTransaction() { if (!ClientTransaction) { const mod = await import('x-client-transaction-id'); ClientTransaction = mod.ClientTransaction; return mod.ClientTransaction; } return ClientTransaction; } async function generateTransactionId(url, fetchFn, method) { const parsedUrl = new URL(url); const path = parsedUrl.pathname; log$3(`Generating transaction ID for ${method} ${path}`); const document = await getCachedDocument(fetchFn); const ClientTransactionClass = await clientTransaction(); const transaction = await ClientTransactionClass.create(document); const transactionId = await transaction.generateTransactionId(method, path); log$3(`Transaction ID: ${transactionId}`); return transactionId; } 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"; const CHROME_SEC_CH_UA = '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"'; const log$2 = debug("twitter-scraper:api"); const bearerToken = "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF"; const bearerToken2 = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; async function jitter(maxMs) { const jitter2 = Math.random() * maxMs; await new Promise((resolve) => setTimeout(resolve, jitter2)); } async function requestApi(url, auth, method = "GET", platform = new Platform(), headers = new headersPolyfill.Headers(), bearerTokenOverride) { log$2(`Making ${method} request to ${url}`); await auth.installTo(headers, url, bearerTokenOverride); await platform.randomizeCiphers(); if (auth instanceof TwitterGuestAuth && auth.options?.experimental?.xClientTransactionId) { const transactionId = await generateTransactionId( url, auth.fetch.bind(auth), method ); headers.set("x-client-transaction-id", transactionId); } let res; do { const fetchParameters = [ url, { method, headers, credentials: "include" } ]; try { res = await auth.fetch(...fetchParameters); } catch (err) { if (!(err instanceof Error)) { throw err; } return { success: false, err }; } await updateCookieJar(auth.cookieJar(), res.headers); if (res.status === 429) { log$2("Rate limit hit, waiting for retry..."); await auth.onRateLimit({ fetchParameters, response: res }); } } while (res.status === 429); if (!res.ok) { return { success: false, err: await ApiError.fromResponse(res) }; } const value = await flexParseJson(res); if (res.headers.get("x-rate-limit-incoming") == "0") { auth.deleteToken(); return { success: true, value }; } else { return { success: true, value }; } } async function flexParseJson(res) { try { return await res.json(); } catch { log$2("Failed to parse response as JSON, trying text parse..."); const text = await res.text(); log$2("Response text:", text); return JSON.parse(text); } } function addApiFeatures(o) { return { ...o, rweb_lists_timeline_redesign_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, tweetypie_unmention_optimization_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, longform_notetweets_rich_text_read_enabled: true, responsive_web_enhance_cards_enabled: false, subscriptions_verification_info_enabled: true, subscriptions_verification_info_reason_enabled: true, subscriptions_verification_info_verified_since_enabled: true, super_follow_badge_privacy_enabled: false, super_follow_exclusive_tweet_notifications_enabled: false, super_follow_tweet_api_enabled: false, super_follow_user_api_enabled: false, android_graphql_skip_api_media_color_palette: false, creator_subscriptions_subscription_count_enabled: false, blue_business_profile_image_shape_enabled: false, unified_cards_ad_metadata_container_dynamic_card_content_query_enabled: false }; } function addApiParams(params, includeTweetReplies) { params.set("include_profile_interstitial_type", "1"); params.set("include_blocking", "1"); params.set("include_blocked_by", "1"); params.set("include_followed_by", "1"); params.set("include_want_retweets", "1"); params.set("include_mute_edge", "1"); params.set("include_can_dm", "1"); params.set("include_can_media_tag", "1"); params.set("include_ext_has_nft_avatar", "1"); params.set("include_ext_is_blue_verified", "1"); params.set("include_ext_verified_type", "1"); params.set("skip_status", "1"); params.set("cards_platform", "Web-12"); params.set("include_cards", "1"); params.set("include_ext_alt_text", "true"); params.set("include_ext_limited_action_results", "false"); params.set("include_quote_count", "true"); params.set("include_reply_count", "1"); params.set("tweet_mode", "extended"); params.set("include_ext_collab_control", "true"); params.set("include_ext_views", "true"); params.set("include_entities", "true"); params.set("include_user_entities", "true"); params.set("include_ext_media_color", "true"); params.set("include_ext_media_availability", "true"); params.set("include_ext_sensitive_media_warning", "true"); params.set("include_ext_trusted_friends_metadata", "true"); params.set("send_error_codes", "true"); params.set("simple_quoted_tweet", "true"); params.set("include_tweet_replies", `${includeTweetReplies}`); params.set( "ext", "mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,enrichments,superFollowMetadata,unmentionInfo,editControl,collab_control,vibe" ); return params; } const log$1 = debug("twitter-scraper:auth-user"); const TwitterUserAuthSubtask = typebox.Type.Object({ subtask_id: typebox.Type.String(), enter_text: typebox.Type.Optional(typebox.Type.Object({})) }); const _TwitterUserAuth = class _TwitterUserAuth extends TwitterGuestAuth { constructor(bearerToken, options) { super(bearerToken, options); this.subtaskHandlers = /* @__PURE__ */ new Map(); this.initializeDefaultHandlers(); } /** * Register a custom subtask handler or override an existing one * @param subtaskId The ID of the subtask to handle * @param handler The handler function that processes the subtask */ registerSubtaskHandler(subtaskId, handler) { this.subtaskHandlers.set(subtaskId, handler); } initializeDefaultHandlers() { this.subtaskH