From 0081990d40b25d3a51d723a646245d5a86444ada Mon Sep 17 00:00:00 2001 From: Steven Coaila Date: Wed, 27 May 2026 23:35:10 -0500 Subject: [PATCH] fix(cli): normalize legacy production api url --- src/commands/auth/logout.ts | 2 +- src/commands/auth/status.ts | 4 ++-- src/lib/api-client.ts | 2 +- src/lib/config.ts | 15 +++++++++++++-- tests/lib/config.test.ts | 27 +++++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/commands/auth/logout.ts b/src/commands/auth/logout.ts index e63e20e..62bf728 100644 --- a/src/commands/auth/logout.ts +++ b/src/commands/auth/logout.ts @@ -11,7 +11,7 @@ export async function runLogout(): Promise { let revoked = false; if (creds?.token) { - const apiUrl = creds.apiUrl ?? getApiUrl(); + const apiUrl = getApiUrl(creds); const response = await fetch(`${apiUrl}/api/cli/logout`, { method: "POST", headers: { diff --git a/src/commands/auth/status.ts b/src/commands/auth/status.ts index 439a6c2..6aebf39 100644 --- a/src/commands/auth/status.ts +++ b/src/commands/auth/status.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { loadCredentials } from "../../lib/config.js"; +import { getApiUrl, loadCredentials } from "../../lib/config.js"; import { output } from "../../lib/output.js"; export const statusCommand = new Command("status") @@ -11,7 +11,7 @@ export const statusCommand = new Command("status") } output.success({ authenticated: true, - apiUrl: creds!.apiUrl, + apiUrl: getApiUrl(creds), deviceName: creds!.deviceName, expiresAt: creds!.expiresAt, }); diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 7a26b33..35dc738 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -87,7 +87,7 @@ export async function apiRequest( output.error("Token expired. Run: lucas auth login"); } - const apiUrl = creds!.apiUrl ?? getApiUrl(); + const apiUrl = getApiUrl(creds); const url = new URL(path, apiUrl); if (queryParams) { diff --git a/src/lib/config.ts b/src/lib/config.ts index 584f928..ef10166 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -20,9 +20,20 @@ export interface Credentials { export const CONFIG_DIR = join(homedir(), ".config", "lucas"); const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json"); const DEFAULT_API_URL = "https://api.lucasapp.app"; +const LEGACY_PRODUCTION_API_URLS = new Set(["https://lucas.stevenacz.com"]); -export function getApiUrl(): string { - return process.env.LUCAS_API_URL ?? DEFAULT_API_URL; +export function normalizeApiUrl(apiUrl: string): string { + const normalized = apiUrl.trim().replace(/\/+$/, ""); + if (LEGACY_PRODUCTION_API_URLS.has(normalized)) { + return DEFAULT_API_URL; + } + return normalized; +} + +export function getApiUrl(creds?: Pick | null): string { + return normalizeApiUrl( + process.env.LUCAS_API_URL ?? creds?.apiUrl ?? DEFAULT_API_URL, + ); } export function ensureConfigDir(): void { diff --git a/tests/lib/config.test.ts b/tests/lib/config.test.ts index c099ff4..977fb3d 100644 --- a/tests/lib/config.test.ts +++ b/tests/lib/config.test.ts @@ -5,8 +5,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; describe("config credential storage", () => { let tempHome: string | undefined; + const originalLucasApiUrl = process.env.LUCAS_API_URL; beforeEach(async () => { + delete process.env.LUCAS_API_URL; tempHome = await mkdtemp(join(tmpdir(), "lucas-cli-home-")); vi.resetModules(); vi.doMock("os", () => ({ @@ -17,6 +19,11 @@ describe("config credential storage", () => { afterEach(async () => { vi.doUnmock("os"); vi.resetModules(); + if (originalLucasApiUrl === undefined) { + delete process.env.LUCAS_API_URL; + } else { + process.env.LUCAS_API_URL = originalLucasApiUrl; + } if (tempHome) { await rm(tempHome, { recursive: true, force: true }); tempHome = undefined; @@ -41,4 +48,24 @@ describe("config credential storage", () => { expect(dirMode).toBe(0o700); expect(fileMode).toBe(0o600); }); + + it("normalizes legacy production API URLs from stored credentials", async () => { + const { getApiUrl } = await import("../../src/lib/config.js"); + + expect(getApiUrl({ apiUrl: "https://lucas.stevenacz.com" })).toBe( + "https://api.lucasapp.app", + ); + expect(getApiUrl({ apiUrl: "https://lucas.stevenacz.com/" })).toBe( + "https://api.lucasapp.app", + ); + }); + + it("lets LUCAS_API_URL override stored credentials", async () => { + process.env.LUCAS_API_URL = "http://localhost:3301"; + const { getApiUrl } = await import("../../src/lib/config.js"); + + expect(getApiUrl({ apiUrl: "https://example.test" })).toBe( + "http://localhost:3301", + ); + }); });