Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/lib/oauth/services/codexImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,19 @@ export type CodexImportPayload = {
email: string;
expiresAt: string;
testStatus: "active";
isActive: true;
errorCode: null;
lastError: null;
lastErrorAt: null;
lastErrorType: null;
lastErrorSource: null;
backoffLevel: 0;
rateLimitedUntil: null;
priority?: number;
providerSpecificData?: {
chatgptAccountId?: string;
// Canonical alias consumed by the existing Codex workspace upsert path.
workspaceId?: string;
chatgptPlanType?: string;
};
};
Expand Down Expand Up @@ -224,7 +235,12 @@ export function normalizeCodexImportRecord(input: unknown): NormalizeResult {
const expiresAt = parseExpiry(rec.expired) ?? parseAccessTokenExp(accessToken);

const providerSpecificData: CodexImportPayload["providerSpecificData"] = {};
if (chatgptAccountId) providerSpecificData.chatgptAccountId = chatgptAccountId;
if (chatgptAccountId) {
providerSpecificData.chatgptAccountId = chatgptAccountId;
// CodexSwitcher stores this stable value as account_id; mirror it into
// workspaceId so createProviderConnection performs its existing upsert.
providerSpecificData.workspaceId = chatgptAccountId;
}
if (chatgptPlanType) providerSpecificData.chatgptPlanType = chatgptPlanType;

const payload: CodexImportPayload = {
Expand All @@ -235,8 +251,20 @@ export function normalizeCodexImportRecord(input: unknown): NormalizeResult {
email,
expiresAt,
testStatus: "active",
// Fresh imported OAuth credentials supersede a previous refresh failure.
isActive: true,
errorCode: null,
lastError: null,
lastErrorAt: null,
lastErrorType: null,
lastErrorSource: null,
backoffLevel: 0,
rateLimitedUntil: null,
};
if (idToken) payload.idToken = idToken;
if (typeof rec.priority === "number" && Number.isInteger(rec.priority) && rec.priority > 0) {
payload.priority = rec.priority;
}
if (Object.keys(providerSpecificData).length > 0) {
payload.providerSpecificData = providerSpecificData;
}
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/codexBulkImport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,19 @@ describe("normalizeCodexImportRecord", () => {
assert.equal(payload.refreshToken, "refresh-xyz");
assert.equal(payload.idToken, FULL_RECORD.id_token);
assert.equal(payload.testStatus, "active");
assert.equal(payload.isActive, true);
assert.equal(payload.errorCode, null);
assert.equal(payload.lastError, null);
assert.equal(payload.lastErrorAt, null);
assert.equal(payload.lastErrorType, null);
assert.equal(payload.lastErrorSource, null);
assert.equal(payload.backoffLevel, 0);
assert.equal(payload.rateLimitedUntil, null);
// JWT email wins over the record-level email.
assert.equal(payload.email, "test-jwt@example.com");
assert.deepEqual(payload.providerSpecificData, {
chatgptAccountId: "acct-from-jwt",
workspaceId: "acct-from-jwt",
chatgptPlanType: "plus",
});
// ISO-formatted, parses back to the right instant.
Expand All @@ -87,6 +96,7 @@ describe("normalizeCodexImportRecord", () => {
if (!result.ok) return;
assert.equal(result.payload.email, "fallback@example.com");
assert.equal(result.payload.providerSpecificData?.chatgptAccountId, "x");
assert.equal(result.payload.providerSpecificData?.workspaceId, "x");
});

test("falls back to account_id when id_token is missing", () => {
Expand All @@ -99,6 +109,7 @@ describe("normalizeCodexImportRecord", () => {
assert.equal(result.ok, true);
if (!result.ok) return;
assert.equal(result.payload.providerSpecificData?.chatgptAccountId, "acct-top-level");
assert.equal(result.payload.providerSpecificData?.workspaceId, "acct-top-level");
assert.equal(result.payload.idToken, undefined);
});

Expand Down Expand Up @@ -205,6 +216,7 @@ describe("normalizeCodexImportRecord", () => {
assert.equal(result.payload.email, "cli@example.com");
assert.deepEqual(result.payload.providerSpecificData, {
chatgptAccountId: "acct-cli",
workspaceId: "acct-cli",
chatgptPlanType: "free",
});
// Falls back to the access_token's `exp` claim when no `expired` field exists.
Expand Down Expand Up @@ -283,6 +295,7 @@ describe("9router camelCase Codex export (#6665)", () => {
assert.equal(payload.email, "jwt-9r@example.com");
assert.deepEqual(payload.providerSpecificData, {
chatgptAccountId: "acct-jwt-9r",
workspaceId: "acct-jwt-9r",
chatgptPlanType: "plus",
});
// camelCase `expiresAt` is honored as the expiry source.
Expand All @@ -301,6 +314,7 @@ describe("9router camelCase Codex export (#6665)", () => {
assert.equal(result.payload.email, "no-jwt-9r@example.com");
assert.deepEqual(result.payload.providerSpecificData, {
chatgptAccountId: "acct-psd",
workspaceId: "acct-psd",
chatgptPlanType: "team",
});
});
Expand Down
Loading