(code: string, verifier: string)
| 37 | } |
| 38 | |
| 39 | async function exchangeCodeForToken(code: string, verifier: string): Promise<XOAuthTokenSet> { |
| 40 | const cfg = loadXApiConfig(); |
| 41 | if (!cfg.callbackUrl) { |
| 42 | throw new Error('Missing X_CALLBACK_URL in .env.local'); |
| 43 | } |
| 44 | |
| 45 | const basic = Buffer.from(`${cfg.clientId}:${cfg.clientSecret}`).toString('base64'); |
| 46 | const body = new URLSearchParams({ |
| 47 | grant_type: 'authorization_code', |
| 48 | code, |
| 49 | redirect_uri: cfg.callbackUrl, |
| 50 | code_verifier: verifier, |
| 51 | client_id: cfg.clientId, |
| 52 | }); |
| 53 | |
| 54 | const response = await fetch('https://api.x.com/2/oauth2/token', { |
| 55 | method: 'POST', |
| 56 | headers: { |
| 57 | Authorization: `Basic ${basic}`, |
| 58 | 'Content-Type': 'application/x-www-form-urlencoded', |
| 59 | }, |
| 60 | body, |
| 61 | }); |
| 62 | |
| 63 | const text = await response.text(); |
| 64 | const parsed = JSON.parse(text); |
| 65 | if (!response.ok) { |
| 66 | throw new Error(`Token exchange failed (HTTP ${response.status}). Check your X API credentials.`); |
| 67 | } |
| 68 | |
| 69 | return { |
| 70 | access_token: parsed.access_token, |
| 71 | refresh_token: parsed.refresh_token, |
| 72 | expires_in: parsed.expires_in, |
| 73 | scope: parsed.scope, |
| 74 | token_type: parsed.token_type, |
| 75 | obtained_at: new Date().toISOString(), |
| 76 | }; |
| 77 | } |
| 78 | |
| 79 | export async function saveTwitterOAuthToken(token: XOAuthTokenSet): Promise<string> { |
| 80 | ensureDataDir(); |
no test coverage detected