Current section
Files
Jump to
Current section
Files
lib/codex/oauth/app_server_refresh_responder.ex
defmodule Codex.OAuth.AppServerRefreshResponder do
@moduledoc false
use GenServer
alias Codex.AppServer.Connection
alias Codex.OAuth
alias Codex.OAuth.Session
alias Codex.OAuth.TokenStore.Memory, as: MemoryTokenStore
@refresh_method "account/chatgptAuthTokens/refresh"
@json_rpc_internal_error -32_000
defmodule State do
@moduledoc false
defstruct [:conn, :conn_ref, :session]
end
@spec start(pid(), Session.t(), keyword()) :: GenServer.on_start()
def start(conn, %Session{} = session, opts \\ [])
when is_pid(conn) and is_list(opts) do
GenServer.start(__MODULE__, {conn, session})
end
@impl true
def init({conn, %Session{} = session}) do
Process.flag(:trap_exit, true)
case Connection.subscribe(conn, methods: [@refresh_method]) do
:ok ->
{:ok,
%State{
conn: conn,
conn_ref: Process.monitor(conn),
session: session
}}
{:error, _} = error ->
{:stop, error}
end
end
@impl true
def handle_info({:codex_request, id, @refresh_method, params}, %State{} = state) do
case refresh_response(state.session, params || %{}) do
{:ok, refreshed_session, result} ->
case Connection.respond(state.conn, id, result) do
:ok ->
{:noreply, cache_session(state, refreshed_session)}
{:error, reason} ->
{:stop, reason, state}
end
{:error, reason} ->
message = error_message(reason)
_ = Connection.respond_error(state.conn, id, @json_rpc_internal_error, message)
{:noreply, state}
end
end
def handle_info({:DOWN, ref, :process, _pid, _reason}, %State{conn_ref: ref} = state) do
{:stop, :normal, state}
end
def handle_info(_message, %State{} = state) do
{:noreply, state}
end
@impl true
def terminate(_reason, %State{conn: conn}) do
_ = Connection.unsubscribe(conn)
:ok
end
defp refresh_response(%Session{} = session, params) do
previous_account_id =
Map.get(params, "previousAccountId") || Map.get(params, "previous_account_id")
with {:ok, refreshed_session} <- OAuth.refresh_session(session, persist?: false),
:ok <- validate_previous_account_id(refreshed_session, previous_account_id),
{:ok, result} <- result_payload(refreshed_session) do
{:ok, refreshed_session, result}
end
end
defp validate_previous_account_id(_session, nil), do: :ok
defp validate_previous_account_id(%Session{} = session, previous_account_id)
when is_binary(previous_account_id) and previous_account_id != "" do
tokens = session.auth_record.tokens
refreshed_account_id = tokens && (tokens.chatgpt_account_id || tokens.account_id)
if refreshed_account_id == previous_account_id do
:ok
else
{:error, :previous_account_id_mismatch}
end
end
defp validate_previous_account_id(_session, _previous_account_id), do: :ok
defp result_payload(%Session{} = session) do
tokens = session.auth_record.tokens
access_token = tokens && tokens.access_token
account_id = tokens && (tokens.chatgpt_account_id || tokens.account_id)
cond do
not (is_binary(access_token) and access_token != "") ->
{:error, :missing_access_token}
not (is_binary(account_id) and account_id != "") ->
{:error, :missing_chatgpt_account_id}
true ->
{:ok,
%{}
|> Map.put("accessToken", access_token)
|> Map.put("chatgptAccountId", account_id)
|> maybe_put("chatgptPlanType", tokens.plan_type)}
end
end
defp cache_session(%State{} = state, %Session{} = refreshed_session) do
if is_pid(refreshed_session.token_store) do
:ok = MemoryTokenStore.put(refreshed_session.token_store, refreshed_session)
end
%State{state | session: refreshed_session}
end
defp error_message(:previous_account_id_mismatch) do
"refreshed ChatGPT account did not match the previous account"
end
defp error_message(:missing_access_token), do: "refreshed ChatGPT access token was missing"
defp error_message(:missing_chatgpt_account_id), do: "refreshed ChatGPT account id was missing"
defp error_message(:missing_refresh_token), do: "ChatGPT refresh token is unavailable"
defp error_message(:missing_tokens), do: "ChatGPT auth tokens are unavailable"
defp error_message({:refresh_failed, _status, _body}),
do: "failed to refresh ChatGPT auth tokens"
defp error_message(_reason), do: "failed to refresh ChatGPT auth tokens"
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end