// device-login.mjs — Phone-side device auth page // User scans QR code → lands here → logs in via Claude OAuth → token sent to device // // URL: /api/device-login?code=WOLF-3847 // Redirects to Claude OAuth with state=code, callback returns here with tokens. import crypto from "crypto"; const CLAUDE_OAUTH_URL = "https://claude.ai/oauth/authorize"; const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"; export async function handler(event) { const params = new URLSearchParams(event.rawQuery || ""); const code = params.get("code"); const oauthCode = params.get("oauth_code"); const error = params.get("error"); const baseUrl = process.env.URL || "https://aesthetic.computer"; const callbackUrl = `${baseUrl}/api/device-login`; // Step 3: OAuth callback — exchange code and approve device if (oauthCode && code) { return await handleOAuthCallback(oauthCode, code, callbackUrl, baseUrl); } // Error from OAuth if (error) { return servePage("Auth Failed", `

Error: ${error}

Close this page and try again on the device.

`); } // Step 1: Show device code confirmation page with "Login with Claude" button if (!code) { return servePage("Missing Code", `

No device code provided.

Scan the QR code on your device to start.

`); } // Verify code exists and is pending try { const checkRes = await fetch(`${baseUrl}/api/device-auth?action=poll&code=${code}`); const checkData = await checkRes.json(); if (checkData.error) { return servePage("Invalid Code", `

Code ${code} not found or expired.

Try again on the device.

`); } if (checkData.status === "approved") { return servePage("Already Done", `

This device is already logged in!

`); } } catch (e) { // Continue anyway — let the user try } // Generate PKCE challenge for Claude OAuth const { codeVerifier, codeChallenge } = generatePKCE(); // Store verifier in a short-lived cookie (needed for callback) const state = `${code}:${codeVerifier}`; const stateB64 = Buffer.from(state).toString("base64url"); const oauthUrl = `${CLAUDE_OAUTH_URL}?` + new URLSearchParams({ client_id: CLAUDE_CLIENT_ID, response_type: "code", redirect_uri: `${callbackUrl}`, scope: SCOPES, code_challenge: codeChallenge, code_challenge_method: "S256", state: stateB64, }).toString(); return servePage("Device Login", `

Logging in device:

${code}

This will connect your Claude account to your AC Native device.

Login with Claude

Your device is waiting...

`); } async function handleOAuthCallback(oauthCode, stateB64, callbackUrl, baseUrl) { // Decode state = "DEVICE_CODE:CODE_VERIFIER" let deviceCode, codeVerifier; try { // state comes back as a query param — might be the raw base64url from our redirect const raw = Buffer.from(stateB64, "base64url").toString(); const colonIdx = raw.indexOf(":"); deviceCode = raw.substring(0, colonIdx); codeVerifier = raw.substring(colonIdx + 1); } catch { return servePage("Error", `

Invalid state parameter.

`); } // Exchange OAuth code for tokens using PKCE try { const tokenRes = await fetch("https://claude.ai/oauth/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "authorization_code", client_id: CLAUDE_CLIENT_ID, code: oauthCode, redirect_uri: callbackUrl, code_verifier: codeVerifier, }), }); if (!tokenRes.ok) { const errText = await tokenRes.text(); console.error("Token exchange failed:", errText); return servePage("Token Error", `

Failed to exchange authorization code.

Try again on the device.

`); } const tokens = await tokenRes.json(); // Build credentials in Claude Code format const credentials = { claudeAiOauth: { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + (tokens.expires_in || 3600) * 1000, scopes: SCOPES.split(" "), }, }; // Approve the device code const approveRes = await fetch(`${baseUrl}/api/device-auth`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "approve", code: deviceCode, credentials, }), }); if (!approveRes.ok) { const err = await approveRes.text(); console.error("Approve failed:", err); return servePage("Error", `

Failed to send token to device.

`); } return servePage("Success!", `

Your device is now logged in!

${deviceCode}

You can close this page. The device will pick up the login automatically.

`); } catch (err) { console.error("OAuth callback error:", err); return servePage("Error", `

Authentication failed: ${err.message}

`); } } // PKCE helper (Node.js crypto) function generatePKCE() { const codeVerifier = crypto.randomBytes(32).toString("base64url"); const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url"); return { codeVerifier, codeChallenge }; } function servePage(title, bodyContent) { const html = ` ${title} — AC Native

${title}

${bodyContent}
`; return { statusCode: 200, headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }, body: html, }; }