feat(codex): import single Codex auth.json as OAuth connection

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)
- `<ImportCodexAuthModal>` 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
This commit is contained in:
diegosouzapw
2026-05-17 14:04:48 -03:00
parent 02564d5a5b
commit 8a6d681c15
7 changed files with 1007 additions and 0 deletions

View File

@@ -1059,6 +1059,7 @@ export default function ProviderDetailPage() {
);
const [applyingCodexAuthId, setApplyingCodexAuthId] = useState<string | null>(null);
const [exportingCodexAuthId, setExportingCodexAuthId] = useState<string | null>(null);
const [importCodexModalOpen, setImportCodexModalOpen] = useState(false);
const [codexGlobalFastServiceTier, setCodexGlobalFastServiceTier] = useState(false);
const [savingCodexGlobalFastServiceTier, setSavingCodexGlobalFastServiceTier] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -3339,6 +3340,17 @@ export default function ProviderDetailPage() {
Experimental OAuth
</Button>
)}
{providerId === "codex" && (
<Button
variant="secondary"
icon="upload_file"
onClick={() => setImportCodexModalOpen(true)}
>
{typeof t.has === "function" && t.has("importCodexAuth")
? t("importCodexAuth")
: "Import auth"}
</Button>
)}
</>
)}
</div>
@@ -3787,6 +3799,17 @@ export default function ProviderDetailPage() {
isCcCompatible={isCcCompatible}
/>
)}
{/* Codex Import Auth Modal */}
{providerId === "codex" && importCodexModalOpen && (
<ImportCodexAuthModal
key={importCodexModalOpen ? "open" : "closed"}
onClose={() => setImportCodexModalOpen(false)}
onSuccess={() => {
setImportCodexModalOpen(false);
fetchData();
}}
/>
)}
{/* Batch Test Results Modal */}
{batchTestResults && (
<div
@@ -6972,6 +6995,279 @@ function AddApiKeyModal({
);
}
interface ImportCodexAuthModalProps {
onClose: () => 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<unknown>(null);
const [parseError, setParseError] = useState<string | null>(null);
const [detectedEmail, setDetectedEmail] = useState<string | null>(null);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [overwriteExisting, setOverwriteExisting] = useState(false);
const [loading, setLoading] = useState(false);
const [resultError, setResultError] = useState<string | null>(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<string, unknown>) : 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<string, unknown>)
: 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<HTMLInputElement>) {
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 (
<Modal
isOpen
onClose={onClose}
title={t("codexImportModalTitle") || "Import Codex Auth"}
maxWidth="max-w-lg"
>
<div className="flex flex-col gap-4">
{/* Tabs */}
<div className="flex border-b border-border">
{(["upload", "paste"] as const).map((id) => (
<button
key={id}
onClick={() => {
setTab(id);
setParsedJson(null);
setParseError(null);
setDetectedEmail(null);
}}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
tab === id
? "border-primary text-primary"
: "border-transparent text-text-muted hover:text-text-main"
}`}
>
{id === "upload"
? t("codexImportTabUpload") || "Upload file"
: t("codexImportTabPaste") || "Paste JSON"}
</button>
))}
</div>
{/* Upload tab */}
{tab === "upload" && (
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-text-main">
{t("codexImportFileLabel") || "Choose auth.json"}
</label>
<input
type="file"
accept=".json"
onChange={handleFileChange}
className="text-sm text-text-muted file:mr-3 file:py-1.5 file:px-3 file:rounded-lg file:border file:border-border file:text-xs file:bg-bg-subtle file:text-text-main hover:file:bg-bg-hover cursor-pointer"
/>
<p className="text-xs text-text-muted">
{t("codexImportFileHint") ||
"Select the auth.json file exported from Codex or OmniRoute."}
</p>
</div>
)}
{/* Paste tab */}
{tab === "paste" && (
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-text-main">
{t("codexImportPasteLabel") || "Paste the JSON content"}
</label>
<textarea
value={pasteText}
onChange={(e) => handlePasteChange(e.target.value)}
rows={8}
placeholder='{ "auth_mode": "chatgpt", ... }'
className="w-full rounded-lg border border-border bg-bg-subtle px-3 py-2 text-xs font-mono text-text-main placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/40 resize-none"
/>
</div>
)}
{/* Parse error */}
{parseError && <p className="text-sm text-red-500">{parseError}</p>}
{/* Detected email preview */}
{detectedEmail && !parseError && (
<p className="text-xs text-text-muted">
{t("codexImportDetectedEmail", { email: detectedEmail }) ||
`Detected: ${detectedEmail}`}
</p>
)}
{/* Shared fields */}
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-text-main">
{t("codexImportEmailLabel") || "Account email"}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="user@example.com"
className="rounded-lg border border-border bg-bg-subtle px-3 py-2 text-sm text-text-main placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/40"
/>
<p className="text-xs text-text-muted">
{t("codexImportEmailHint") || "Auto-detected from the file; edit if needed."}
</p>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-text-main">
{t("codexImportNameLabel") || "Connection name (optional)"}
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={email || "Codex (imported)"}
className="rounded-lg border border-border bg-bg-subtle px-3 py-2 text-sm text-text-main placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/40"
/>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={overwriteExisting}
onChange={(e) => setOverwriteExisting(e.target.checked)}
className="rounded border-border"
/>
<span className="text-sm text-text-main">
{t("codexImportOverwriteLabel") ||
"Replace existing connection if account already exists"}
</span>
</label>
</div>
{/* Result error banner */}
{resultError && (
<div className="rounded-lg bg-red-500/10 border border-red-500/20 px-4 py-3 text-sm text-red-400">
{resultError}
</div>
)}
<div className="flex gap-2 pt-1">
<Button onClick={handleSubmit} disabled={!canSubmit} loading={loading} fullWidth>
{t("codexImportSubmit") || "Import"}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
{t("cancel")}
</Button>
</div>
</div>
</Modal>
);
}
function normalizeAndValidateHttpBaseUrl(rawValue, fallbackUrl) {
const value = (typeof rawValue === "string" ? rawValue.trim() : "") || fallbackUrl;
try {

View File

@@ -0,0 +1,96 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { CodexAuthFileError } from "@/lib/oauth/utils/codexAuthFile";
import {
parseAndValidateCodexAuth,
createConnectionFromAuthFile,
} from "@/lib/oauth/utils/codexAuthImport";
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index";
import { getProviderAuditTarget } from "@/lib/compliance/providerAudit";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { importCodexAuthSchema } from "@/shared/validation/schemas";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { sanitizeProviderSpecificDataForResponse } from "@/lib/providers/requestDefaults";
function sanitizeConnectionForResponse(connection: Record<string, unknown>) {
const safe = { ...connection };
delete safe.accessToken;
delete safe.refreshToken;
delete safe.idToken;
delete safe.apiKey;
if (safe.providerSpecificData) {
safe.providerSpecificData = sanitizeProviderSpecificDataForResponse(safe.providerSpecificData);
}
return safe;
}
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
const auditContext = getAuditRequestContext(request);
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const parsedBody = validateBody(importCodexAuthSchema, body);
if (isValidationFailure(parsedBody)) {
return NextResponse.json({ error: parsedBody.error }, { status: 400 });
}
const { source, name, email, overwriteExisting } = parsedBody.data;
let rawJson: unknown;
try {
rawJson = source.kind === "json" ? source.json : JSON.parse(source.text);
} catch {
return NextResponse.json(
{ error: "Could not parse the content as JSON", code: "invalid_json" },
{ status: 400 }
);
}
try {
const parsed = parseAndValidateCodexAuth(rawJson);
const { connection, created } = await createConnectionFromAuthFile(parsed, {
name,
email,
overwriteExisting,
});
logAuditEvent({
action: "provider.credentials.imported",
actor: "admin",
target: getProviderAuditTarget(connection),
resourceType: "provider_credentials",
status: "success",
ipAddress: auditContext.ipAddress || undefined,
requestId: auditContext.requestId,
metadata: {
provider: "codex",
created,
email: parsed.email || email,
},
});
return NextResponse.json({
connection: sanitizeConnectionForResponse(connection as Record<string, unknown>),
created,
});
} catch (error) {
if (error instanceof CodexAuthFileError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.status }
);
}
return NextResponse.json(
{ error: sanitizeErrorMessage(error) || "Failed to import Codex auth" },
{ status: 500 }
);
}
}

View File

@@ -3106,6 +3106,25 @@
"codexAuthApplyFailed": "Failed to apply Codex auth.json locally",
"codexAuthExported": "Codex auth.json exported",
"codexAuthExportFailed": "Failed to export Codex auth.json",
"importCodexAuth": "Import auth",
"codexImportModalTitle": "Import Codex Auth",
"codexImportTabUpload": "Upload file",
"codexImportTabPaste": "Paste JSON",
"codexImportFileLabel": "Choose auth.json",
"codexImportFileHint": "Select the auth.json file exported from Codex or from OmniRoute.",
"codexImportPasteLabel": "Paste the JSON content",
"codexImportEmailLabel": "Account email",
"codexImportEmailHint": "Auto-detected from the file; edit if needed.",
"codexImportNameLabel": "Connection name (optional)",
"codexImportOverwriteLabel": "Replace existing connection if account already exists",
"codexImportSubmit": "Import",
"codexImportSuccess": "Codex connection imported successfully",
"codexImportInvalidJson": "Could not parse the file as JSON",
"codexImportInvalidShape": "The file is not a valid Codex auth.json",
"codexImportDuplicate": "Account already exists — enable \"Replace existing\" to overwrite",
"codexImportFailed": "Failed to import Codex auth",
"codexImportDetectedEmail": "Detected: {email}",
"codexImportNoEmailDetected": "No email detected in file",
"advancedSettings": "Advanced Settings",
"chatPathLabel": "Chat Endpoint Path",
"chatPathPlaceholder": "/chat/completions",

View File

@@ -0,0 +1,219 @@
import {
getProviderConnections,
createProviderConnection,
updateProviderConnection,
} from "@/lib/localDb";
import { CodexAuthFileError } from "@/lib/oauth/utils/codexAuthFile";
type JsonRecord = Record<string, unknown>;
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function decodeJwtPayload(jwt: string): JsonRecord | null {
try {
const parts = jwt.split(".");
if (parts.length !== 3) return null;
const payload = Buffer.from(parts[1], "base64url").toString("utf8");
return toRecord(JSON.parse(payload));
} catch {
return null;
}
}
function extractExpiresAt(idToken: string): string | null {
const payload = decodeJwtPayload(idToken);
if (!payload) return null;
const exp = payload.exp;
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
return new Date(exp * 1000).toISOString();
}
function extractJwtEmail(idToken: string): string | null {
const payload = decodeJwtPayload(idToken);
if (!payload) return null;
return toNonEmptyString(payload.email);
}
function extractCodexAccountId(
idToken: string,
tokensAccountId: string | undefined
): string | null {
if (tokensAccountId && tokensAccountId.trim()) return tokensAccountId.trim();
const payload = decodeJwtPayload(idToken);
const authInfo = payload ? toRecord(payload["https://api.openai.com/auth"]) : {};
return (
toNonEmptyString(authInfo.chatgpt_account_id) || toNonEmptyString(authInfo.account_id) || null
);
}
// ──── Public types ────────────────────────────────────────────────────────────
export interface CodexAuthFileInput {
auth_mode?: unknown;
OPENAI_API_KEY?: unknown;
tokens?: {
id_token?: unknown;
access_token?: unknown;
refresh_token?: unknown;
account_id?: unknown;
};
last_refresh?: unknown;
}
export interface ParsedCodexAuth {
idToken: string;
accessToken: string;
refreshToken: string;
accountId: string;
email: string | null;
expiresAt: string | null;
}
export interface CreateConnectionOptions {
name?: string;
email?: string;
overwriteExisting?: boolean;
}
// ──── Parse & validate ────────────────────────────────────────────────────────
export function parseAndValidateCodexAuth(raw: unknown): ParsedCodexAuth {
const doc = toRecord(raw);
if (doc.auth_mode !== "chatgpt") {
throw new CodexAuthFileError(
'Not a Codex auth.json — expected auth_mode: "chatgpt"',
400,
"invalid_auth_file"
);
}
const tokens = toRecord(doc.tokens);
const idToken = toNonEmptyString(tokens.id_token);
const accessToken = toNonEmptyString(tokens.access_token);
const refreshToken = toNonEmptyString(tokens.refresh_token);
if (!idToken) {
throw new CodexAuthFileError(
"id_token is missing or empty in the auth.json",
400,
"missing_id_token"
);
}
if (!accessToken) {
throw new CodexAuthFileError(
"access_token is missing or empty in the auth.json",
400,
"missing_access_token"
);
}
if (!refreshToken) {
throw new CodexAuthFileError(
"refresh_token is missing or empty in the auth.json",
400,
"missing_refresh_token"
);
}
const tokensAccountId = toNonEmptyString(tokens.account_id) ?? undefined;
const accountId = extractCodexAccountId(idToken, tokensAccountId);
if (!accountId) {
throw new CodexAuthFileError(
"Unable to derive account_id from the auth.json tokens",
400,
"missing_account_id"
);
}
return {
idToken,
accessToken,
refreshToken,
accountId,
email: extractJwtEmail(idToken),
expiresAt: extractExpiresAt(idToken),
};
}
// ──── Create / update connection ──────────────────────────────────────────────
export async function createConnectionFromAuthFile(
parsed: ParsedCodexAuth,
options: CreateConnectionOptions
): Promise<{ connection: JsonRecord; created: boolean }> {
const existing = await findExistingCodexConnection(parsed.accountId);
if (existing) {
if (!options.overwriteExisting) {
throw new CodexAuthFileError(
"A Codex connection for this account already exists. Pass overwriteExisting: true to replace it.",
409,
"duplicate_account"
);
}
const updated = await updateProviderConnection(existing.id as string, {
accessToken: parsed.accessToken,
refreshToken: parsed.refreshToken,
idToken: parsed.idToken,
expiresAt: parsed.expiresAt,
email: options.email || parsed.email || (existing.email as string | undefined),
name:
options.name ||
(existing.name as string | undefined) ||
options.email ||
parsed.email ||
"Codex (imported)",
testStatus: "active",
providerSpecificData: {
...toRecord(existing.providerSpecificData),
workspaceId: parsed.accountId,
importedAt: new Date().toISOString(),
},
});
return { connection: updated || existing, created: false };
}
const name = options.name || options.email || parsed.email || "Codex (imported)";
const connection = await createProviderConnection({
provider: "codex",
authType: "oauth",
name,
email: options.email || parsed.email || undefined,
accessToken: parsed.accessToken,
refreshToken: parsed.refreshToken,
idToken: parsed.idToken,
expiresAt: parsed.expiresAt,
isActive: true,
testStatus: "active",
providerSpecificData: {
workspaceId: parsed.accountId,
importedAt: new Date().toISOString(),
},
});
return { connection, created: true };
}
async function findExistingCodexConnection(accountId: string): Promise<JsonRecord | null> {
const connections = await getProviderConnections({ provider: "codex" });
return (
(connections.find((c) => {
const psd = toRecord((c as JsonRecord).providerSpecificData);
return toNonEmptyString(psd.workspaceId) === accountId;
}) as JsonRecord | undefined) ?? null
);
}

View File

@@ -322,6 +322,21 @@ export const bulkCreateProviderSchema = z
}
});
// ──── Codex Import Schema ────
export const importCodexAuthSchema = z.object({
source: z.discriminatedUnion("kind", [
z.object({ kind: z.literal("json"), json: z.unknown() }),
z.object({
kind: z.literal("text"),
text: z.string().max(256 * 1024, "Paste content must be under 256 KB"),
}),
]),
name: z.string().min(1).max(200).optional(),
email: z.string().email("Must be a valid email").optional(),
overwriteExisting: z.boolean().optional(),
});
// ──── API Key Schemas ────
export const createKeySchema = z.object({

View File

@@ -0,0 +1,115 @@
import test from "node:test";
import assert from "node:assert/strict";
import { z } from "zod";
// Local copy of importCodexAuthSchema — avoids importing Next.js deps from schemas.ts.
const importCodexAuthSchema = z.object({
source: z.discriminatedUnion("kind", [
z.object({ kind: z.literal("json"), json: z.unknown() }),
z.object({
kind: z.literal("text"),
text: z.string().max(256 * 1024),
}),
]),
name: z.string().min(1).max(200).optional(),
email: z.string().email().optional(),
overwriteExisting: z.boolean().optional(),
});
function parse(body: unknown) {
return importCodexAuthSchema.safeParse(body);
}
// ──── Valid cases ─────────────────────────────────────────────────────────────
test("schema: valid json source", () => {
const result = parse({
source: { kind: "json", json: { auth_mode: "chatgpt" } },
});
assert.ok(result.success);
assert.equal(result.data.source.kind, "json");
});
test("schema: valid text source", () => {
const result = parse({
source: { kind: "text", text: JSON.stringify({ auth_mode: "chatgpt" }) },
});
assert.ok(result.success);
assert.equal(result.data.source.kind, "text");
});
test("schema: optional fields are optional", () => {
const result = parse({ source: { kind: "json", json: {} } });
assert.ok(result.success);
assert.equal(result.data.name, undefined);
assert.equal(result.data.email, undefined);
assert.equal(result.data.overwriteExisting, undefined);
});
test("schema: all optional fields accepted", () => {
const result = parse({
source: { kind: "json", json: {} },
name: "My Account",
email: "user@example.com",
overwriteExisting: true,
});
assert.ok(result.success);
assert.equal(result.data.name, "My Account");
assert.equal(result.data.email, "user@example.com");
assert.equal(result.data.overwriteExisting, true);
});
// ──── Invalid cases ───────────────────────────────────────────────────────────
test("schema: missing source fails", () => {
const result = parse({});
assert.ok(!result.success);
});
test("schema: unknown kind fails", () => {
const result = parse({ source: { kind: "file" } });
assert.ok(!result.success);
});
test("schema: invalid email fails", () => {
const result = parse({
source: { kind: "json", json: {} },
email: "not-an-email",
});
assert.ok(!result.success);
const emailIssue = result.error.issues.find((i) => i.path.includes("email"));
assert.ok(emailIssue, "expected email validation issue");
});
test("schema: empty name fails", () => {
const result = parse({
source: { kind: "json", json: {} },
name: "",
});
assert.ok(!result.success);
});
test("schema: text source with oversized content fails", () => {
const bigText = "x".repeat(256 * 1024 + 1);
const result = parse({ source: { kind: "text", text: bigText } });
assert.ok(!result.success);
});
test("schema: text source exactly at 256KB limit passes", () => {
const maxText = "x".repeat(256 * 1024);
const result = parse({ source: { kind: "text", text: maxText } });
assert.ok(result.success);
});
test("schema: source missing kind fails", () => {
const result = parse({ source: { json: {} } });
assert.ok(!result.success);
});
test("schema: overwriteExisting must be boolean", () => {
const result = parse({
source: { kind: "json", json: {} },
overwriteExisting: "yes",
});
assert.ok(!result.success);
});

View File

@@ -0,0 +1,247 @@
import test from "node:test";
import assert from "node:assert/strict";
// Pure-function copy of helpers from codexAuthImport.ts so we don't drag DB deps.
type JsonRecord = Record<string, unknown>;
function buildJwt(payload: JsonRecord): string {
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
return `${header}.${body}.fake-signature`;
}
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function decodeJwtPayload(jwt: string): JsonRecord | null {
try {
const parts = jwt.split(".");
if (parts.length !== 3) return null;
const payload = Buffer.from(parts[1], "base64url").toString("utf8");
return toRecord(JSON.parse(payload));
} catch {
return null;
}
}
function extractJwtEmail(idToken: string): string | null {
const payload = decodeJwtPayload(idToken);
if (!payload) return null;
return toNonEmptyString(payload.email);
}
function extractExpiresAt(idToken: string): string | null {
const payload = decodeJwtPayload(idToken);
if (!payload) return null;
const exp = payload.exp;
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
return new Date(exp * 1000).toISOString();
}
function extractCodexAccountId(
idToken: string,
tokensAccountId: string | undefined
): string | null {
if (tokensAccountId && tokensAccountId.trim()) return tokensAccountId.trim();
const payload = decodeJwtPayload(idToken);
const authInfo = payload ? toRecord(payload["https://api.openai.com/auth"]) : {};
return (
toNonEmptyString(authInfo.chatgpt_account_id) || toNonEmptyString(authInfo.account_id) || null
);
}
// Mirror of parseAndValidateCodexAuth (without the throw — just the logic)
interface ParsedCodexAuth {
idToken: string;
accessToken: string;
refreshToken: string;
accountId: string;
email: string | null;
expiresAt: string | null;
}
function parseCodexAuth(raw: unknown): ParsedCodexAuth | { error: string; code: string } {
const doc = toRecord(raw);
if (doc.auth_mode !== "chatgpt") {
return { error: "Not a Codex auth.json", code: "invalid_auth_file" };
}
const tokens = toRecord(doc.tokens);
const idToken = toNonEmptyString(tokens.id_token);
const accessToken = toNonEmptyString(tokens.access_token);
const refreshToken = toNonEmptyString(tokens.refresh_token);
if (!idToken) return { error: "missing id_token", code: "missing_id_token" };
if (!accessToken) return { error: "missing access_token", code: "missing_access_token" };
if (!refreshToken) return { error: "missing refresh_token", code: "missing_refresh_token" };
const tokensAccountId = toNonEmptyString(tokens.account_id) ?? undefined;
const accountId = extractCodexAccountId(idToken, tokensAccountId);
if (!accountId) return { error: "missing account_id", code: "missing_account_id" };
return {
idToken,
accessToken,
refreshToken,
accountId,
email: extractJwtEmail(idToken),
expiresAt: extractExpiresAt(idToken),
};
}
// ──── Tests ───────────────────────────────────────────────────────────────────
test("parseCodexAuth: valid file returns all fields", () => {
const idToken = buildJwt({
email: "alice@example.com",
exp: 9999999999,
"https://api.openai.com/auth": { chatgpt_account_id: "acct-abc123" },
});
const raw = {
auth_mode: "chatgpt",
OPENAI_API_KEY: null,
tokens: {
id_token: idToken,
access_token: "at-xxx",
refresh_token: "rt-yyy",
account_id: "acct-abc123",
},
last_refresh: new Date().toISOString(),
};
const result = parseCodexAuth(raw);
assert.ok(!("error" in result));
const parsed = result as ParsedCodexAuth;
assert.equal(parsed.idToken, idToken);
assert.equal(parsed.accessToken, "at-xxx");
assert.equal(parsed.refreshToken, "rt-yyy");
assert.equal(parsed.accountId, "acct-abc123");
assert.equal(parsed.email, "alice@example.com");
assert.ok(parsed.expiresAt !== null);
});
test("parseCodexAuth: wrong auth_mode returns error", () => {
const result = parseCodexAuth({ auth_mode: "api_key", tokens: {} });
assert.ok("error" in result);
assert.equal((result as { code: string }).code, "invalid_auth_file");
});
test("parseCodexAuth: missing id_token returns error", () => {
const result = parseCodexAuth({
auth_mode: "chatgpt",
tokens: { access_token: "at", refresh_token: "rt" },
});
assert.ok("error" in result);
assert.equal((result as { code: string }).code, "missing_id_token");
});
test("parseCodexAuth: missing access_token returns error", () => {
const idToken = buildJwt({ email: "a@b.com" });
const result = parseCodexAuth({
auth_mode: "chatgpt",
tokens: { id_token: idToken, refresh_token: "rt" },
});
assert.ok("error" in result);
assert.equal((result as { code: string }).code, "missing_access_token");
});
test("parseCodexAuth: missing refresh_token returns error", () => {
const idToken = buildJwt({ email: "a@b.com" });
const result = parseCodexAuth({
auth_mode: "chatgpt",
tokens: { id_token: idToken, access_token: "at" },
});
assert.ok("error" in result);
assert.equal((result as { code: string }).code, "missing_refresh_token");
});
test("JWT email extraction: email claim extracted", () => {
const jwt = buildJwt({ email: "test@example.com", sub: "123" });
assert.equal(extractJwtEmail(jwt), "test@example.com");
});
test("JWT email extraction: no email claim returns null", () => {
const jwt = buildJwt({ sub: "123" });
assert.equal(extractJwtEmail(jwt), null);
});
test("JWT email extraction: malformed JWT returns null", () => {
assert.equal(extractJwtEmail("not.a.valid.jwt.at.all"), null);
});
test("extractCodexAccountId: tokens.account_id wins over JWT claim", () => {
const jwt = buildJwt({
"https://api.openai.com/auth": { chatgpt_account_id: "claim-id" },
});
assert.equal(extractCodexAccountId(jwt, "direct-id"), "direct-id");
});
test("extractCodexAccountId: falls back to JWT chatgpt_account_id claim", () => {
const jwt = buildJwt({
"https://api.openai.com/auth": { chatgpt_account_id: "claim-id" },
});
assert.equal(extractCodexAccountId(jwt, undefined), "claim-id");
});
test("extractCodexAccountId: falls back to account_id claim", () => {
const jwt = buildJwt({
"https://api.openai.com/auth": { account_id: "acct-fallback" },
});
assert.equal(extractCodexAccountId(jwt, undefined), "acct-fallback");
});
test("extractCodexAccountId: returns null when no id available", () => {
const jwt = buildJwt({ sub: "123" });
assert.equal(extractCodexAccountId(jwt, undefined), null);
});
test("extractExpiresAt: derives ISO date from exp claim", () => {
const expUnix = 1900000000;
const jwt = buildJwt({ exp: expUnix });
const result = extractExpiresAt(jwt);
assert.ok(result !== null);
assert.equal(new Date(result).getTime(), expUnix * 1000);
});
test("extractExpiresAt: returns null when no exp claim", () => {
const jwt = buildJwt({ sub: "123" });
assert.equal(extractExpiresAt(jwt), null);
});
test("parseCodexAuth: accountId from tokens.account_id takes precedence over JWT", () => {
const idToken = buildJwt({
"https://api.openai.com/auth": { chatgpt_account_id: "jwt-id" },
});
const result = parseCodexAuth({
auth_mode: "chatgpt",
tokens: {
id_token: idToken,
access_token: "at",
refresh_token: "rt",
account_id: "direct-id",
},
});
assert.ok(!("error" in result));
assert.equal((result as ParsedCodexAuth).accountId, "direct-id");
});
test("parseCodexAuth: non-object input returns error", () => {
const result = parseCodexAuth("not an object");
assert.ok("error" in result);
assert.equal((result as { code: string }).code, "invalid_auth_file");
});
test("parseCodexAuth: null input returns error", () => {
const result = parseCodexAuth(null);
assert.ok("error" in result);
assert.equal((result as { code: string }).code, "invalid_auth_file");
});