"use client"; import { useEffect, useRef, useState } from "react"; import Modal from "./Modal"; import Button from "./Button"; import Input from "./Input"; const TRAE_CLIENT_ID = "en1oxy7wnw8j9n"; function uuid(): string { const c = (globalThis.crypto || (globalThis as any).crypto) as Crypto | undefined; if (c?.randomUUID) return c.randomUUID(); return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => { const r = (Math.random() * 16) | 0; return (ch === "x" ? r : (r & 0x3) | 0x8).toString(16); }); } function randomHex(bytes: number): string { const buf = new Uint8Array(bytes); (globalThis.crypto || (globalThis as any).crypto).getRandomValues(buf); return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join(""); } function randomDigits(n: number): string { let s = ""; while (s.length < n) s += Math.floor(Math.random() * 1e10).toString(); return s.slice(0, n); } function buildTraeAuthorizeUrl(callbackUrl: string, traceId: string): string { // Per-attempt machine/device IDs — Trae doesn't tie tokens to a specific // machine_id, but the field is required and is echoed into provider metadata. const machineId = randomHex(32); const deviceId = randomDigits(19); const params = new URLSearchParams({ login_version: "1", auth_from: "solo", login_channel: "native_ide", plugin_version: "2.3.24254", auth_type: "local", client_id: TRAE_CLIENT_ID, redirect: "0", login_trace_id: traceId, auth_callback_url: callbackUrl, machine_id: machineId, device_id: deviceId, x_device_id: deviceId, x_machine_id: machineId, x_device_brand: "Mac14,7", x_device_type: "mac", x_os_version: "macOS 26.4.1", x_env: "", x_app_version: "0.1.7", x_app_type: "stable", // Match what the SOLO desktop client sends: hide_saas_login=true. With // auth_from=solo, trae.ai's authorize page is *only* a confirmation gate // for an already-cookied session — so leaving the SaaS sign-in surface // visible (false) causes the server to short-circuit to "Login Failed". // The user must be signed in on trae.ai in the same browser; we surface // that in the modal copy and provide an "Open solo.trae.ai" button. hide_saas_login: "true", }); return `https://www.trae.ai/authorization?${params.toString()}`; } type TraeAuthModalProps = { isOpen: boolean; onSuccess?: () => void; onClose: () => void; reauthConnection?: unknown; }; /** * Trae SOLO Auth Modal — paste the Cloud-IDE-JWT from solo.trae.ai. * * Trae has no public OAuth or local credential store like Cursor: the user * signs in to solo.trae.ai in a browser, copies the JWT sent in the * Authorization header (Cloud-IDE-JWT scheme), and pastes it here. JWT * lifetime is ~14 days; re-import on expiry. */ export default function TraeAuthModal({ isOpen, onSuccess, onClose, reauthConnection: _, }: TraeAuthModalProps) { const [accessToken, setAccessToken] = useState(""); const [webId, setWebId] = useState(""); const [bizUserId, setBizUserId] = useState(""); const [userUniqueId, setUserUniqueId] = useState(""); const [scope, setScope] = useState("marscode-us"); const [tenant, setTenant] = useState("marscode"); const [region, setRegion] = useState("US-East"); const [error, setError] = useState(null); const [importing, setImporting] = useState(false); const [authorizing, setAuthorizing] = useState(false); const popupRef = useRef(null); const traceIdRef = useRef(null); // Listen for postMessage from the callback page (window.opener.postMessage). // Only act on messages with our type tag, and verify the loginTraceId matches // the one we sent in the authorize URL — protects against unrelated postMessages. useEffect(() => { if (!isOpen) return; const onMessage = (ev: MessageEvent) => { const m = ev.data as { type?: string; success?: boolean; error?: string; loginTraceId?: string; } | null; if (!m || m.type !== "trae-oauth-callback") return; // Accept this window's origin OR the sibling loopback host: the dashboard // usually runs on localhost while Trae forces the callback onto 127.0.0.1, // so they are different origins by design. Restrict to that known pair // (never wildcard), then rely on the random loginTraceId for CSRF. const here = window.location; const altHost = here.hostname === "127.0.0.1" ? "localhost" : here.hostname === "localhost" ? "127.0.0.1" : null; const allowedOrigins = new Set([here.origin]); if (altHost) allowedOrigins.add(`${here.protocol}//${altHost}${here.port ? `:${here.port}` : ""}`); if (!allowedOrigins.has(ev.origin)) return; if (!traceIdRef.current || m.loginTraceId !== traceIdRef.current) return; setAuthorizing(false); if (m.success) { onSuccess?.(); onClose(); } else { setError(m.error || "Authorization failed"); } }; window.addEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage); }, [isOpen, onSuccess, onClose]); const handleAuthorizeWithBrowser = () => { setError(null); setAuthorizing(true); const traceId = uuid(); traceIdRef.current = traceId; // Trae's authorize endpoint validates two things about auth_callback_url: // 1. host must be a loopback IP (127.0.0.1) — "localhost" hostname gets // rejected with "Login Failed". // 2. path must end with `/authorize` — any other path (e.g. our earlier // "/api/oauth/trae/callback") also short-circuits to "Login Failed". // The receiving handler therefore lives at the app root (src/app/authorize). const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80"); const callbackUrl = `http://127.0.0.1:${port}/authorize`; const authUrl = buildTraeAuthorizeUrl(callbackUrl, traceId); const w = window.open(authUrl, "trae-oauth", "width=520,height=720"); if (!w) { setAuthorizing(false); setError("Popup blocked — allow popups for this site, or paste the token manually below."); return; } popupRef.current = w; // If the user closes the popup without completing, drop the spinner. const poll = setInterval(() => { if (w.closed) { clearInterval(poll); setAuthorizing((prev) => { if (prev) setError("Authorization window was closed before completing."); return false; }); } }, 700); }; const handleImportToken = async () => { if (!accessToken.trim()) { setError("Access token is required."); return; } setImporting(true); setError(null); try { const body: Record = { accessToken: accessToken.trim() }; if (webId.trim()) body.webId = webId.trim(); if (bizUserId.trim()) body.bizUserId = bizUserId.trim(); if (userUniqueId.trim()) body.userUniqueId = userUniqueId.trim(); if (scope.trim()) body.scope = scope.trim(); if (tenant.trim()) body.tenant = tenant.trim(); if (region.trim()) body.region = region.trim(); const res = await fetch("/api/oauth/trae/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) { throw new Error( typeof data.error === "string" ? data.error : data.error?.message || "Import failed" ); } onSuccess?.(); onClose(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setImporting(false); } }; return (
{/* Primary path: browser-based OAuth via trae.ai/authorization */}

Authorize via trae.ai in a popup. The popup will close itself once the token has been imported.

Important: you must be logged in to{" "} trae.ai in this browser first. The authorize page only confirms an existing session — it cannot sign you in. If you see "Login Failed", click "Open solo.trae.ai", sign in, then return here.

or paste a token manually

Sign in to solo.trae.ai, open DevTools → Network, send any chat message, and copy the JWT from the{" "} Authorization: Cloud-IDE-JWT <token> request header. JWT lifetime is ~14 days. Optional identity fields come from{" "} common_params in the same request body.