fix: prevent non-SSE stream pipe crash and cross-IdP account overwrites (#2244)

- streamingHandler: when upstream returns non-SSE/JSON (e.g. Cloudflare
  5xx HTML), read body, sanitize <title>, notify streamController and
  return a clean JSON error instead of crashing the pipe.
- connectionsRepo: dedup OAuth connections on (email + username) so
  cross-IdP accounts sharing an email no longer overwrite each other;
  workspace providers keep workspace-id matching.
- kimchi: bump User-Agent to 0.1.50, add svg asset + browser-login
  service, and 21 unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
KunN-21
2026-07-03 11:07:08 +07:00
committed by decolua
parent abc0add031
commit cb0135b695
7 changed files with 410 additions and 8 deletions

View File

@@ -98,13 +98,26 @@ export async function createProviderConnection(data) {
let existing = null;
if (data.authType === "oauth" && data.email) {
const incomingUsername = data.providerSpecificData?.username;
const incomingWs = data.providerSpecificData?.chatgptAccountId;
existing = all.find(c => {
if (c.authType !== "oauth" || c.email !== data.email) return false;
// If both sides have a workspace ID, they must match for dedup
// Workspace providers (Codex) use workspace ID when both sides have it
const existingWs = c.providerSpecificData?.chatgptAccountId;
if (incomingWs && existingWs) return incomingWs === existingWs;
return true; // fallback: email-only match for non-workspace providers
if (incomingWs && !existingWs) return false;
if (!incomingWs && existingWs) return false;
// Non-workspace providers: match on (email + username) so cross-IdP
// accounts don't overwrite each other. Require username on both sides
// — if only one side has it, treat as a distinct identity rather than
// collapsing onto the bare-email fallback (which would re-introduce
// the cross-IdP overwrite).
const existingUsername = c.providerSpecificData?.username;
if (incomingUsername && existingUsername) {
return incomingUsername === existingUsername;
}
if (incomingUsername || existingUsername) return false;
return true;
});
} else if (data.authType === "apikey" && data.name) {
existing = all.find(c => c.authType === "apikey" && c.name === data.name);

View File

@@ -0,0 +1,133 @@
// Kimchi browser-login service.
//
// Ports Kimchi CLI's authenticateViaBrowser (src/cli-auth/index.ts) onto
// 9Router's shared startLocalServer util (same one xai/antigravity use).
// Simpler than those: the token arrives directly on the callback query
// string — no authorization-code exchange, no PKCE.
//
// In-flight logins are held in `sessions` keyed by state. The OAuthModal
// device_code flow starts one via requestDeviceCode(); pollToken() peeks
// at the resolved token; the generic [provider]/[action] route calls
// createProviderConnection with the real token.
import { randomBytes } from "node:crypto";
import { startLocalServer } from "../utils/server.js";
import { KIMCHI_CONFIG } from "../constants/oauth.js";
const sessions = new Map(); // state -> { result, close, timeout, done, resolved }
const SESSION_TTL_MS = 5 * 60 * 1000;
export function buildKimchiAuthUrl(callbackUrl, state) {
const params = new URLSearchParams({ callback: callbackUrl, state });
return `${KIMCHI_CONFIG.webAppUrl}/cli-auth?${params.toString()}`;
}
export function generateState() {
return randomBytes(32).toString("hex");
}
// Returns the resolved { token } if the session for `state` has completed,
// or null if it is still pending / unknown.
export function getResolvedSession(state) {
const s = sessions.get(state);
if (!s || !s.done || !s.resolved) return null;
return s.resolved;
}
export class KimchiService {
async startLogin() {
const state = generateState();
let resolveResult;
const result = new Promise((resolve) => { resolveResult = resolve; });
const { port, close } = await startLocalServer((params) => {
this._handleCallback(params, state)
.then(resolveResult)
.catch((err) => resolveResult({ error: err.message }));
});
const timeout = setTimeout(() => {
resolveResult({ error: "Browser login timed out — please try again" });
close();
}, KIMCHI_CONFIG.callbackTimeoutMs);
sessions.set(state, { result, close, timeout, done: false, resolved: null });
// Stash the resolved value so pollToken() can retrieve the real token,
// close the loopback server, and reap the session after a TTL so the
// Map can't grow unbounded across many logins.
result.then((r) => {
const s = sessions.get(state);
if (!s) return;
s.done = true;
s.resolved = r;
clearTimeout(s.timeout);
try { s.close(); } catch { /* already closed */ }
setTimeout(() => sessions.delete(state), SESSION_TTL_MS).unref?.();
});
const callbackUrl = `http://127.0.0.1:${port}${KIMCHI_CONFIG.callbackPath}`;
const authUrl = buildKimchiAuthUrl(callbackUrl, state);
return { authUrl, port, state, result, close };
}
async _handleCallback(params, expectedState) {
if (params.error) {
throw new Error(params.error_description || params.error);
}
const candidate = params.state;
if (!candidate || candidate !== expectedState) {
throw new Error("This request isn't valid. Please restart the Kimchi login flow.");
}
const token = params.token;
if (!token) {
throw new Error("No token was returned by the Kimchi authentication server");
}
const check = await this.validateToken(token);
if (!check.valid) {
throw new Error(check.error || "Kimchi token validation failed");
}
return { token };
}
async fetchProfile(token) {
try {
const res = await fetch(KIMCHI_CONFIG.meUrl, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return {};
const j = await res.json();
return { displayName: j.name, email: j.email, username: j.username };
} catch {
return {};
}
}
// Validate a token against Kimchi's supported-providers endpoint.
// 200 → valid; 401/403 → invalid; anything else (incl. network/timeout)
// → fail-open valid so a flaky validation never blocks a good login.
async validateToken(token) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
let status = 0;
try {
const res = await fetch(KIMCHI_CONFIG.validationUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
signal: controller.signal,
});
status = res.status;
} catch {
// Network error / abort → fail-open
return { valid: true };
} finally {
clearTimeout(timer);
}
if (status === 200) return { valid: true };
if (status === 401) return { valid: false, error: "Kimchi token invalid or expired" };
if (status === 403) return { valid: false, error: "Kimchi token lacks required scope" };
return { valid: true };
}
}