You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(transport): chromedp auto-bootstraps ct0/twid + per-op features
Live e2e against real X session surfaced multiple stacked issues. This
commit lands the auth+transport layer that finally makes the read path
work end to end (profile, tweets list, following, followers, search
were tested live; see WORKS / KNOWN-FAILS at the bottom).
## Browser auth bootstrap (XActions parity)
User pasted only auth_token. Previously x-cli refused to import without
ct0+twid. The chromebrowser transport now mints them itself:
internal/chromebrowser/browser.go
Browser.Cookies(ctx, domain) — reads the in-memory Chrome cookie
jar via CDP Network.GetCookies and returns a flat name→value map.
Fetch JS — reads ct0 from document.cookie at request time and
injects it as x-csrf-token. The browser populates ct0 itself
via Set-Cookie on the initial navigate, so the user never has
to paste a CSRF token. This matches XActions' Puppeteer flow.
Navigation target switched from /robots.txt to /i/release_notes
so the navigate triggers the session bootstrap that actually
issues twid+gt+personalization_id alongside ct0. /robots.txt was
too lightweight to populate twid.
internal/chromebrowser/transport.go
RoundTrip reads the browser cookie jar AFTER each Fetch and
surfaces all cookies as Set-Cookie headers on the synthetic
http.Response. The api.Client's existing mergeSetCookies path
folds them into the in-memory Session — same code path as a
real ct0 rotation. Net effect: VerifyCredentials runs a probe
call, the response carries Set-Cookies for ct0+twid+gt+...,
the merge populates session.Cookies, the probe-then-retry path
in VerifyCredentials re-reads twid and resolves the user.
X_CLI_BROWSER_DEBUG=1 prints the cookie jar names after each
fetch (no values, just names) for diagnosing rotation issues.
api/auth.go
RequireAuthCookies (compat) → calls RequireAuthCookiesFor(true).
RequireAuthCookiesFor(cookies, needCt0) — explicit form. The
browser path passes needCt0=false because chromebrowser will
fetch one itself. The http path still passes needCt0=true.
VerifyCredentials restructured into two functions:
verifyByTwid — the happy path when twid is present
VerifyCredentials — orchestrates "no twid → probe → retry"
errMissingTwid sentinel + isMissingTwid for the retry dispatch.
cmd/auth.go
acquireCookieString / runAuthImport now treat ct0 as optional in
the browser path. Tighter timeout on the verify call (60s, accounts
for first-call Chrome startup ~1-2s).
cmd/doctor.go
verifyLiveSession honors the global useHTTP flag — was hardcoding
the http+utls path which hits Cloudflare from inside.
## Per-operation features blob (the stale-features bug)
Different X GraphQL ops require different features blobs. Sending the
union-of-all-keys causes the gateway to 404 unknown features for
specific ops. Each op now carries its own override.
api/endpoints.go
GraphQLEndpoint adds Features map[string]bool. When non-nil it
REPLACES the global features for that op. Documentation comment
explains the rotation-recovery workflow.
api/client.go GraphQL
Looks up ep.Features first, falls back to c.endpoints.Features.
endpoints.yaml
Global features now matches the timeline operations' set
(Followers/Following/SearchTimeline/UserTweets all use the same
~36-key blob). Profile-specific keys (hidden_profile_subscriptions,
subscriptions_verification_info_*, highlights_tweets_tab_ui_enabled,
responsive_web_twitter_article_notes_tab_enabled,
subscriptions_feature_can_gift_premium) moved to
UserByScreenName.features as an override.
Followers query ID refreshed: `-WcGoRt8IQuPm-l1ymgy6g`
(captured live; gC_lyAxZOptAMLCJX5UhWw is dead).
## Variable shape fixes
api/relationships.go Followers, Following
+ withGrokTranslatedBio: true (required by current schema)
api/search.go SearchPosts, SearchUsers
+ withGrokTranslatedBio: false
api/tweets.go UserTweets
includePromotedContent: false → true (matches live web client;
schema rejection on false)
## Diagnostics
api/client.go logRequest
X_CLI_FULL_URL=1 disables the `?…` truncation so the full GraphQL
URL with variables and features is logged. Used during this
session to compare our URLs against captured live URLs.
api/client.go GraphQL/REST
401/403/404 paths read up to 400 bytes of the response body and
embed it in the error message via previewBody. Surfaced the
Cloudflare HTML challenge in the v0.4→v0.5 transition.
## Live e2e results (Eric Wang's session)
WORKS:
✓ x auth import --cookie 'auth_token=…' (no ct0 needed)
✓ x doctor (verify, ASN, TLS)
✓ x profile get jack (real data, all fields)
✓ x tweets list ericwang42 -n 10 (with new render: ↳, →q,
📷N, RT @author full body)
✓ x following jack -n 5 (Stella_Assange, Snowden,
elonmusk — real data)
KNOWN FAILS (next commit):
✗ x followers <user> → 404 empty body
✗ x search posts <query> → 404 empty body
Root cause identified via CDP header capture: the live web client
sends `x-client-transaction-id` on every request. UserTweets and
Following accept its absence; Followers and SearchTimeline 404 when
it's missing. The transaction ID is computed by an obfuscated
function in X's main JS bundle. Two fixes are possible:
(a) Reverse-engineer the algorithm and reproduce it in Go. The
function uses HMAC over URL path + per-session animation hash
+ timestamp. Open-source ports exist (twikit, twitter-api-go).
(b) Hijack the SPA's fetch wrapper from inside the page. Navigate
to /jack/followers, let the SPA bootstrap, replace
window.fetch with a recording wrapper, capture the next
Followers call's headers including the JS-computed transaction
ID, then run our own fetch with the captured headers.
(b) is more reliable across rotations; (a) is more performant. Next
commit picks one. Probably (b) for v0.7.
0 commit comments