mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(security): sanitize error messages, fix ReDoS patterns, harden OAuth callback
Error message sanitization (Hard Rule #12): - claude-auth/export, codex-auth/export, gemini-cli-auth/export routes: replace raw err.message with sanitizeErrorMessage() from open-sse/utils/error.ts - imageGeneration, musicGeneration, videoGeneration handlers: import sanitizeErrorMessage and replace all err.message in return values - veoaifree-web executor: replace raw upstream response data in errResp() calls with static strings OAuth callback page (callback/page.tsx): - Remove useSearchParams/Suspense dependency that caused hydration failures in popup windows navigating back from Google OAuth (COOP header severs opener) - Use window.location.search directly in useEffect with three send methods: postMessage, BroadcastChannel, localStorage - Fix postMessage target from "*" to window.location.origin (semgrep finding) - Move setCurrentUrl call to manual-only branch to avoid unnecessary renders copilot-web executor: - Move accessToken from WebSocket URL query string to Authorization header (avoids credential exposure in server logs) - Add MAX_POOL_SIZE=100 cap to sessionPool with LRU eviction of oldest entry CodeQL ReDoS fixes (js/polynomial-redos #233-240): - Replace while(s.endsWith("/")) s=s.slice(0,-1) pattern (O(n²) allocations) with index-based loop (O(n) time, single final slice) in: bin/cli/api.mjs, all 6 cli-helper config generators, opencode-provider Gemini OAuth: - mapTokens: add idToken field to fix "missing id_token" export error
This commit is contained in:
@@ -193,7 +193,9 @@ export function normalizeBaseURL(rawBaseURL: string): string {
|
||||
);
|
||||
}
|
||||
let base = trimmed;
|
||||
while (base.endsWith("/")) base = base.slice(0, -1);
|
||||
let end = base.length;
|
||||
while (end > 0 && base[end - 1] === "/") end--;
|
||||
base = end < base.length ? base.slice(0, end) : base;
|
||||
if (base.endsWith("/v1")) base = base.slice(0, -3);
|
||||
return base + "/v1";
|
||||
}
|
||||
|
||||
@@ -45,9 +45,10 @@ export function getBaseUrl(opts = {}) {
|
||||
}
|
||||
|
||||
function stripTrailingSlash(value) {
|
||||
let s = String(value);
|
||||
while (s.endsWith("/")) s = s.slice(0, -1);
|
||||
return s;
|
||||
const s = String(value);
|
||||
let end = s.length;
|
||||
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
|
||||
return end === s.length ? s : s.slice(0, end);
|
||||
}
|
||||
|
||||
function resolveUrl(path, opts) {
|
||||
|
||||
@@ -117,6 +117,7 @@ const sessionPool = new Map<string, CopilotSession>();
|
||||
let sessionRotationCount = 0;
|
||||
const MIN_REMAINING_TURNS = 5;
|
||||
const MAX_ROTATIONS = 1000;
|
||||
const MAX_POOL_SIZE = 100;
|
||||
|
||||
// ─── Executor ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -150,6 +151,10 @@ export class CopilotWebExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
const session = await this.createSession(accessToken, signal);
|
||||
// Evict oldest entry if pool is at capacity (Map preserves insertion order)
|
||||
if (sessionPool.size >= MAX_POOL_SIZE) {
|
||||
sessionPool.delete(sessionPool.keys().next().value!);
|
||||
}
|
||||
sessionPool.set(poolKey, session);
|
||||
sessionRotationCount++;
|
||||
return session;
|
||||
@@ -214,11 +219,8 @@ export class CopilotWebExecutor extends BaseExecutor {
|
||||
accessToken?: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<ReadableStream<Uint8Array>> {
|
||||
// Build WebSocket URL with optional auth
|
||||
let wsUrl = `${COPILOT_WS_URL}&clientSessionId=${crypto.randomUUID()}`;
|
||||
if (accessToken) {
|
||||
wsUrl += `&accessToken=${encodeURIComponent(accessToken)}`;
|
||||
}
|
||||
// Build WebSocket URL without credentials in query string
|
||||
const wsUrl = `${COPILOT_WS_URL}&clientSessionId=${crypto.randomUUID()}`;
|
||||
|
||||
return new ReadableStream({
|
||||
start: async (controller) => {
|
||||
@@ -261,13 +263,23 @@ export class CopilotWebExecutor extends BaseExecutor {
|
||||
signal?.addEventListener("abort", () => abort("Request aborted"), { once: true });
|
||||
|
||||
try {
|
||||
// Use Node.js built-in WebSocket if available, else dynamic import
|
||||
// Use Node.js built-in WebSocket if available, else dynamic import.
|
||||
// Pass the access token via Authorization header (not URL) to avoid
|
||||
// credential exposure in server logs.
|
||||
let WS = globalThis.WebSocket;
|
||||
if (!WS) {
|
||||
// @ts-ignore — ws module has no type declarations in this project
|
||||
WS = (await import("ws")).default as unknown as typeof WebSocket;
|
||||
if (accessToken) {
|
||||
// @ts-ignore — ws module supports headers option in second arg
|
||||
ws = new WS(wsUrl, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
}) as WebSocket;
|
||||
}
|
||||
}
|
||||
if (!ws) {
|
||||
ws = new WS(wsUrl) as WebSocket;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => abort("Copilot WebSocket timeout"), FETCH_TIMEOUT_MS);
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ async function handleVideo(nonce: string, prompt: string, aspectRatio: string):
|
||||
});
|
||||
const sceneData = genResult.trim();
|
||||
if (!sceneData || sceneData === "0" || sceneData.toLowerCase().includes("error")) {
|
||||
return errResp(`Video generation failed: ${sceneData}`);
|
||||
return errResp("Video generation failed");
|
||||
}
|
||||
|
||||
// Poll
|
||||
@@ -123,7 +123,7 @@ async function handleImage(nonce: string, prompt: string, aspectRatio: string):
|
||||
});
|
||||
const trimmed = result.trim();
|
||||
if (!trimmed || trimmed === "0" || trimmed.toLowerCase().includes("error")) {
|
||||
return errResp(`Image generation failed: ${trimmed}`);
|
||||
return errResp("Image generation failed");
|
||||
}
|
||||
// Response is comma-separated base64 PNGs or URLs
|
||||
const parts = trimmed
|
||||
@@ -191,7 +191,7 @@ async function handleTTS(prompt: string, voice?: string, lang?: string): Promise
|
||||
} catch {
|
||||
/* not JSON */
|
||||
}
|
||||
return errResp(`TTS unexpected response: ${data.slice(0, 200)}`);
|
||||
return errResp("TTS unexpected response format");
|
||||
}
|
||||
|
||||
async function handleEnhance(nonce: string, prompt: string): Promise<Response> {
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
extractComfyOutputFiles,
|
||||
} from "../utils/comfyuiClient.ts";
|
||||
import { fetchRemoteImage } from "@/shared/network/remoteImageFetch";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
|
||||
interface KieImageOptions {
|
||||
model: string;
|
||||
@@ -722,7 +723,11 @@ async function handleGeminiImageGeneration({ model, providerConfig, body, creden
|
||||
requestBody: logRequestBody,
|
||||
}).catch(() => {});
|
||||
|
||||
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,7 +1306,7 @@ async function handleFalAIImageGeneration({
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `Image provider error: ${err.message}`,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1491,7 +1496,7 @@ async function handleStabilityAIImageGeneration({
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `Image provider error: ${err.message}`,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1610,7 +1615,7 @@ async function handleBlackForestLabsImageGeneration({
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `Image provider error: ${err.message}`,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1685,7 +1690,7 @@ async function handleRecraftImageGeneration({
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `Image provider error: ${err.message}`,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1773,7 +1778,7 @@ async function handleTopazImageGeneration({
|
||||
model,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `Image provider error: ${err.message}`,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2353,7 +2358,7 @@ async function fetchImageEndpoint(url, headers, body, provider, log) {
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${err.message}`,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2448,7 +2453,11 @@ async function handleHyperbolicImageGeneration({
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2702,7 +2711,11 @@ async function handleNanoBananaImageGeneration({
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2894,7 +2907,11 @@ async function handleSDWebUIImageGeneration({ model, provider, providerConfig, b
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2996,7 +3013,11 @@ async function handleComfyUIImageGeneration({ model, provider, providerConfig, b
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3104,7 +3125,11 @@ async function handleHaiperImageGeneration({
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3232,7 +3257,11 @@ async function handleLeonardoImageGeneration({
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3318,7 +3347,11 @@ async function handleIdeogramImageGeneration({
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Image provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "../utils/comfyuiClient.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { getKieCallbackUrl, isJsonObject, parseKieResultJson } from "../utils/kieTask.ts";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
|
||||
function normalizeKieSunoModel(model: string): string {
|
||||
const map: Record<string, string> = {
|
||||
@@ -219,7 +220,11 @@ async function handleComfyUIMusicGeneration({ model, provider, providerConfig, b
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Music provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Music provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +374,7 @@ async function handleKieMusicGeneration({
|
||||
return {
|
||||
success: false,
|
||||
status: isJsonObject(err) && Number.isFinite(Number(err.status)) ? Number(err.status) : 502,
|
||||
error: `Music provider error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
error: sanitizeErrorMessage(err) || "Music provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -487,7 +492,11 @@ async function handleSunoMusicGeneration({
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Music provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Music provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +607,10 @@ async function handleUdioMusicGeneration({
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Music provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Music provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
extractComfyOutputFiles,
|
||||
} from "../utils/comfyuiClient.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
|
||||
/**
|
||||
* Handle video generation request
|
||||
@@ -200,7 +201,11 @@ async function handleComfyUIVideoGeneration({ model, provider, providerConfig, b
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Video provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Video provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +293,11 @@ async function handleSDWebUIVideoGeneration({ model, provider, providerConfig, b
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Video provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Video provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +428,7 @@ async function handleKieVideoGeneration({
|
||||
return {
|
||||
success: false,
|
||||
status: isJsonObject(err) && Number.isFinite(Number(err.status)) ? Number(err.status) : 502,
|
||||
error: `Video provider error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
error: sanitizeErrorMessage(err) || "Video provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -619,7 +628,11 @@ async function handleRunwayVideoGeneration({
|
||||
duration: Date.now() - startTime,
|
||||
error: err.message,
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: `Video provider error: ${err.message}` };
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: sanitizeErrorMessage(err) || "Video provider error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { buildClaudeAuthFile, ClaudeAuthFileError } from "@/lib/oauth/utils/claudeAuthFile";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
function toErrorResponse(error: unknown) {
|
||||
if (error instanceof ClaudeAuthFileError) {
|
||||
@@ -13,7 +14,7 @@ function toErrorResponse(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : "Failed to export Claude auth file";
|
||||
const message = sanitizeErrorMessage(error) || "Failed to export Claude auth file";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { buildCodexAuthFile, CodexAuthFileError } from "@/lib/oauth/utils/codexAuthFile";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
function toErrorResponse(error: unknown) {
|
||||
if (error instanceof CodexAuthFileError) {
|
||||
@@ -13,7 +14,7 @@ function toErrorResponse(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : "Failed to export Codex auth file";
|
||||
const message = sanitizeErrorMessage(error) || "Failed to export Codex auth file";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { buildGeminiAuthFile, GeminiAuthFileError } from "@/lib/oauth/utils/geminiAuthFile";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
function toErrorResponse(error: unknown) {
|
||||
if (error instanceof GeminiAuthFileError) {
|
||||
@@ -13,7 +14,7 @@ function toErrorResponse(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : "Failed to export Gemini auth file";
|
||||
const message = sanitizeErrorMessage(error) || "Failed to export Gemini auth file";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* OAuth Callback Page Content
|
||||
* OAuth Callback Page
|
||||
*
|
||||
* Reads URL params via window.location.search (not useSearchParams) to avoid
|
||||
* the Next.js Suspense boundary requirement, which can delay hydration in popup
|
||||
* windows that navigate back from a cross-origin OAuth page (e.g. Google).
|
||||
* Sends the callback data back via three methods in order of reliability:
|
||||
* 1. postMessage to window.opener (may be null after COOP cross-origin nav)
|
||||
* 2. BroadcastChannel (same-origin, works across browsing context groups)
|
||||
* 3. localStorage storage event (works across browsing context groups)
|
||||
*/
|
||||
function CallbackContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const [status, setStatus] = useState("processing");
|
||||
export default function CallbackPage() {
|
||||
const [status, setStatus] = useState<"processing" | "success" | "done" | "manual">("processing");
|
||||
const [currentUrl, setCurrentUrl] = useState("");
|
||||
const t = useTranslations("auth");
|
||||
|
||||
useEffect(() => {
|
||||
const code = searchParams.get("code");
|
||||
const state = searchParams.get("state");
|
||||
const error = searchParams.get("error");
|
||||
const errorDescription = searchParams.get("error_description");
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get("code");
|
||||
const state = params.get("state");
|
||||
const error = params.get("error");
|
||||
const errorDescription = params.get("error_description");
|
||||
|
||||
const callbackData = {
|
||||
code,
|
||||
@@ -29,23 +36,23 @@ function CallbackContent() {
|
||||
|
||||
let sent = false;
|
||||
|
||||
// Check if this callback is from expected origin/port
|
||||
const expectedOrigins = [
|
||||
window.location.origin, // Same origin (for most providers)
|
||||
"http://localhost:1455", // Codex specific port
|
||||
];
|
||||
|
||||
// Method 1: postMessage to opener (popup mode)
|
||||
// Method 1: postMessage to opener (popup mode).
|
||||
// May be null when Google OAuth's COOP header severs the opener reference.
|
||||
if (window.opener) {
|
||||
try {
|
||||
window.opener.postMessage({ type: "oauth_callback", data: callbackData }, "*"); // Allow any origin for local dev
|
||||
// Target this origin specifically — popup mode is only used when isTrueLocalhost,
|
||||
// so the opener is always on the same origin as the callback page.
|
||||
window.opener.postMessage(
|
||||
{ type: "oauth_callback", data: callbackData },
|
||||
window.location.origin
|
||||
);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
console.log("postMessage failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: BroadcastChannel (same origin tabs)
|
||||
// Method 2: BroadcastChannel — works across browsing context groups for same origin.
|
||||
try {
|
||||
const channel = new BroadcastChannel("oauth_callback");
|
||||
channel.postMessage(callbackData);
|
||||
@@ -55,7 +62,8 @@ function CallbackContent() {
|
||||
console.log("BroadcastChannel failed:", e);
|
||||
}
|
||||
|
||||
// Method 3: localStorage event (fallback)
|
||||
// Method 3: localStorage — triggers storage event in all same-origin windows,
|
||||
// regardless of browsing context group isolation from COOP.
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"oauth_callback",
|
||||
@@ -67,25 +75,25 @@ function CallbackContent() {
|
||||
}
|
||||
|
||||
if (sent && (code || error)) {
|
||||
// Use setTimeout to avoid synchronous setState in effect
|
||||
setTimeout(() => {
|
||||
// Only auto-close if opened as popup (has opener) — remote access keeps tab open
|
||||
if (window.opener) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- initialization effect, window-only
|
||||
setStatus("success");
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
// If can't close (not a popup), show success message
|
||||
// If close is prevented (browser policy), fall through to manual close prompt.
|
||||
setTimeout(() => setStatus("done"), 500);
|
||||
}, 1500);
|
||||
} else {
|
||||
// Opened as new tab (remote access) — show URL for manual copy
|
||||
// Opened as new tab or opener severed by COOP — show close prompt.
|
||||
setStatus("done");
|
||||
}
|
||||
}, 0);
|
||||
} else {
|
||||
setTimeout(() => setStatus("manual"), 0);
|
||||
// No code/error in URL or all send methods failed — show URL for manual copy.
|
||||
// Batch the URL and status update so they render together (React 18 auto-batching).
|
||||
setCurrentUrl(window.location.href);
|
||||
setStatus("manual");
|
||||
}
|
||||
}, [searchParams]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg">
|
||||
@@ -124,9 +132,7 @@ function CallbackContent() {
|
||||
<h1 className="text-xl font-semibold mb-2">{t("copyUrl")}</h1>
|
||||
<p className="text-text-muted mb-4">{t("copyUrlManual")}</p>
|
||||
<div className="bg-surface border border-border rounded-lg p-3 text-left">
|
||||
<code className="text-xs break-all">
|
||||
{typeof window !== "undefined" ? window.location.href : ""}
|
||||
</code>
|
||||
<code className="text-xs break-all">{currentUrl}</code>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -134,29 +140,3 @@ function CallbackContent() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Callback Page
|
||||
* Receives callback from OAuth providers and sends data back via multiple methods
|
||||
*/
|
||||
export default function CallbackPage() {
|
||||
const t = useTranslations("auth");
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg">
|
||||
<div className="text-center p-8">
|
||||
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-3xl text-primary animate-spin">
|
||||
progress_activity
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-muted">{t("loading")}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CallbackContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -704,13 +704,13 @@ export const autoSearchIndex: AutoGenSearchItem[] = [
|
||||
fileName: "reference/PROVIDER_REFERENCE.md",
|
||||
section: "Reference",
|
||||
content:
|
||||
"Auto-generated from src/shared/constants/providers.ts — do not edit by hand. Regenerate with: npm run gen:provider-reference Last generated: 2026-05-13 Total providers: 177. See category breakdown below. - Free — free tier with API key (configured via dashboard) - OAuth — sign-in flow handled by Omn",
|
||||
"Auto-generated from src/shared/constants/providers.ts — do not edit by hand. Regenerate with: npm run gen:provider-reference Last generated: 2026-05-17 Total providers: 177. See category breakdown below. - Free — free tier with API key (configured via dashboard) - OAuth — sign-in flow handled by Omn",
|
||||
headings: [
|
||||
"Categories",
|
||||
"Free Tier (OAuth-first or no-key) (5)",
|
||||
"OAuth Providers (11)",
|
||||
"Web Cookie Providers (5)",
|
||||
"API Key Providers (paid / paid-with-free-credits) (123)",
|
||||
"Web Cookie Providers (6)",
|
||||
"API Key Providers (paid / paid-with-free-credits) (122)",
|
||||
"Local Providers (10)",
|
||||
"Search Providers (11)",
|
||||
"Audio-only Providers (7)",
|
||||
|
||||
@@ -9,7 +9,9 @@ export function generateClaudeConfig(options: {
|
||||
model?: string;
|
||||
}): string {
|
||||
let base = options.baseUrl;
|
||||
while (base.endsWith("/")) base = base.slice(0, -1);
|
||||
let end = base.length;
|
||||
while (end > 0 && base[end - 1] === "/") end--;
|
||||
base = end < base.length ? base.slice(0, end) : base;
|
||||
if (base.endsWith("/v1")) base = base.slice(0, -3);
|
||||
const model = options.model || "claude-3-5-sonnet-20241022";
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ export function generateClineConfig(options: {
|
||||
model?: string;
|
||||
}): string {
|
||||
let base = options.baseUrl;
|
||||
while (base.endsWith("/")) base = base.slice(0, -1);
|
||||
let end = base.length;
|
||||
while (end > 0 && base[end - 1] === "/") end--;
|
||||
base = end < base.length ? base.slice(0, end) : base;
|
||||
if (base.endsWith("/v1")) base = base.slice(0, -3);
|
||||
|
||||
const config = {
|
||||
|
||||
@@ -18,7 +18,9 @@ export async function generateCodexConfig(options: {
|
||||
}): Promise<string> {
|
||||
const y = await loadYaml();
|
||||
let base = options.baseUrl;
|
||||
while (base.endsWith("/")) base = base.slice(0, -1);
|
||||
let end = base.length;
|
||||
while (end > 0 && base[end - 1] === "/") end--;
|
||||
base = end < base.length ? base.slice(0, end) : base;
|
||||
if (base.endsWith("/v1")) base = base.slice(0, -3);
|
||||
|
||||
const config = {
|
||||
|
||||
@@ -18,7 +18,9 @@ export async function generateContinueConfig(options: {
|
||||
}): Promise<string> {
|
||||
const y = await loadYaml();
|
||||
let base = options.baseUrl;
|
||||
while (base.endsWith("/")) base = base.slice(0, -1);
|
||||
let end = base.length;
|
||||
while (end > 0 && base[end - 1] === "/") end--;
|
||||
base = end < base.length ? base.slice(0, end) : base;
|
||||
if (base.endsWith("/v1")) base = base.slice(0, -3);
|
||||
|
||||
const config = {
|
||||
|
||||
@@ -9,7 +9,9 @@ export function generateKilocodeConfig(options: {
|
||||
model?: string;
|
||||
}): string {
|
||||
let base = options.baseUrl;
|
||||
while (base.endsWith("/")) base = base.slice(0, -1);
|
||||
let end = base.length;
|
||||
while (end > 0 && base[end - 1] === "/") end--;
|
||||
base = end < base.length ? base.slice(0, end) : base;
|
||||
if (base.endsWith("/v1")) base = base.slice(0, -3);
|
||||
|
||||
const config = {
|
||||
|
||||
@@ -9,7 +9,9 @@ export function generateOpencodeConfig(options: {
|
||||
model?: string;
|
||||
}): string {
|
||||
let base = options.baseUrl;
|
||||
while (base.endsWith("/")) base = base.slice(0, -1);
|
||||
let end = base.length;
|
||||
while (end > 0 && base[end - 1] === "/") end--;
|
||||
base = end < base.length ? base.slice(0, end) : base;
|
||||
if (base.endsWith("/v1")) base = base.slice(0, -3);
|
||||
|
||||
const config = {
|
||||
|
||||
@@ -90,6 +90,7 @@ export const gemini = {
|
||||
mapTokens: (tokens, extra) => ({
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
idToken: tokens.id_token ?? null,
|
||||
expiresIn: tokens.expires_in,
|
||||
scope: tokens.scope,
|
||||
email: extra?.userInfo?.email,
|
||||
|
||||
Reference in New Issue
Block a user