mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix: (cloud) CLI cloud commands now use API key when configured (#698)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,14 +45,26 @@ def get_cloud_config() -> tuple[str, str, str]:
|
||||
|
||||
async def get_authenticated_headers(auth: CLIAuth | None = None) -> dict[str, str]:
|
||||
"""
|
||||
Get authentication headers with JWT token.
|
||||
handles jwt refresh if needed.
|
||||
Get authentication headers for cloud API requests.
|
||||
|
||||
Credential priority mirrors async_client._resolve_cloud_token():
|
||||
1. API key (config.cloud_api_key) — fast, no refresh needed
|
||||
2. OAuth token via CLIAuth — handles JWT refresh automatically
|
||||
"""
|
||||
# --- API key (preferred) ---
|
||||
config_manager = ConfigManager()
|
||||
api_key = config_manager.config.cloud_api_key
|
||||
if api_key:
|
||||
return {"Authorization": f"Bearer {api_key}"}
|
||||
|
||||
# --- OAuth fallback ---
|
||||
client_id, domain, _ = get_cloud_config()
|
||||
auth_obj = auth or CLIAuth(client_id=client_id, authkit_domain=domain)
|
||||
token = await auth_obj.get_valid_token()
|
||||
if not token:
|
||||
console.print("[red]Not authenticated. Please run 'bm cloud login' first.[/red]")
|
||||
console.print(
|
||||
"[red]Not authenticated. Run 'bm cloud set-key <key>' or 'bm cloud login' first.[/red]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
@@ -163,3 +163,71 @@ async def test_cloud_utils_fetch_and_exists_and_create_project(
|
||||
assert created.new_project["name"] == "My Project"
|
||||
# Path should be permalink-like (kebab)
|
||||
assert seen["create_payload"]["path"] == "my-project"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_api_request_prefers_api_key_over_oauth(config_home, config_manager):
|
||||
"""API key in config should be used without needing an OAuth token on disk."""
|
||||
# Arrange: set an API key in config, no OAuth token on disk
|
||||
config = config_manager.load_config()
|
||||
config.cloud_api_key = "bmc_test_key_12345"
|
||||
config_manager.save_config(config)
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
# Verify the API key is sent as the Bearer token
|
||||
assert request.headers.get("authorization") == "Bearer bmc_test_key_12345"
|
||||
return httpx.Response(200, json={"ok": True})
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
|
||||
@asynccontextmanager
|
||||
async def http_client_factory():
|
||||
async with httpx.AsyncClient(transport=transport) as client:
|
||||
yield client
|
||||
|
||||
# Act — no auth= parameter, no OAuth token file; should use API key from config
|
||||
resp = await make_api_request(
|
||||
method="GET",
|
||||
url="https://cloud.example.test/proxy/health",
|
||||
http_client_factory=http_client_factory,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert resp.json()["ok"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_api_request_falls_back_to_oauth_when_no_api_key(config_home, config_manager):
|
||||
"""When no API key is configured, should fall back to OAuth token."""
|
||||
# Arrange: no API key, but OAuth token on disk
|
||||
config = config_manager.load_config()
|
||||
config.cloud_api_key = None
|
||||
config_manager.save_config(config)
|
||||
|
||||
auth = CLIAuth(client_id="cid", authkit_domain="https://auth.example.test")
|
||||
auth.token_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
auth.token_file.write_text(
|
||||
'{"access_token":"oauth-token-456","refresh_token":null,'
|
||||
'"expires_at":9999999999,"token_type":"Bearer"}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.headers.get("authorization") == "Bearer oauth-token-456"
|
||||
return httpx.Response(200, json={"ok": True})
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
|
||||
@asynccontextmanager
|
||||
async def http_client_factory():
|
||||
async with httpx.AsyncClient(transport=transport) as client:
|
||||
yield client
|
||||
|
||||
resp = await make_api_request(
|
||||
method="GET",
|
||||
url="https://cloud.example.test/proxy/health",
|
||||
auth=auth,
|
||||
http_client_factory=http_client_factory,
|
||||
)
|
||||
|
||||
assert resp.json()["ok"] is True
|
||||
|
||||
Reference in New Issue
Block a user