const DEVICE_CODE_URL = 'https://github.com/login/device/code'; const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'; const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code'; const encodeForm = (params) => { const body = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value == null) continue; body.set(key, String(value)); } return body.toString(); }; async function postForm(url, params) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', }, body: encodeForm(params), }); const payload = await response.json().catch(() => null); if (!response.ok) { const message = payload?.error_description || payload?.error || response.statusText; const error = new Error(message || 'GitHub request failed'); error.status = response.status; error.payload = payload; throw error; } return payload; } export async function startDeviceFlow({ clientId, scope }) { return postForm(DEVICE_CODE_URL, { client_id: clientId, scope, }); } export async function exchangeDeviceCode({ clientId, deviceCode }) { // GitHub returns 200 with {error: 'authorization_pending'|...} for non-success states. const payload = await postForm(ACCESS_TOKEN_URL, { client_id: clientId, device_code: deviceCode, grant_type: DEVICE_GRANT_TYPE, }); return payload; }