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
17 changes: 17 additions & 0 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,23 @@ pub fn runCodexLogin(opts: LoginOptions) !void {
try ensureCodexLoginSucceeded(term);
}

pub fn runCodexLoginWithCodexHome(allocator: std.mem.Allocator, opts: LoginOptions, codex_home: []const u8) !void {
var env_map = try std.process.getEnvMap(allocator);
defer env_map.deinit();
try env_map.put("CODEX_HOME", codex_home);

var child = std.process.Child.init(codexLoginArgs(opts), allocator);
child.env_map = &env_map;
child.stdin_behavior = .Inherit;
child.stdout_behavior = .Inherit;
child.stderr_behavior = .Inherit;
const term = child.spawnAndWait() catch |err| {
writeCodexLoginLaunchFailureHint(@errorName(err), stderrColorEnabled()) catch {};
return err;
};
try ensureCodexLoginSucceeded(term);
}

pub fn selectAccount(allocator: std.mem.Allocator, reg: *registry.Registry) !?[]const u8 {
return selectAccountWithUsageOverrides(allocator, reg, null);
}
Expand Down
68 changes: 66 additions & 2 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const account_api = @import("account_api.zig");
const account_name_refresh = @import("account_name_refresh.zig");
const cli = @import("cli.zig");
Expand Down Expand Up @@ -1530,8 +1531,16 @@ fn handleList(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.Li
}

fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.LoginOptions) !void {
try cli.runCodexLogin(opts);
const auth_path = try registry.activeAuthPath(allocator, codex_home);
const login_home = try createTempLoginCodexHome(allocator);
defer {
std.fs.cwd().deleteTree(login_home) catch |err| {
std.log.warn("failed to remove temporary Codex login home `{s}`: {s}", .{ login_home, @errorName(err) });
};
allocator.free(login_home);
}

try cli.runCodexLoginWithCodexHome(allocator, opts, login_home);
const auth_path = try registry.activeAuthPath(allocator, login_home);
defer allocator.free(auth_path);

const info = try auth.parseAuthInfo(allocator, auth_path);
Expand All @@ -1540,6 +1549,8 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L
var reg = try registry.loadRegistry(allocator, codex_home);
defer reg.deinit(allocator);

_ = try registry.syncActiveAccountFromAuth(allocator, codex_home, &reg);

const email = info.email orelse return error.MissingEmail;
_ = email;
const record_key = info.record_key orelse return error.MissingChatgptUserId;
Expand All @@ -1548,6 +1559,9 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L

try registry.ensureAccountsDir(allocator, codex_home);
try registry.copyFile(auth_path, dest);
const active_auth_path = try registry.activeAuthPath(allocator, codex_home);
defer allocator.free(active_auth_path);
try registry.copyFile(auth_path, active_auth_path);

const record = try registry.accountFromAuth(allocator, "", &info);
try registry.upsertAccount(allocator, &reg, record);
Expand All @@ -1556,6 +1570,56 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L
try registry.saveRegistry(allocator, codex_home, &reg);
}

fn createTempLoginCodexHome(allocator: std.mem.Allocator) ![]u8 {
const base = try tempBasePathAlloc(allocator);
defer allocator.free(base);
var counter: usize = 0;
while (counter < 100) : (counter += 1) {
const path = try std.fmt.allocPrint(
allocator,
"{s}{c}codex-auth-login-{d}-{d}",
.{ base, std.fs.path.sep, std.time.nanoTimestamp(), counter },
);
std.fs.cwd().makePath(path) catch |err| switch (err) {
error.PathAlreadyExists => {
allocator.free(path);
continue;
},
else => {
allocator.free(path);
return err;
},
};
return path;
}
return error.PathAlreadyExists;
}

fn tempBasePathAlloc(allocator: std.mem.Allocator) ![]u8 {
if (builtin.os.tag == .windows) {
if (try getNonEmptyEnvVarOwned(allocator, "TEMP")) |path| return path;
if (try getNonEmptyEnvVarOwned(allocator, "TMP")) |path| return path;
if (try getNonEmptyEnvVarOwned(allocator, "TMPDIR")) |path| return path;
return allocator.dupe(u8, "C:\\Temp");
}
if (try getNonEmptyEnvVarOwned(allocator, "TMPDIR")) |path| return path;
if (try getNonEmptyEnvVarOwned(allocator, "TMP")) |path| return path;
if (try getNonEmptyEnvVarOwned(allocator, "TEMP")) |path| return path;
return allocator.dupe(u8, "/tmp");
}

