From e715ae912fa471785e5042a759f7ee67c09bbe46 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 14 Jul 2025 10:53:26 -0700 Subject: [PATCH] custom `callback_host` for arcade login (#481) Say you are @shubcodes and running arcade on replit or vs codespaces. Today, `arcade login` assumes that the browser you've opened for the auth flow's "localhost" is the same that is running the `arcade login` command. If you are running on one of these remote code execution environments, that won't be true. Usage: ``` arcade login --callback-host "https://replit.com:9999/path/to/my/codespace" ``` --- libs/arcade-cli/arcade_cli/main.py | 7 ++++++- libs/arcade-cli/arcade_cli/utils.py | 15 +++++++++++++-- libs/tests/cli/test_utils.py | 15 ++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/libs/arcade-cli/arcade_cli/main.py b/libs/arcade-cli/arcade_cli/main.py index f7ab9b56..b1ff3712 100644 --- a/libs/arcade-cli/arcade_cli/main.py +++ b/libs/arcade-cli/arcade_cli/main.py @@ -99,6 +99,11 @@ def login( "--port", help="The port of the Arcade Cloud host (if running locally).", ), + callback_host: str = typer.Option( + "localhost", + "--callback-host", + help="The host to use to complete the auth flow - this should be the same as the host that the CLI is running on. Include the port if needed.", + ), debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"), ) -> None: """ @@ -119,7 +124,7 @@ def login( try: # Open the browser for user login - login_url = compute_login_url(host, state, port) + login_url = compute_login_url(host, state, port, callback_host) console.print("Opening a browser to log you in...") if not webbrowser.open(login_url): diff --git a/libs/arcade-cli/arcade_cli/utils.py b/libs/arcade-cli/arcade_cli/utils.py index 99f3a254..707859a0 100644 --- a/libs/arcade-cli/arcade_cli/utils.py +++ b/libs/arcade-cli/arcade_cli/utils.py @@ -176,11 +176,22 @@ def compute_base_url( return f"{protocol}://{encoded_host}" -def compute_login_url(host: str, state: str, port: int | None) -> str: +def compute_login_url( + host: str, state: str, port: int | None, callback_host: str | None = None +) -> str: """ Compute the full URL for the CLI login endpoint. """ - callback_uri = f"http://{LOCALHOST}:9905/callback" + if callback_host: + if not (callback_host.startswith("http://") or callback_host.startswith("https://")): + callback_uri = f"http://{callback_host}" + else: + callback_uri = callback_host + if not callback_uri.rstrip("/").endswith("/callback"): + callback_uri = callback_uri.rstrip("/") + "/callback" + else: + callback_uri = f"http://{LOCALHOST}:9905/callback" + params = urlencode({"callback_uri": callback_uri, "state": state}) port = port if port else 8000 diff --git a/libs/tests/cli/test_utils.py b/libs/tests/cli/test_utils.py index c4b40c62..b3bef020 100644 --- a/libs/tests/cli/test_utils.py +++ b/libs/tests/cli/test_utils.py @@ -218,9 +218,22 @@ def test_compute_base_url(inputs: dict, expected_output: str): "https://cloud.arcade.dev/api/v1/auth/cli_login?callback_uri=http%3A%2F%2Flocalhost%3A9905%2Fcallback&state=123", id="cloud host with an ignored custom port", ), + pytest.param( + { + "host_input": DEFAULT_CLOUD_HOST, + "port_input": DEFAULT_PORT, + "state": "123", + "callback_host": "other-host.com/123", + }, + "https://cloud.arcade.dev/api/v1/auth/cli_login?callback_uri=http%3A%2F%2Fother-host.com%2F123%2Fcallback&state=123", + id="cloud host with a custom callback host", + ), ], ) def test_compute_login_url(inputs: dict, expected_output: str): - login_url = compute_login_url(inputs["host_input"], inputs["state"], inputs["port_input"]) + callback_host = inputs.get("callback_host") + login_url = compute_login_url( + inputs["host_input"], inputs["state"], inputs["port_input"], callback_host + ) assert login_url == expected_output