From 8a6d681c15f72abde5392cd16e6efa118deffa59 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 17 May 2026 14:04:48 -0300 Subject: [PATCH] feat(codex): import single Codex auth.json as OAuth connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an import flow that lets users bring an existing Codex auth.json into OmniRoute without a fresh OAuth login. Both a file-upload tab and a paste-JSON tab are supported. - `codexAuthImport.ts`: pure parser + createConnectionFromAuthFile (conflict detection, overwriteExisting, JWT email/exp extraction) - `POST /api/providers/codex-auth/import`: Zod-validated endpoint with audit log (`provider.credentials.imported`) - `importCodexAuthSchema` in schemas.ts (discriminated union json/text, 256 KB cap on paste source) - `` in providers/[id]/page.tsx with upload/paste tabs, email auto-detection, name/email/overwrite fields - "Import auth" toolbar button shown only on the Codex provider page - 29 unit tests (17 parser + 12 schema) — all passing --- .../dashboard/providers/[id]/page.tsx | 296 ++++++++++++++++++ .../api/providers/codex-auth/import/route.ts | 96 ++++++ src/i18n/messages/en.json | 19 ++ src/lib/oauth/utils/codexAuthImport.ts | 219 +++++++++++++ src/shared/validation/schemas.ts | 15 + tests/unit/codex-import-route.test.ts | 115 +++++++ tests/unit/codexAuthImport.test.ts | 247 +++++++++++++++ 7 files changed, 1007 insertions(+) create mode 100644 src/app/api/providers/codex-auth/import/route.ts create mode 100644 src/lib/oauth/utils/codexAuthImport.ts create mode 100644 tests/unit/codex-import-route.test.ts create mode 100644 tests/unit/codexAuthImport.test.ts diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 587cfb1b93..b9a9993e47 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -1059,6 +1059,7 @@ export default function ProviderDetailPage() { ); const [applyingCodexAuthId, setApplyingCodexAuthId] = useState(null); const [exportingCodexAuthId, setExportingCodexAuthId] = useState(null); + const [importCodexModalOpen, setImportCodexModalOpen] = useState(false); const [codexGlobalFastServiceTier, setCodexGlobalFastServiceTier] = useState(false); const [savingCodexGlobalFastServiceTier, setSavingCodexGlobalFastServiceTier] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); @@ -3339,6 +3340,17 @@ export default function ProviderDetailPage() { Experimental OAuth )} + {providerId === "codex" && ( + + )} )} @@ -3787,6 +3799,17 @@ export default function ProviderDetailPage() { isCcCompatible={isCcCompatible} /> )} + {/* Codex Import Auth Modal */} + {providerId === "codex" && importCodexModalOpen && ( + setImportCodexModalOpen(false)} + onSuccess={() => { + setImportCodexModalOpen(false); + fetchData(); + }} + /> + )} {/* Batch Test Results Modal */} {batchTestResults && (
void; + onSuccess: () => void; +} + +function ImportCodexAuthModal({ onClose, onSuccess }: ImportCodexAuthModalProps) { + const t = useTranslations("providers"); + const notify = useNotificationStore(); + const [tab, setTab] = useState<"upload" | "paste">("upload"); + const [pasteText, setPasteText] = useState(""); + const [parsedJson, setParsedJson] = useState(null); + const [parseError, setParseError] = useState(null); + const [detectedEmail, setDetectedEmail] = useState(null); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [overwriteExisting, setOverwriteExisting] = useState(false); + const [loading, setLoading] = useState(false); + const [resultError, setResultError] = useState(null); + + function extractEmailFromJwt(idToken: string): string | null { + try { + const parts = idToken.split("."); + if (parts.length !== 3) return null; + const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")); + return typeof payload.email === "string" ? payload.email : null; + } catch { + return null; + } + } + + function tryParseAndPreview(json: unknown) { + setParseError(null); + setDetectedEmail(null); + setParsedJson(null); + try { + const doc = json && typeof json === "object" ? (json as Record) : null; + if (!doc || doc.auth_mode !== "chatgpt") { + setParseError(t("codexImportInvalidShape") || "Not a valid Codex auth.json"); + return; + } + const tokens = + doc.tokens && typeof doc.tokens === "object" + ? (doc.tokens as Record) + : null; + if (!tokens?.id_token || typeof tokens.id_token !== "string") { + setParseError(t("codexImportInvalidShape") || "Not a valid Codex auth.json"); + return; + } + const detected = extractEmailFromJwt(tokens.id_token as string); + setDetectedEmail(detected); + if (detected && !email) setEmail(detected); + setParsedJson(doc); + } catch { + setParseError(t("codexImportInvalidJson") || "Could not parse JSON"); + } + } + + function handleFileChange(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const text = ev.target?.result as string; + try { + const json = JSON.parse(text); + tryParseAndPreview(json); + setParsedJson(json); + } catch { + setParseError(t("codexImportInvalidJson") || "Could not parse JSON"); + setParsedJson(null); + } + }; + reader.readAsText(file); + } + + function handlePasteChange(text: string) { + setPasteText(text); + if (!text.trim()) { + setParsedJson(null); + setParseError(null); + setDetectedEmail(null); + return; + } + try { + const json = JSON.parse(text); + tryParseAndPreview(json); + } catch { + setParseError(t("codexImportInvalidJson") || "Could not parse JSON"); + setParsedJson(null); + } + } + + async function handleSubmit() { + if (!parsedJson) return; + setLoading(true); + setResultError(null); + try { + const res = await fetch("/api/providers/codex-auth/import", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + source: { kind: "json", json: parsedJson }, + name: name.trim() || undefined, + email: email.trim() || undefined, + overwriteExisting, + }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + if (data.code === "duplicate_account") { + setResultError( + t("codexImportDuplicate") || + "Account already exists — enable Replace existing to overwrite" + ); + } else { + setResultError(data.error || t("codexImportFailed") || "Failed to import Codex auth"); + } + return; + } + notify.success(t("codexImportSuccess") || "Codex connection imported successfully"); + onSuccess(); + } catch { + setResultError(t("codexImportFailed") || "Failed to import Codex auth"); + } finally { + setLoading(false); + } + } + + const canSubmit = !!parsedJson && !parseError && !loading; + + return ( + +
+ {/* Tabs */} +
+ {(["upload", "paste"] as const).map((id) => ( + + ))} +
+ + {/* Upload tab */} + {tab === "upload" && ( +
+ + +

+ {t("codexImportFileHint") || + "Select the auth.json file exported from Codex or OmniRoute."} +

+
+ )} + + {/* Paste tab */} + {tab === "paste" && ( +
+ +