fn getNonEmptyEnvVarOwned(allocator: std.mem.Allocator, name: []const u8) !?[]u8 {
const value = std.process.getEnvVarOwned(allocator, name) catch |err| switch (err) {
error.EnvironmentVariableNotFound => return null,
else => return err,
};
if (value.len == 0) {
allocator.free(value);
return null;
}
return value;
}

fn handleImport(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.ImportOptions) !void {
if (opts.purge) {
var report = try registry.purgeRegistryFromImportSource(allocator, codex_home, opts.auth_path, opts.alias);
Expand Down
73 changes: 73 additions & 0 deletions src/tests/e2e_cli_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,79 @@ test "Scenario: Given CODEX_HOME override when running login then it stores auth
try std.testing.expect(std.mem.eql(u8, loaded.accounts.items[0].email, expected_email));
}

test "Scenario: Given existing active auth when login succeeds then upstream login uses a temporary Codex home" {
const gpa = std.testing.allocator;
const project_root = try projectRootAlloc(gpa);
defer gpa.free(project_root);
try buildCliBinary(gpa, project_root);

var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();

const home_root = try tmp.dir.realpathAlloc(gpa, ".");
defer gpa.free(home_root);
try tmp.dir.makePath(".codex");
try tmp.dir.makePath("fake-bin");

const existing_auth = try bdd.authJsonWithEmailPlan(gpa, "existing@example.com", "plus");
defer gpa.free(existing_auth);
try tmp.dir.writeFile(.{ .sub_path = ".codex/auth.json", .data = existing_auth });

const expected_email = "new-login@example.com";
const fake_auth = try bdd.authJsonWithEmailPlan(gpa, expected_email, "pro");
defer gpa.free(fake_auth);
try tmp.dir.writeFile(.{ .sub_path = "fake-auth.json", .data = fake_auth });
try writeSuccessfulFakeCodex(tmp.dir);

const fake_bin_path = try std.fs.path.join(gpa, &[_][]const u8{ home_root, "fake-bin" });
defer gpa.free(fake_bin_path);
const path_override = try prependPathEntryAlloc(gpa, fake_bin_path);
defer gpa.free(path_override);

const result = try runCliWithIsolatedHomeAndPath(
gpa,
project_root,
home_root,
path_override,
&[_][]const u8{ "login", "--device-auth" },
);
defer gpa.free(result.stdout);
defer gpa.free(result.stderr);

try expectSuccess(result);

const active_auth_path = try authJsonPathAlloc(gpa, home_root);
defer gpa.free(active_auth_path);
const active_auth = try bdd.readFileAlloc(gpa, active_auth_path);
defer gpa.free(active_auth);
try std.testing.expectEqualStrings(fake_auth, active_auth);

const codex_home = try codexHomeAlloc(gpa, home_root);
defer gpa.free(codex_home);
var loaded = try registry.loadRegistry(gpa, codex_home);
defer loaded.deinit(gpa);
try std.testing.expectEqual(@as(usize, 2), loaded.accounts.items.len);
try std.testing.expect(loaded.active_account_key != null);

const expected_account_key = try bdd.accountKeyForEmailAlloc(gpa, expected_email);
defer gpa.free(expected_account_key);
try std.testing.expectEqualStrings(expected_account_key, loaded.active_account_key.?);

const snapshot_path = try registry.accountAuthPath(gpa, codex_home, expected_account_key);
defer gpa.free(snapshot_path);
const snapshot_data = try bdd.readFileAlloc(gpa, snapshot_path);
defer gpa.free(snapshot_data);
try std.testing.expectEqualStrings(fake_auth, snapshot_data);

const existing_account_key = try bdd.accountKeyForEmailAlloc(gpa, "existing@example.com");
defer gpa.free(existing_account_key);
const existing_snapshot_path = try registry.accountAuthPath(gpa, codex_home, existing_account_key);
defer gpa.free(existing_snapshot_path);
const existing_snapshot_data = try bdd.readFileAlloc(gpa, existing_snapshot_path);
defer gpa.free(existing_snapshot_data);
try std.testing.expectEqualStrings(existing_auth, existing_snapshot_data);
}

test "Scenario: Given failed device auth login with existing auth json when running login then it forwards the flag and does not mutate the registry" {
const gpa = std.testing.allocator;
const project_root = try projectRootAlloc(gpa);
Expand Down
Loading