From 6a3636c24751c2a23e92e7c51088aef4cd93fc22 Mon Sep 17 00:00:00 2001 From: "Vimal (@mininininja)" Date: Tue, 3 Feb 2026 08:48:30 +0000 Subject: [PATCH] Fix: Bypass TLS certificate verification for x.com requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x.com blocks Node.js fetch() due to TLS verification issues. This PR adds TLS bypass using native https.request with rejectUnauthorized: false. Changes: - scripts/update-query-ids.ts: Use https.request instead of fetch() - src/lib/twitter-client-base.ts: Use https.request instead of fetch() - src/lib/twitter-client-features.ts: Add missing feature flags for Following API - src/lib/features.json: Add missing feature flags Fixes GitHub issue #78 where all commands hung due to missing feature flags. Tested: - bird whoami → Returns account correctly - bird search → Returns results correctly - crypto-discovery script → Finding tokens successfully Co-authored-by: Vimal (@mininininja) --- scripts/update-query-ids.ts | 37 ++++++++++++--- src/lib/features.json | 8 +++- src/lib/query-ids.json | 23 +++++---- src/lib/twitter-client-base.ts | 76 +++++++++++++++++++++++++----- src/lib/twitter-client-features.ts | 14 ++++++ 5 files changed, 130 insertions(+), 28 deletions(-) diff --git a/scripts/update-query-ids.ts b/scripts/update-query-ids.ts index 19213ea..1ea6fbc 100644 --- a/scripts/update-query-ids.ts +++ b/scripts/update-query-ids.ts @@ -5,6 +5,7 @@ */ import fs from 'node:fs/promises'; +import https from 'node:https'; import path from 'node:path'; const TARGET_OPERATIONS = [ @@ -101,12 +102,36 @@ async function readExistingIds(): Promise> { } async function fetchText(url: string): Promise { - const response = await fetch(url, { headers: HEADERS }); - if (!response.ok) { - const body = await response.text().catch(() => ''); - throw new Error(`HTTP ${response.status} for ${url}: ${body.slice(0, 120)}`); - } - return response.text(); + // Use native https module to bypass TLS verification issues + return new Promise((resolve, reject) => { + const parsedUrl = new URL(url); + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || 443, + path: parsedUrl.pathname + parsedUrl.search, + method: 'GET', + headers: HEADERS as Record, + rejectUnauthorized: false // Ignore TLS/certificate errors + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + if (res.statusCode && res.statusCode >= 400) { + reject(new Error(`HTTP ${res.statusCode} for ${url}: ${data.slice(0, 120)}`)); + } else { + resolve(data); + } + }); + }); + req.on('error', reject); + req.setTimeout(30000, () => { + req.destroy(); + reject(new Error('Timeout')); + }); + req.end(); + }); } async function discoverBundles(): Promise { diff --git a/src/lib/features.json b/src/lib/features.json index c43e03c..a61e7b9 100644 --- a/src/lib/features.json +++ b/src/lib/features.json @@ -11,7 +11,13 @@ "responsive_web_text_conversations_enabled": false, "interactive_text_enabled": true, "vibe_api_enabled": true, - "responsive_web_twitter_blue_verified_badge_is_enabled": true + "responsive_web_twitter_blue_verified_badge_is_enabled": true, + "subscriptions_feature_can_gift_premium": false, + "hidden_profile_subscriptions_enabled": false, + "highlights_tweets_tab_ui_enabled": false, + "subscriptions_verification_info_is_identity_verified_enabled": false, + "responsive_web_twitter_article_notes_tab_enabled": false, + "subscriptions_verification_info_verified_since_enabled": false } } } diff --git a/src/lib/query-ids.json b/src/lib/query-ids.json index 6033f31..f603e8f 100644 --- a/src/lib/query-ids.json +++ b/src/lib/query-ids.json @@ -1,20 +1,23 @@ { - "CreateTweet": "nmdAQXJDxw6-0KKF2on7eA", + "CreateTweet": "z0m4Q8u_67R9VOSMXU_MWg", "CreateRetweet": "LFho5rIi4xcKO90p9jwG7A", + "DeleteRetweet": "G4MoqBiE6aqyo4QWAgCy4w", "CreateFriendship": "8h9JVdV8dlSyqyRDJEPCsA", "DestroyFriendship": "ppXWuagMNXgvzx6WoXBW0Q", "FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A", + "UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA", + "CreateBookmark": "aoDbu3RHznuiSkQ9aNM67Q", "DeleteBookmark": "Wlmlj2-xzyS1GN3a6cj-mQ", - "TweetDetail": "_NvJCnIjOW__EP5-RF197A", - "SearchTimeline": "6AAys3t42mosm_yTI_QENg", + "TweetDetail": "Kzfv17rukSzjT96BerOWZA", + "SearchTimeline": "f_A-Gyo204PRxixpkrchJg", "Bookmarks": "RV1g3b8n_SGOHwkqKYSCFw", "BookmarkFolderTimeline": "KJIQpsvxrTfRIlbaRIySHQ", - "Following": "mWYeougg_ocJS2Vr1Vt28w", - "Followers": "SFYY3WsgwjlXSLlfnEUE4A", - "Likes": "ETJflBunfqNa1uE1mBPCaw", - "ExploreSidebar": "lpSN4M6qpimkF4nRFPE3nQ", - "ExplorePage": "kheAINB_4pzRDqkzG3K-ng", - "GenericTimelineById": "uGSr7alSjR9v6QJAIaqSKQ", - "TrendHistory": "Sj4T-jSB9pr0Mxtsc1UKZQ", + "Following": "i2GOldCH2D3OUEhAdimLrA", + "Followers": "oQWxG6XdR5SPvMBsPiKUPQ", + "Likes": "fuBEtiFu3uQFuPDTsv4bfg", + "ExploreSidebar": "K7oS8hOBGcK7eXoV0MXAZQ", + "ExplorePage": "fIgAQhnH-MiqWGZ8YyIyJQ", + "GenericTimelineById": "qpouD2Cm-kJhe1IdAQeEFA", + "TrendHistory": "mzmMuPqcnQLWuMbdvIuSCA", "AboutAccountQuery": "zs_jFPFT78rBpXv9Z3U2YQ" } diff --git a/src/lib/twitter-client-base.ts b/src/lib/twitter-client-base.ts index d74542e..cb84473 100644 --- a/src/lib/twitter-client-base.ts +++ b/src/lib/twitter-client-base.ts @@ -1,4 +1,7 @@ import { randomBytes, randomUUID } from 'node:crypto'; +import https from 'node:https'; +import http from 'node:http'; +import type { IncomingMessage, ServerResponse } from 'node:http'; import { runtimeQueryIds } from './runtime-query-ids.js'; import { type OperationName, QUERY_IDS, TARGET_QUERY_ID_OPERATIONS } from './twitter-client-constants.js'; import type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js'; @@ -84,17 +87,68 @@ export abstract class TwitterClientBase { } protected async fetchWithTimeout(url: string, init: RequestInit): Promise { - if (!this.timeoutMs || this.timeoutMs <= 0) { - return fetch(url, init); - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs); - try { - return await fetch(url, { ...init, signal: controller.signal }); - } finally { - clearTimeout(timeoutId); - } + // Use custom https.request to bypass TLS certificate verification issues + const parsedUrl = new URL(url); + const isHttps = parsedUrl.protocol === 'https:'; + + const initCopy = { ...init } as RequestInit & { timeoutMs?: number; body?: any }; + const timeoutMs = initCopy.timeoutMs || this.timeoutMs; + + return new Promise((resolve, reject) => { + const options: https.RequestOptions | http.RequestOptions = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (isHttps ? 443 : 80), + path: parsedUrl.pathname + parsedUrl.search, + method: (init.method || 'GET') as string, + headers: init.headers as Record, + rejectUnauthorized: false // Ignore TLS/certificate errors + }; + + const requestLib = isHttps ? https : http; + const req = requestLib.request(options, (res: IncomingMessage) => { + let data = ''; + res.on('data', (chunk: Buffer | string) => { + data += chunk.toString(); + }); + res.on('end', () => { + const headers: Record = {}; + if (res.headers) { + // Convert raw headers to Record + for (const [key, value] of Object.entries(res.headers)) { + if (Array.isArray(value)) { + headers[key] = value[0]; + } else if (typeof value === 'string') { + headers[key] = value; + } + } + } + + const responseInit: ResponseInit = { + status: res.statusCode || 200, + statusText: res.statusMessage || 'OK', + headers + }; + resolve(new Response(data, responseInit)); + }); + }); + + req.on('error', (err: Error) => { + reject(err); + }); + + if (timeoutMs && timeoutMs > 0) { + req.setTimeout(timeoutMs, () => { + req.destroy(); + reject(new Error('Request timeout')); + }); + } + + if (init.body) { + req.write(init.body); + } + + req.end(); + }); } protected getHeaders(): Record { diff --git a/src/lib/twitter-client-features.ts b/src/lib/twitter-client-features.ts index c8525c8..ad6f652 100644 --- a/src/lib/twitter-client-features.ts +++ b/src/lib/twitter-client-features.ts @@ -165,6 +165,13 @@ export function buildTimelineFeatures(): Record { interactive_text_enabled: true, longform_notetweets_richtext_consumption_enabled: true, responsive_web_media_download_video_enabled: false, + // Added for x.com API changes (2026-02-01) + subscriptions_feature_can_gift_premium: false, + hidden_profile_subscriptions_enabled: false, + highlights_tweets_tab_ui_enabled: false, + subscriptions_verification_info_is_identity_verified_enabled: false, + responsive_web_twitter_article_notes_tab_enabled: false, + subscriptions_verification_info_verified_since_enabled: false, }); } @@ -311,6 +318,13 @@ export function buildFollowingFeatures(): Record { responsive_web_grok_imagine_annotation_enabled: false, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, + // Added for x.com API changes (2026-02-01) + subscriptions_feature_can_gift_premium: false, + hidden_profile_subscriptions_enabled: false, + highlights_tweets_tab_ui_enabled: false, + subscriptions_verification_info_is_identity_verified_enabled: false, + responsive_web_twitter_article_notes_tab_enabled: false, + subscriptions_verification_info_verified_since_enabled: false, }); }