mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
fix(oauth): address Trae review feedback (loopback origin, stream cleanup, scripts)
- Restrict the /authorize postMessage and the modal listener to the loopback origin pair (localhost + 127.0.0.1) instead of "*"/single-origin. The dashboard runs on localhost while Trae forces the callback onto 127.0.0.1, so a single window.location.origin target silently dropped the success message (popup never closed). Addresses CWE-359 without breaking the cross-loopback flow. - TraeExecutor: cancel the SSE reader on completion, abort upfront when the caller signal is already aborted, and accept string elements in array message content. - Move ad-hoc dev scripts to scripts/ad-hoc/ per the repo style guide.
This commit is contained in:
@@ -32,7 +32,11 @@ function flattenQuery(messages: ChatMessage[]): string {
|
||||
if (typeof m.content === "string") content = m.content;
|
||||
else if (Array.isArray(m.content)) {
|
||||
content = m.content
|
||||
.map((p) => (p && typeof p === "object" ? String((p as JsonRecord).text ?? "") : ""))
|
||||
.map((p) => {
|
||||
if (typeof p === "string") return p;
|
||||
if (p && typeof p === "object") return String((p as JsonRecord).text ?? "");
|
||||
return "";
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
if (m.role === "system") parts.push(`[System]\n${content}`);
|
||||
@@ -165,6 +169,9 @@ export class TraeExecutor extends BaseExecutor {
|
||||
): Promise<void> {
|
||||
const url = `${this.base()}/chat_sessions/${sessionId}/events?reply_to_message_id=${encodeURIComponent(replyTo)}`;
|
||||
const ctrl = new AbortController();
|
||||
// If the caller's signal is already aborted, abort upfront so we don't open
|
||||
// a network request the consumer no longer wants.
|
||||
if (signal?.aborted) ctrl.abort();
|
||||
const timer = setTimeout(() => ctrl.abort(new Error("trae stream timeout")), STREAM_TIMEOUT_MS);
|
||||
const onAbort = () => ctrl.abort();
|
||||
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
||||
@@ -193,7 +200,10 @@ export class TraeExecutor extends BaseExecutor {
|
||||
} catch {
|
||||
data = { _raw: payload };
|
||||
}
|
||||
if (onEvent(ev, data)) return; // consumer signalled completion
|
||||
if (onEvent(ev, data)) {
|
||||
await reader.cancel().catch(() => {});
|
||||
return;
|
||||
}
|
||||
} else if (line === "") ev = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Authorization variants, so we can see which one (if any) the server accepts.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/diag-trae-auth.mjs <YOUR_CLOUD_IDE_JWT>
|
||||
// TRAE_TOKEN=eyJ... node scripts/diag-trae-auth.mjs
|
||||
// node scripts/ad-hoc/diag-trae-auth.mjs <YOUR_CLOUD_IDE_JWT>
|
||||
// TRAE_TOKEN=eyJ... node scripts/ad-hoc/diag-trae-auth.mjs
|
||||
//
|
||||
// Paste ONLY the token value (no "Cloud-IDE-JWT " prefix). The token is never
|
||||
// printed in full; only its length + last 6 chars are shown for sanity.
|
||||
@@ -31,7 +31,10 @@ const commonHeaders = {
|
||||
|
||||
// Each variant = a set of auth-bearing headers to try.
|
||||
const variants = [
|
||||
{ name: "Authorization: Cloud-IDE-JWT <t>", headers: { Authorization: `Cloud-IDE-JWT ${token}` } },
|
||||
{
|
||||
name: "Authorization: Cloud-IDE-JWT <t>",
|
||||
headers: { Authorization: `Cloud-IDE-JWT ${token}` },
|
||||
},
|
||||
{ name: "Authorization: Bearer <t>", headers: { Authorization: `Bearer ${token}` } },
|
||||
{ name: "Authorization: <t> (raw)", headers: { Authorization: token } },
|
||||
{ name: "Cloud-IDE-JWT: <t> (header)", headers: { "Cloud-IDE-JWT": token } },
|
||||
@@ -44,7 +47,10 @@ async function probe(label, url, method, headers) {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { ...commonHeaders, ...headers },
|
||||
body: method === "POST" ? JSON.stringify({ mode: "code", env: "remote", origin: "web" }) : undefined,
|
||||
body:
|
||||
method === "POST"
|
||||
? JSON.stringify({ mode: "code", env: "remote", origin: "web" })
|
||||
: undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
const snippet = text.replace(/\s+/g, " ").slice(0, 160);
|
||||
@@ -1,13 +1,13 @@
|
||||
// End-to-end smoke test: dergaет TraeExecutor против реального Trae с твоим
|
||||
// JWT из ../trae_solo.env, выводит content + usage.
|
||||
// Запуск: node --import tsx/esm scripts/smoke-trae.mjs
|
||||
// End-to-end smoke test: drives TraeExecutor against the real Trae API with your
|
||||
// JWT from trae_solo.env (kept outside the repo), printing content + usage.
|
||||
// Run: node --import tsx/esm scripts/ad-hoc/smoke-trae.mjs
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const envPath = path.resolve(__dirname, "../../trae_solo.env");
|
||||
const envPath = path.resolve(__dirname, "../../../trae_solo.env");
|
||||
if (!fs.existsSync(envPath)) {
|
||||
console.error(`Не найден ${envPath}. Положи туда TRAE_TOKEN= и TRAE_WEB_ID= и т.д.`);
|
||||
process.exit(1);
|
||||
@@ -23,7 +23,7 @@ const cfg = Object.fromEntries(
|
||||
})
|
||||
);
|
||||
|
||||
const { TraeExecutor } = await import("../open-sse/executors/trae.ts");
|
||||
const { TraeExecutor } = await import("../../open-sse/executors/trae.ts");
|
||||
const ex = new TraeExecutor();
|
||||
|
||||
const credentials = {
|
||||
@@ -30,8 +30,12 @@ import { parseTraeCallbackQuery } from "./parseCallback";
|
||||
* the echoed state before trusting the postMessage.
|
||||
*/
|
||||
function htmlClose(message: Record<string, unknown>): NextResponse {
|
||||
// Embedding values: only emit the small/sanitized status payload — never
|
||||
// the raw token. The opener trusts origin === self anyway.
|
||||
// Embedding values: only emit the small/sanitized status payload — never the
|
||||
// raw token. We post to the loopback origin pair (localhost + 127.0.0.1) on
|
||||
// this same port rather than "*": Trae forces the callback onto 127.0.0.1,
|
||||
// but the dashboard opener is usually on localhost, so a single
|
||||
// window.location.origin target would silently drop the message. Restricting
|
||||
// to the two known loopback hosts keeps it secure (CWE-359) and working.
|
||||
const safe = JSON.stringify({
|
||||
type: "trae-oauth-callback",
|
||||
...message,
|
||||
@@ -41,7 +45,17 @@ function htmlClose(message: Record<string, unknown>): NextResponse {
|
||||
<h2 style="margin:0 0 8px">Trae authorization ${message.success ? "✓" : "failed"}</h2>
|
||||
<p>${message.success ? "You can close this window." : "Return to the dashboard."}</p>
|
||||
<script>
|
||||
try { if (window.opener) window.opener.postMessage(${safe}, "*"); } catch (e) {}
|
||||
(function () {
|
||||
try {
|
||||
if (!window.opener) return;
|
||||
var msg = ${safe};
|
||||
var loc = window.location;
|
||||
var targets = [loc.origin];
|
||||
var alt = loc.hostname === "127.0.0.1" ? "localhost" : loc.hostname === "localhost" ? "127.0.0.1" : null;
|
||||
if (alt) targets.push(loc.protocol + "//" + alt + (loc.port ? ":" + loc.port : ""));
|
||||
targets.forEach(function (t) { try { window.opener.postMessage(msg, t); } catch (e) {} });
|
||||
} catch (e) {}
|
||||
})();
|
||||
setTimeout(function () { window.close(); }, ${message.success ? 800 : 4000});
|
||||
</script>
|
||||
</body></html>`,
|
||||
|
||||
@@ -111,7 +111,22 @@ export default function TraeAuthModal({
|
||||
loginTraceId?: string;
|
||||
} | null;
|
||||
if (!m || m.type !== "trae-oauth-callback") return;
|
||||
if (traceIdRef.current && m.loginTraceId && m.loginTraceId !== traceIdRef.current) 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?.();
|
||||
|
||||
Reference in New Issue
Block a user