OmniRoute/tests/unit/codexAuthImport.test.ts
diegosouzapw 8a6d681c15 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
2026-05-17 14:04:48 -03:00

247 lines
8.3 KiB
TypeScript

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");
});