Summary
The x connector's OAuth authorize request is rejected by X (oauth_denied / invalid request) before the consent screen renders. The app in the X Developer Portal is configured correctly (project-attached, confidential client, Read and write, exact callback registered).
The cause is the state parameter. signOAuthState (src/connectors/oauth.ts) packs the whole flow context — provider, principalId, redirectUri, orgId, clientRef, returnTo, PKCE codeVerifier, issuedAt, nonce — into an HS256 JWT sent as state. For an ordinary deployment (19-char principal email, default <prefix>-portal.fly.dev public URL) the JWT comes out at 519 characters. X's OAuth 2.0 authorize endpoint rejects any state longer than 500 characters; other providers (Google, GitHub, Dropbox, Notion, Linear) tolerate long state, so only the x connector breaks.
Because the overage is small, whether a given deployment hits it depends on the length of the signing email, org id, and public URL — which is presumably why it slipped through.
Repro
- Deploy with any org whose principal email + publicUrl push the state JWT past 500 chars (a 19-char email and a 20-char hostname suffice).
- Configure the
x connector per the setup guide and connect from the keychain page.
- X responds with invalid request at
x.com/i/oauth2/authorize; the consent screen never renders.
Expected
The authorize request stays within every provider's documented limits regardless of principal email / URL length.
Possible fixes
- Keep the flow context server-side (Postgres, consistent with the durable-by-default rule) keyed by a short random
state, or
- slim the JWT: the 43-char PKCE
codeVerifier doesn't need to ride in the URL at all, and redirectUri is derivable from config + provider. Dropping those two alone brings the state comfortably under 500.
Happy to send a PR if there's a preferred direction.
Summary
The
xconnector's OAuth authorize request is rejected by X (oauth_denied/ invalid request) before the consent screen renders. The app in the X Developer Portal is configured correctly (project-attached, confidential client, Read and write, exact callback registered).The cause is the
stateparameter.signOAuthState(src/connectors/oauth.ts) packs the whole flow context — provider, principalId, redirectUri, orgId, clientRef, returnTo, PKCE codeVerifier, issuedAt, nonce — into an HS256 JWT sent asstate. For an ordinary deployment (19-char principal email, default<prefix>-portal.fly.devpublic URL) the JWT comes out at 519 characters. X's OAuth 2.0 authorize endpoint rejects anystatelonger than 500 characters; other providers (Google, GitHub, Dropbox, Notion, Linear) tolerate long state, so only thexconnector breaks.Because the overage is small, whether a given deployment hits it depends on the length of the signing email, org id, and public URL — which is presumably why it slipped through.
Repro
xconnector per the setup guide and connect from the keychain page.x.com/i/oauth2/authorize; the consent screen never renders.Expected
The authorize request stays within every provider's documented limits regardless of principal email / URL length.
Possible fixes
state, orcodeVerifierdoesn't need to ride in the URL at all, andredirectUriis derivable from config + provider. Dropping those two alone brings the state comfortably under 500.Happy to send a PR if there's a preferred direction.