defmodule Codex.AppServer.Account do @moduledoc false alias Codex.AppServer.Connection alias Codex.AppServer.Params alias Codex.Auth alias Codex.Config.LayerStack alias Codex.Runtime.Env, as: RuntimeEnv @type connection :: pid() @spec login_start(connection(), :chatgpt | {:api_key, String.t()} | map()) :: {:ok, map()} | {:error, term()} def login_start(conn, :chatgpt) when is_pid(conn) do login_start(conn, :chatgpt, []) end def login_start(conn, "chatgpt") when is_pid(conn) do login_start(conn, :chatgpt, []) end def login_start(conn, {:api_key, api_key}) when is_pid(conn) and is_binary(api_key) do login_start(conn, {:api_key, api_key}, []) end def login_start(conn, %{} = params) when is_pid(conn) do login_start(conn, params, []) end @spec login_start(connection(), :chatgpt | {:api_key, String.t()} | map(), keyword()) :: {:ok, map()} | {:error, term()} def login_start(conn, :chatgpt, opts) when is_pid(conn) and is_list(opts) do params = %{"type" => "chatgpt"} with :ok <- enforce_login_constraints(:chatgpt, params, opts) do Connection.request(conn, "account/login/start", params, timeout_ms: 30_000) end end def login_start(conn, "chatgpt", opts) when is_pid(conn) and is_list(opts) do login_start(conn, :chatgpt, opts) end def login_start(conn, {:api_key, api_key}, opts) when is_pid(conn) and is_binary(api_key) and is_list(opts) do params = %{"type" => "apiKey", "apiKey" => api_key} with :ok <- enforce_login_constraints(:api_key, params, opts) do Connection.request(conn, "account/login/start", params, timeout_ms: 30_000) end end def login_start(conn, %{} = params, opts) when is_pid(conn) and is_list(opts) do normalized = Params.normalize_map(params) with {:ok, login_type} <- infer_login_type(normalized), :ok <- enforce_login_constraints(login_type, normalized, opts) do Connection.request(conn, "account/login/start", normalized, timeout_ms: 30_000) end end @spec login_cancel(connection(), String.t()) :: {:ok, map()} | {:error, term()} def login_cancel(conn, login_id) when is_pid(conn) and is_binary(login_id) do Connection.request(conn, "account/login/cancel", %{"loginId" => login_id}, timeout_ms: 30_000) end @spec logout(connection()) :: {:ok, map()} | {:error, term()} def logout(conn) when is_pid(conn) do Connection.request(conn, "account/logout", nil, timeout_ms: 30_000) end @spec read(connection(), keyword()) :: {:ok, map()} | {:error, term()} def read(conn, opts \\ []) when is_pid(conn) and is_list(opts) do params = %{"refreshToken" => !!Keyword.get(opts, :refresh_token, false)} Connection.request(conn, "account/read", params, timeout_ms: 30_000) end @spec rate_limits(connection()) :: {:ok, map()} | {:error, term()} def rate_limits(conn) when is_pid(conn) do Connection.request(conn, "account/rateLimits/read", nil, timeout_ms: 30_000) end defp enforce_login_constraints(login_type, params, opts) do %{forced_login_method: forced_method, forced_chatgpt_workspace_id: workspace_id} = load_forced_login_config(opts) with :ok <- enforce_forced_login_method(forced_method, login_type) do enforce_forced_workspace_id(workspace_id, login_type, params) end end defp enforce_forced_login_method(nil, _login_type), do: :ok defp enforce_forced_login_method("chatgpt", :chatgpt), do: :ok defp enforce_forced_login_method("api", :api_key), do: :ok defp enforce_forced_login_method(forced, login_type) do {:error, {:forced_login_method, forced, login_type}} end defp enforce_forced_workspace_id(nil, _login_type, _params), do: :ok defp enforce_forced_workspace_id(_workspace_id, :api_key, _params), do: :ok defp enforce_forced_workspace_id(workspace_id, :chatgpt, params) do case fetch_workspace_id(params) do nil -> :ok ^workspace_id -> :ok other -> {:error, {:forced_chatgpt_workspace_id, workspace_id, other}} end end defp infer_login_type(%{} = params) do case Map.get(params, "type") || Map.get(params, :type) do "chatgpt" -> {:ok, :chatgpt} "chatgptAuthTokens" -> {:ok, :chatgpt} "chatgpt_auth_tokens" -> {:ok, :chatgpt} "apiKey" -> {:ok, :api_key} "api_key" -> {:ok, :api_key} "api" -> {:ok, :api_key} other -> {:error, {:invalid_login_type, other}} end end defp load_forced_login_config(opts) do with {:ok, child_process_env} <- resolve_child_env(opts), {:ok, cwd} <- resolve_cwd(opts), codex_home <- resolve_codex_home(opts, child_process_env), {:ok, layers} <- LayerStack.load(codex_home, cwd) do config = LayerStack.effective_config(layers) %{ forced_login_method: Map.get(config, "forced_login_method") || Map.get(config, :forced_login_method), forced_chatgpt_workspace_id: Map.get(config, "forced_chatgpt_workspace_id") || Map.get(config, :forced_chatgpt_workspace_id) } else {:error, _reason} -> %{forced_login_method: nil, forced_chatgpt_workspace_id: nil} end end defp resolve_child_env(opts) do process_env = Keyword.get(opts, :process_env, Keyword.get(opts, :env)) with {:ok, overrides} <- RuntimeEnv.normalize_overrides(process_env) do {:ok, Map.merge(System.get_env(), overrides)} end end defp resolve_cwd(opts) do case Keyword.get(opts, :cwd) do nil -> case File.cwd() do {:ok, value} -> {:ok, value} _ -> {:ok, nil} end cwd when is_binary(cwd) -> if String.trim(cwd) == "", do: {:error, {:invalid_cwd, cwd}}, else: {:ok, cwd} other -> {:error, {:invalid_cwd, other}} end end defp resolve_codex_home(opts, child_process_env) do case Keyword.get(opts, :codex_home) || Map.get(child_process_env, "CODEX_HOME") do value when is_binary(value) and value != "" -> value _ -> Auth.codex_home() end end defp fetch_workspace_id(params) do Map.get(params, "workspaceId") || Map.get(params, "chatgptAccountId") || Map.get(params, "workspace_id") || Map.get(params, "chatgpt_account_id") || Map.get(params, :workspaceId) || Map.get(params, :chatgptAccountId) || Map.get(params, :workspace_id) || Map.get(params, :chatgpt_account_id) end end