From 2422a4063a65d3a773aa8995548ec7b14186538a Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 10:58:12 -0700 Subject: [PATCH 01/14] feat: read MUX_BASE_URL from env-file and persist to config When logging in with --env-file, MUX_BASE_URL is now parsed and, if nonstandard, saved to the environment's config.json. getMuxBaseUrl() checks env var > config > default, so subsequent commands automatically use the configured base URL without needing the env var set. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.ts | 13 ++++++++++++- src/commands/webhooks/listen.ts | 2 +- src/commands/whoami.ts | 2 +- src/lib/config.ts | 1 + src/lib/errors.ts | 2 +- src/lib/mux.ts | 21 ++++++++++++++++----- 6 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/commands/login.ts b/src/commands/login.ts index 6bf6ed1..39146ae 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -2,13 +2,14 @@ import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { Command } from '@cliffy/command'; import { listEnvironments, setEnvironment } from '../lib/config.ts'; -import { validateCredentials } from '../lib/mux.ts'; +import { DEFAULT_BASE_URL, validateCredentials } from '../lib/mux.ts'; import { inputPrompt, secretPrompt } from '../lib/prompt.ts'; import { getConfigPath } from '../lib/xdg.ts'; export interface EnvVars { MUX_TOKEN_ID?: string; MUX_TOKEN_SECRET?: string; + MUX_BASE_URL?: string; } /** @@ -48,6 +49,8 @@ export async function parseEnvFile(filePath: string): Promise { envVars.MUX_TOKEN_ID = value; } else if (key === 'MUX_TOKEN_SECRET') { envVars.MUX_TOKEN_SECRET = value; + } else if (key === 'MUX_BASE_URL') { + envVars.MUX_BASE_URL = value; } } } @@ -80,6 +83,8 @@ export const loginCommand = new Command() ); } + let baseUrl: string | undefined; + if (options.envFile) { // Read from .env file console.log(`Reading credentials from ${options.envFile}...`); @@ -95,6 +100,10 @@ export const loginCommand = new Command() tokenId = envVars.MUX_TOKEN_ID; tokenSecret = envVars.MUX_TOKEN_SECRET; + + if (envVars.MUX_BASE_URL && envVars.MUX_BASE_URL !== DEFAULT_BASE_URL) { + baseUrl = envVars.MUX_BASE_URL; + } } else { // Interactive prompts console.log('Enter your Mux API credentials.'); @@ -118,6 +127,7 @@ export const loginCommand = new Command() const validation = await validateCredentials( tokenId.trim(), tokenSecret.trim(), + baseUrl, ); if (!validation.valid) { @@ -131,6 +141,7 @@ export const loginCommand = new Command() tokenId: tokenId.trim(), tokenSecret: tokenSecret.trim(), environmentId: validation.environmentId, + ...(baseUrl && { baseUrl }), }); console.log( diff --git a/src/commands/webhooks/listen.ts b/src/commands/webhooks/listen.ts index b1809f4..533cbe7 100644 --- a/src/commands/webhooks/listen.ts +++ b/src/commands/webhooks/listen.ts @@ -61,7 +61,7 @@ export const listenCommand = new Command() } const authHeaders = await getAuthHeaders(); - const baseUrl = getMuxBaseUrl(); + const baseUrl = await getMuxBaseUrl(); const url = `${baseUrl}/system/v1/webhook-events/stream`; let signingSecret: string | undefined; diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index 17e2036..5d9958d 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -12,7 +12,7 @@ export const whoamiCommand = new Command() .action(async (options: WhoAmIOptions) => { try { const headers = await getAuthHeaders(); - const baseUrl = getMuxBaseUrl(); + const baseUrl = await getMuxBaseUrl(); const response = await fetch(`${baseUrl}/system/v1/whoami`, { headers }); if (!response.ok) { diff --git a/src/lib/config.ts b/src/lib/config.ts index bc8659f..ca36caa 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -7,6 +7,7 @@ export interface Environment { tokenId: string; tokenSecret: string; environmentId?: string; + baseUrl?: string; signingKeyId?: string; signingPrivateKey?: string; forwardUrl?: string; diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 731f8a7..77d6909 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -48,7 +48,7 @@ async function fetchTokenInfo(): Promise<{ } | null> { try { const headers = await getAuthHeaders(); - const baseUrl = getMuxBaseUrl(); + const baseUrl = await getMuxBaseUrl(); const response = await fetch(`${baseUrl}/system/v1/whoami`, { headers }); if (!response.ok) return null; diff --git a/src/lib/mux.ts b/src/lib/mux.ts index a09949f..2c39eaa 100644 --- a/src/lib/mux.ts +++ b/src/lib/mux.ts @@ -3,7 +3,7 @@ import pkg from '../../package.json'; import { getCurrentEnvironment } from './config.ts'; import { isAgentMode } from './context.ts'; -const DEFAULT_BASE_URL = 'https://api.mux.com'; +export const DEFAULT_BASE_URL = 'https://api.mux.com'; function getUserAgent(): string { return isAgentMode() @@ -12,10 +12,20 @@ function getUserAgent(): string { } /** - * Get the Mux API base URL, respecting MUX_BASE_URL env var + * Get the Mux API base URL. + * Priority: MUX_BASE_URL env var > config baseUrl > default */ -export function getMuxBaseUrl(): string { - return process.env.MUX_BASE_URL || DEFAULT_BASE_URL; +export async function getMuxBaseUrl(): Promise { + if (process.env.MUX_BASE_URL) { + return process.env.MUX_BASE_URL; + } + + const env = await getCurrentEnvironment(); + if (env?.environment.baseUrl) { + return env.environment.baseUrl; + } + + return DEFAULT_BASE_URL; } /** @@ -60,9 +70,10 @@ export async function createAuthenticatedMuxClient(): Promise { export async function validateCredentials( tokenId: string, tokenSecret: string, + overrideBaseUrl?: string, ): Promise<{ valid: boolean; environmentId?: string; error?: string }> { try { - const baseUrl = getMuxBaseUrl(); + const baseUrl = overrideBaseUrl || (await getMuxBaseUrl()); const credentials = btoa(`${tokenId}:${tokenSecret}`); const response = await fetch(`${baseUrl}/system/v1/whoami`, { headers: { From 6f9250a1eacc7a4fb628d8b57862f89ecd39f70f Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 11:15:55 -0700 Subject: [PATCH 02/14] feat: show API endpoint in whoami when nonstandard Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/whoami.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index 5d9958d..30032e8 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -1,6 +1,6 @@ import { Command } from '@cliffy/command'; import { handleCommandError } from '@/lib/errors.ts'; -import { getAuthHeaders, getMuxBaseUrl } from '../lib/mux.ts'; +import { DEFAULT_BASE_URL, getAuthHeaders, getMuxBaseUrl } from '../lib/mux.ts'; interface WhoAmIOptions { json?: boolean; @@ -38,6 +38,9 @@ export const whoamiCommand = new Command() console.log( `Permissions: ${(data.permissions as string[]).join(', ')}`, ); + if (baseUrl !== DEFAULT_BASE_URL) { + console.log(`API endpoint: ${baseUrl}`); + } } catch (error) { await handleCommandError(error, 'whoami', 'get', options); } From e894c3c525926cef4b9a8deb3b24dbefc6a7bd7a Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 11:17:04 -0700 Subject: [PATCH 03/14] test: add tests for MUX_BASE_URL parsing and priority Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.test.ts | 16 +++++++++ src/lib/mux.test.ts | 68 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/lib/mux.test.ts diff --git a/src/commands/login.test.ts b/src/commands/login.test.ts index 96448d4..dd454e6 100644 --- a/src/commands/login.test.ts +++ b/src/commands/login.test.ts @@ -111,6 +111,22 @@ MUX_TOKEN_SECRET = test_secret_456`, expect(result.MUX_TOKEN_SECRET).toBe('test_secret_456'); }); + it('should parse MUX_BASE_URL', async () => { + const envPath = join(testDir, '.env'); + await Bun.write( + envPath, + `MUX_TOKEN_ID=test_id_123 +MUX_TOKEN_SECRET=test_secret_456 +MUX_BASE_URL=https://api.staging.mux.com`, + ); + + const result = await parseEnvFile(envPath); + + expect(result.MUX_TOKEN_ID).toBe('test_id_123'); + expect(result.MUX_TOKEN_SECRET).toBe('test_secret_456'); + expect(result.MUX_BASE_URL).toBe('https://api.staging.mux.com'); + }); + it('should ignore other environment variables', async () => { const envPath = join(testDir, '.env'); await Bun.write( diff --git a/src/lib/mux.test.ts b/src/lib/mux.test.ts new file mode 100644 index 0000000..3a16b8b --- /dev/null +++ b/src/lib/mux.test.ts @@ -0,0 +1,68 @@ +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { setEnvironment } from './config.ts'; +import { DEFAULT_BASE_URL, getMuxBaseUrl } from './mux.ts'; + +describe('getMuxBaseUrl', () => { + let testConfigDir: string; + let originalXdgConfigHome: string | undefined; + let originalMuxBaseUrl: string | undefined; + + beforeEach(async () => { + testConfigDir = await mkdtemp(join(tmpdir(), 'mux-cli-test-')); + originalXdgConfigHome = process.env.XDG_CONFIG_HOME; + originalMuxBaseUrl = process.env.MUX_BASE_URL; + process.env.XDG_CONFIG_HOME = testConfigDir; + delete process.env.MUX_BASE_URL; + }); + + afterEach(async () => { + if (originalXdgConfigHome === undefined) { + delete process.env.XDG_CONFIG_HOME; + } else { + process.env.XDG_CONFIG_HOME = originalXdgConfigHome; + } + if (originalMuxBaseUrl === undefined) { + delete process.env.MUX_BASE_URL; + } else { + process.env.MUX_BASE_URL = originalMuxBaseUrl; + } + await rm(testConfigDir, { recursive: true, force: true }); + }); + + it('should return default when no env var or config', async () => { + expect(await getMuxBaseUrl()).toBe(DEFAULT_BASE_URL); + }); + + it('should prefer MUX_BASE_URL env var over everything', async () => { + process.env.MUX_BASE_URL = 'https://env-var.example.com'; + await setEnvironment('default', { + tokenId: 'id', + tokenSecret: 'secret', + baseUrl: 'https://config.example.com', + }); + + expect(await getMuxBaseUrl()).toBe('https://env-var.example.com'); + }); + + it('should use config baseUrl when no env var is set', async () => { + await setEnvironment('default', { + tokenId: 'id', + tokenSecret: 'secret', + baseUrl: 'https://api.staging.mux.com', + }); + + expect(await getMuxBaseUrl()).toBe('https://api.staging.mux.com'); + }); + + it('should fall back to default when config has no baseUrl', async () => { + await setEnvironment('default', { + tokenId: 'id', + tokenSecret: 'secret', + }); + + expect(await getMuxBaseUrl()).toBe(DEFAULT_BASE_URL); + }); +}); From 6a21c55a98598450c497f53a85a86ae3969ef85c Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 13:06:39 -0700 Subject: [PATCH 04/14] fix: pass baseUrl to Mux SDK client createAuthenticatedMuxClient() was not forwarding the configured base URL to the SDK, so SDK-based commands always hit the default api.mux.com regardless of config. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/mux.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/mux.ts b/src/lib/mux.ts index 2c39eaa..dbc430c 100644 --- a/src/lib/mux.ts +++ b/src/lib/mux.ts @@ -56,9 +56,12 @@ export async function createAuthenticatedMuxClient(): Promise { throw new Error("Not logged in. Please run 'mux login' to authenticate."); } + const baseURL = await getMuxBaseUrl(); + return new Mux({ tokenId: env.environment.tokenId, tokenSecret: env.environment.tokenSecret, + ...(baseURL !== DEFAULT_BASE_URL && { baseURL }), defaultHeaders: { 'User-Agent': getUserAgent() }, }); } From f274010dd2c9bf0cae19925866355e2cdc62372d Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 15:21:13 -0700 Subject: [PATCH 05/14] fix: always validate login credentials against explicit base URL Prevents stale config baseUrl from being used when logging in without MUX_BASE_URL set. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/login.ts b/src/commands/login.ts index 39146ae..6d3e393 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -127,7 +127,7 @@ export const loginCommand = new Command() const validation = await validateCredentials( tokenId.trim(), tokenSecret.trim(), - baseUrl, + baseUrl || DEFAULT_BASE_URL, ); if (!validation.valid) { From 404770fdaa59c0921d5339146128613618118cf1 Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 15:28:09 -0700 Subject: [PATCH 06/14] fix: respect MUX_BASE_URL env var during login and avoid redundant config read - Login now checks env-file > process.env.MUX_BASE_URL > default, so interactive login with MUX_BASE_URL set works correctly. - createAuthenticatedMuxClient reads baseUrl from the already-fetched environment instead of calling getMuxBaseUrl() (which would re-read config from disk). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.ts | 16 +++++++++------- src/lib/mux.ts | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/commands/login.ts b/src/commands/login.ts index 6d3e393..6658804 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -83,6 +83,8 @@ export const loginCommand = new Command() ); } + // Resolve base URL: env-file > MUX_BASE_URL env var > default + // Only persist to config if nonstandard let baseUrl: string | undefined; if (options.envFile) { @@ -100,10 +102,7 @@ export const loginCommand = new Command() tokenId = envVars.MUX_TOKEN_ID; tokenSecret = envVars.MUX_TOKEN_SECRET; - - if (envVars.MUX_BASE_URL && envVars.MUX_BASE_URL !== DEFAULT_BASE_URL) { - baseUrl = envVars.MUX_BASE_URL; - } + baseUrl = envVars.MUX_BASE_URL || process.env.MUX_BASE_URL; } else { // Interactive prompts console.log('Enter your Mux API credentials.'); @@ -120,14 +119,17 @@ export const loginCommand = new Command() if (!tokenSecret.trim()) { throw new Error('Token Secret is required'); } + + baseUrl = process.env.MUX_BASE_URL; } - // Validate credentials + // Validate credentials against the resolved base URL (never fall through to config) + const validationUrl = baseUrl || DEFAULT_BASE_URL; console.log('Validating credentials...'); const validation = await validateCredentials( tokenId.trim(), tokenSecret.trim(), - baseUrl || DEFAULT_BASE_URL, + validationUrl, ); if (!validation.valid) { @@ -141,7 +143,7 @@ export const loginCommand = new Command() tokenId: tokenId.trim(), tokenSecret: tokenSecret.trim(), environmentId: validation.environmentId, - ...(baseUrl && { baseUrl }), + ...(baseUrl && baseUrl !== DEFAULT_BASE_URL && { baseUrl }), }); console.log( diff --git a/src/lib/mux.ts b/src/lib/mux.ts index dbc430c..130d387 100644 --- a/src/lib/mux.ts +++ b/src/lib/mux.ts @@ -56,7 +56,8 @@ export async function createAuthenticatedMuxClient(): Promise { throw new Error("Not logged in. Please run 'mux login' to authenticate."); } - const baseURL = await getMuxBaseUrl(); + const baseURL = + process.env.MUX_BASE_URL || env.environment.baseUrl || DEFAULT_BASE_URL; return new Mux({ tokenId: env.environment.tokenId, From 6c6b16f64f0f673deab18cc0ebc18abf73830fbd Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 15:38:39 -0700 Subject: [PATCH 07/14] refactor: extract resolveBaseUrl to eliminate duplicated logic Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/mux.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/lib/mux.ts b/src/lib/mux.ts index 130d387..d9c0a47 100644 --- a/src/lib/mux.ts +++ b/src/lib/mux.ts @@ -12,20 +12,20 @@ function getUserAgent(): string { } /** - * Get the Mux API base URL. + * Resolve the Mux API base URL. * Priority: MUX_BASE_URL env var > config baseUrl > default + * Pass a pre-fetched environment to avoid redundant config reads. */ -export async function getMuxBaseUrl(): Promise { - if (process.env.MUX_BASE_URL) { - return process.env.MUX_BASE_URL; - } +export function resolveBaseUrl(env?: { environment: { baseUrl?: string } } | null): string { + return process.env.MUX_BASE_URL || env?.environment.baseUrl || DEFAULT_BASE_URL; +} +/** + * Get the Mux API base URL (reads config if needed). + */ +export async function getMuxBaseUrl(): Promise { const env = await getCurrentEnvironment(); - if (env?.environment.baseUrl) { - return env.environment.baseUrl; - } - - return DEFAULT_BASE_URL; + return resolveBaseUrl(env); } /** @@ -56,8 +56,7 @@ export async function createAuthenticatedMuxClient(): Promise { throw new Error("Not logged in. Please run 'mux login' to authenticate."); } - const baseURL = - process.env.MUX_BASE_URL || env.environment.baseUrl || DEFAULT_BASE_URL; + const baseURL = resolveBaseUrl(env); return new Mux({ tokenId: env.environment.tokenId, From 9975948bc49e7c348e5c4aa66231b475746d7e6b Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 15:43:35 -0700 Subject: [PATCH 08/14] refactor: reuse resolveBaseUrl in login Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/commands/login.ts b/src/commands/login.ts index 6658804..8aa8342 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { Command } from '@cliffy/command'; import { listEnvironments, setEnvironment } from '../lib/config.ts'; -import { DEFAULT_BASE_URL, validateCredentials } from '../lib/mux.ts'; +import { DEFAULT_BASE_URL, resolveBaseUrl, validateCredentials } from '../lib/mux.ts'; import { inputPrompt, secretPrompt } from '../lib/prompt.ts'; import { getConfigPath } from '../lib/xdg.ts'; @@ -83,9 +83,7 @@ export const loginCommand = new Command() ); } - // Resolve base URL: env-file > MUX_BASE_URL env var > default - // Only persist to config if nonstandard - let baseUrl: string | undefined; + let baseUrl: string; if (options.envFile) { // Read from .env file @@ -102,7 +100,7 @@ export const loginCommand = new Command() tokenId = envVars.MUX_TOKEN_ID; tokenSecret = envVars.MUX_TOKEN_SECRET; - baseUrl = envVars.MUX_BASE_URL || process.env.MUX_BASE_URL; + baseUrl = resolveBaseUrl({ environment: { baseUrl: envVars.MUX_BASE_URL } }); } else { // Interactive prompts console.log('Enter your Mux API credentials.'); @@ -120,11 +118,10 @@ export const loginCommand = new Command() throw new Error('Token Secret is required'); } - baseUrl = process.env.MUX_BASE_URL; + baseUrl = resolveBaseUrl(null); } - // Validate credentials against the resolved base URL (never fall through to config) - const validationUrl = baseUrl || DEFAULT_BASE_URL; + const validationUrl = baseUrl; console.log('Validating credentials...'); const validation = await validateCredentials( tokenId.trim(), @@ -143,7 +140,7 @@ export const loginCommand = new Command() tokenId: tokenId.trim(), tokenSecret: tokenSecret.trim(), environmentId: validation.environmentId, - ...(baseUrl && baseUrl !== DEFAULT_BASE_URL && { baseUrl }), + ...(baseUrl !== DEFAULT_BASE_URL && { baseUrl }), }); console.log( From 831a1848e560cf7f0de21ee80a36df7a0fdc6662 Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 15:51:48 -0700 Subject: [PATCH 09/14] refactor: remove redundant validationUrl alias in login Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/login.ts b/src/commands/login.ts index 8aa8342..76a61d8 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -121,12 +121,11 @@ export const loginCommand = new Command() baseUrl = resolveBaseUrl(null); } - const validationUrl = baseUrl; console.log('Validating credentials...'); const validation = await validateCredentials( tokenId.trim(), tokenSecret.trim(), - validationUrl, + baseUrl, ); if (!validation.valid) { From 76abe688300f8a909a02d245312e990726ad1638 Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 15:52:46 -0700 Subject: [PATCH 10/14] refactor: use resolveBaseUrl in webhooks/listen to avoid redundant config read Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/webhooks/listen.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/webhooks/listen.ts b/src/commands/webhooks/listen.ts index 533cbe7..bc63f59 100644 --- a/src/commands/webhooks/listen.ts +++ b/src/commands/webhooks/listen.ts @@ -3,7 +3,7 @@ import { Command } from '@cliffy/command'; import { getCurrentEnvironment, updateEnvironment } from '@/lib/config.ts'; import { checkFetchPermissionError } from '@/lib/errors.ts'; import { appendEvent, type StoredEvent } from '@/lib/events-store.ts'; -import { getAuthHeaders, getMuxBaseUrl } from '@/lib/mux.ts'; +import { getAuthHeaders, resolveBaseUrl } from '@/lib/mux.ts'; import { parseSSEStream } from '@/lib/sse.ts'; import { buildSignedHeaders, getSigningSecret } from '@/lib/webhook-signing.ts'; @@ -61,7 +61,7 @@ export const listenCommand = new Command() } const authHeaders = await getAuthHeaders(); - const baseUrl = await getMuxBaseUrl(); + const baseUrl = resolveBaseUrl(env); const url = `${baseUrl}/system/v1/webhook-events/stream`; let signingSecret: string | undefined; From e635adace743c000d42c6d77c98dc2681ca45f13 Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 15:54:38 -0700 Subject: [PATCH 11/14] style: format resolveBaseUrl and login imports Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.ts | 10 ++++++++-- src/lib/mux.ts | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/commands/login.ts b/src/commands/login.ts index 76a61d8..ce97a0a 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -2,7 +2,11 @@ import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { Command } from '@cliffy/command'; import { listEnvironments, setEnvironment } from '../lib/config.ts'; -import { DEFAULT_BASE_URL, resolveBaseUrl, validateCredentials } from '../lib/mux.ts'; +import { + DEFAULT_BASE_URL, + resolveBaseUrl, + validateCredentials, +} from '../lib/mux.ts'; import { inputPrompt, secretPrompt } from '../lib/prompt.ts'; import { getConfigPath } from '../lib/xdg.ts'; @@ -100,7 +104,9 @@ export const loginCommand = new Command() tokenId = envVars.MUX_TOKEN_ID; tokenSecret = envVars.MUX_TOKEN_SECRET; - baseUrl = resolveBaseUrl({ environment: { baseUrl: envVars.MUX_BASE_URL } }); + baseUrl = resolveBaseUrl({ + environment: { baseUrl: envVars.MUX_BASE_URL }, + }); } else { // Interactive prompts console.log('Enter your Mux API credentials.'); diff --git a/src/lib/mux.ts b/src/lib/mux.ts index d9c0a47..1ac9e43 100644 --- a/src/lib/mux.ts +++ b/src/lib/mux.ts @@ -16,8 +16,12 @@ function getUserAgent(): string { * Priority: MUX_BASE_URL env var > config baseUrl > default * Pass a pre-fetched environment to avoid redundant config reads. */ -export function resolveBaseUrl(env?: { environment: { baseUrl?: string } } | null): string { - return process.env.MUX_BASE_URL || env?.environment.baseUrl || DEFAULT_BASE_URL; +export function resolveBaseUrl( + env?: { environment: { baseUrl?: string } } | null, +): string { + return ( + process.env.MUX_BASE_URL || env?.environment.baseUrl || DEFAULT_BASE_URL + ); } /** From ee6b73d6eb31041444067351b782adde4b91b1ed Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 16:02:38 -0700 Subject: [PATCH 12/14] refactor: add getAuthContext to avoid redundant config reads Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/whoami.ts | 5 ++--- src/lib/errors.ts | 5 ++--- src/lib/mux.ts | 21 +++++++++++++++++---- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index 30032e8..91554b4 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -1,6 +1,6 @@ import { Command } from '@cliffy/command'; import { handleCommandError } from '@/lib/errors.ts'; -import { DEFAULT_BASE_URL, getAuthHeaders, getMuxBaseUrl } from '../lib/mux.ts'; +import { DEFAULT_BASE_URL, getAuthContext } from '../lib/mux.ts'; interface WhoAmIOptions { json?: boolean; @@ -11,8 +11,7 @@ export const whoamiCommand = new Command() .option('--json', 'Output JSON instead of pretty format') .action(async (options: WhoAmIOptions) => { try { - const headers = await getAuthHeaders(); - const baseUrl = await getMuxBaseUrl(); + const { headers, baseUrl } = await getAuthContext(); const response = await fetch(`${baseUrl}/system/v1/whoami`, { headers }); if (!response.ok) { diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 77d6909..52be4ee 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -1,5 +1,5 @@ import { AuthenticationError, NotFoundError } from '@mux/mux-node'; -import { getAuthHeaders, getMuxBaseUrl } from './mux.ts'; +import { getAuthContext } from './mux.ts'; /** * Format a permission error message for display. @@ -47,8 +47,7 @@ async function fetchTokenInfo(): Promise<{ tokenName?: string; } | null> { try { - const headers = await getAuthHeaders(); - const baseUrl = await getMuxBaseUrl(); + const { headers, baseUrl } = await getAuthContext(); const response = await fetch(`${baseUrl}/system/v1/whoami`, { headers }); if (!response.ok) return null; diff --git a/src/lib/mux.ts b/src/lib/mux.ts index 1ac9e43..232fdd4 100644 --- a/src/lib/mux.ts +++ b/src/lib/mux.ts @@ -33,9 +33,12 @@ export async function getMuxBaseUrl(): Promise { } /** - * Get auth headers for raw fetch requests to Mux API + * Get auth headers and base URL in a single config read. */ -export async function getAuthHeaders(): Promise> { +export async function getAuthContext(): Promise<{ + headers: Record; + baseUrl: string; +}> { const env = await getCurrentEnvironment(); if (!env) { throw new Error("Not logged in. Please run 'mux login' to authenticate."); @@ -45,11 +48,21 @@ export async function getAuthHeaders(): Promise> { `${env.environment.tokenId}:${env.environment.tokenSecret}`, ); return { - Authorization: `Basic ${credentials}`, - 'User-Agent': getUserAgent(), + headers: { + Authorization: `Basic ${credentials}`, + 'User-Agent': getUserAgent(), + }, + baseUrl: resolveBaseUrl(env), }; } +/** + * Get auth headers for raw fetch requests to Mux API + */ +export async function getAuthHeaders(): Promise> { + return (await getAuthContext()).headers; +} + /** * Create an authenticated Mux client using stored credentials * Throws an error if not logged in From 3977964947ad2be1b222a78f5cdf332ba06e4668 Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 16:21:58 -0700 Subject: [PATCH 13/14] refactor: rename resolveBaseUrl to getMuxUrl and remove getMuxBaseUrl wrapper Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.ts | 6 +++--- src/commands/webhooks/listen.ts | 4 ++-- src/lib/mux.test.ts | 17 +++++++++++------ src/lib/mux.ts | 17 ++++------------- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/commands/login.ts b/src/commands/login.ts index ce97a0a..ffed591 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -4,7 +4,7 @@ import { Command } from '@cliffy/command'; import { listEnvironments, setEnvironment } from '../lib/config.ts'; import { DEFAULT_BASE_URL, - resolveBaseUrl, + getMuxUrl, validateCredentials, } from '../lib/mux.ts'; import { inputPrompt, secretPrompt } from '../lib/prompt.ts'; @@ -104,7 +104,7 @@ export const loginCommand = new Command() tokenId = envVars.MUX_TOKEN_ID; tokenSecret = envVars.MUX_TOKEN_SECRET; - baseUrl = resolveBaseUrl({ + baseUrl = getMuxUrl({ environment: { baseUrl: envVars.MUX_BASE_URL }, }); } else { @@ -124,7 +124,7 @@ export const loginCommand = new Command() throw new Error('Token Secret is required'); } - baseUrl = resolveBaseUrl(null); + baseUrl = getMuxUrl(null); } console.log('Validating credentials...'); diff --git a/src/commands/webhooks/listen.ts b/src/commands/webhooks/listen.ts index bc63f59..2cc87dc 100644 --- a/src/commands/webhooks/listen.ts +++ b/src/commands/webhooks/listen.ts @@ -3,7 +3,7 @@ import { Command } from '@cliffy/command'; import { getCurrentEnvironment, updateEnvironment } from '@/lib/config.ts'; import { checkFetchPermissionError } from '@/lib/errors.ts'; import { appendEvent, type StoredEvent } from '@/lib/events-store.ts'; -import { getAuthHeaders, resolveBaseUrl } from '@/lib/mux.ts'; +import { getAuthHeaders, getMuxUrl } from '@/lib/mux.ts'; import { parseSSEStream } from '@/lib/sse.ts'; import { buildSignedHeaders, getSigningSecret } from '@/lib/webhook-signing.ts'; @@ -61,7 +61,7 @@ export const listenCommand = new Command() } const authHeaders = await getAuthHeaders(); - const baseUrl = resolveBaseUrl(env); + const baseUrl = getMuxUrl(env); const url = `${baseUrl}/system/v1/webhook-events/stream`; let signingSecret: string | undefined; diff --git a/src/lib/mux.test.ts b/src/lib/mux.test.ts index 3a16b8b..4e5c886 100644 --- a/src/lib/mux.test.ts +++ b/src/lib/mux.test.ts @@ -2,10 +2,11 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { getCurrentEnvironment } from './config.ts'; import { setEnvironment } from './config.ts'; -import { DEFAULT_BASE_URL, getMuxBaseUrl } from './mux.ts'; +import { DEFAULT_BASE_URL, getMuxUrl } from './mux.ts'; -describe('getMuxBaseUrl', () => { +describe('getMuxUrl', () => { let testConfigDir: string; let originalXdgConfigHome: string | undefined; let originalMuxBaseUrl: string | undefined; @@ -33,7 +34,8 @@ describe('getMuxBaseUrl', () => { }); it('should return default when no env var or config', async () => { - expect(await getMuxBaseUrl()).toBe(DEFAULT_BASE_URL); + const env = await getCurrentEnvironment(); + expect(getMuxUrl(env)).toBe(DEFAULT_BASE_URL); }); it('should prefer MUX_BASE_URL env var over everything', async () => { @@ -44,7 +46,8 @@ describe('getMuxBaseUrl', () => { baseUrl: 'https://config.example.com', }); - expect(await getMuxBaseUrl()).toBe('https://env-var.example.com'); + const env = await getCurrentEnvironment(); + expect(getMuxUrl(env)).toBe('https://env-var.example.com'); }); it('should use config baseUrl when no env var is set', async () => { @@ -54,7 +57,8 @@ describe('getMuxBaseUrl', () => { baseUrl: 'https://api.staging.mux.com', }); - expect(await getMuxBaseUrl()).toBe('https://api.staging.mux.com'); + const env = await getCurrentEnvironment(); + expect(getMuxUrl(env)).toBe('https://api.staging.mux.com'); }); it('should fall back to default when config has no baseUrl', async () => { @@ -63,6 +67,7 @@ describe('getMuxBaseUrl', () => { tokenSecret: 'secret', }); - expect(await getMuxBaseUrl()).toBe(DEFAULT_BASE_URL); + const env = await getCurrentEnvironment(); + expect(getMuxUrl(env)).toBe(DEFAULT_BASE_URL); }); }); diff --git a/src/lib/mux.ts b/src/lib/mux.ts index 232fdd4..b5fe827 100644 --- a/src/lib/mux.ts +++ b/src/lib/mux.ts @@ -14,9 +14,8 @@ function getUserAgent(): string { /** * Resolve the Mux API base URL. * Priority: MUX_BASE_URL env var > config baseUrl > default - * Pass a pre-fetched environment to avoid redundant config reads. */ -export function resolveBaseUrl( +export function getMuxUrl( env?: { environment: { baseUrl?: string } } | null, ): string { return ( @@ -24,14 +23,6 @@ export function resolveBaseUrl( ); } -/** - * Get the Mux API base URL (reads config if needed). - */ -export async function getMuxBaseUrl(): Promise { - const env = await getCurrentEnvironment(); - return resolveBaseUrl(env); -} - /** * Get auth headers and base URL in a single config read. */ @@ -52,7 +43,7 @@ export async function getAuthContext(): Promise<{ Authorization: `Basic ${credentials}`, 'User-Agent': getUserAgent(), }, - baseUrl: resolveBaseUrl(env), + baseUrl: getMuxUrl(env), }; } @@ -73,7 +64,7 @@ export async function createAuthenticatedMuxClient(): Promise { throw new Error("Not logged in. Please run 'mux login' to authenticate."); } - const baseURL = resolveBaseUrl(env); + const baseURL = getMuxUrl(env); return new Mux({ tokenId: env.environment.tokenId, @@ -93,7 +84,7 @@ export async function validateCredentials( overrideBaseUrl?: string, ): Promise<{ valid: boolean; environmentId?: string; error?: string }> { try { - const baseUrl = overrideBaseUrl || (await getMuxBaseUrl()); + const baseUrl = overrideBaseUrl || getMuxUrl(await getCurrentEnvironment()); const credentials = btoa(`${tokenId}:${tokenSecret}`); const response = await fetch(`${baseUrl}/system/v1/whoami`, { headers: { From 0017213e0cde8cc38a1596bdf76eb747aea9e22b Mon Sep 17 00:00:00 2001 From: Jordan Mann Date: Thu, 16 Apr 2026 16:22:39 -0700 Subject: [PATCH 14/14] refactor: rename getMuxUrl back to getMuxBaseUrl Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/login.ts | 6 +++--- src/commands/webhooks/listen.ts | 4 ++-- src/lib/mux.test.ts | 12 ++++++------ src/lib/mux.ts | 9 +++++---- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/commands/login.ts b/src/commands/login.ts index ffed591..ef2a87a 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -4,7 +4,7 @@ import { Command } from '@cliffy/command'; import { listEnvironments, setEnvironment } from '../lib/config.ts'; import { DEFAULT_BASE_URL, - getMuxUrl, + getMuxBaseUrl, validateCredentials, } from '../lib/mux.ts'; import { inputPrompt, secretPrompt } from '../lib/prompt.ts'; @@ -104,7 +104,7 @@ export const loginCommand = new Command() tokenId = envVars.MUX_TOKEN_ID; tokenSecret = envVars.MUX_TOKEN_SECRET; - baseUrl = getMuxUrl({ + baseUrl = getMuxBaseUrl({ environment: { baseUrl: envVars.MUX_BASE_URL }, }); } else { @@ -124,7 +124,7 @@ export const loginCommand = new Command() throw new Error('Token Secret is required'); } - baseUrl = getMuxUrl(null); + baseUrl = getMuxBaseUrl(null); } console.log('Validating credentials...'); diff --git a/src/commands/webhooks/listen.ts b/src/commands/webhooks/listen.ts index 2cc87dc..d7985fd 100644 --- a/src/commands/webhooks/listen.ts +++ b/src/commands/webhooks/listen.ts @@ -3,7 +3,7 @@ import { Command } from '@cliffy/command'; import { getCurrentEnvironment, updateEnvironment } from '@/lib/config.ts'; import { checkFetchPermissionError } from '@/lib/errors.ts'; import { appendEvent, type StoredEvent } from '@/lib/events-store.ts'; -import { getAuthHeaders, getMuxUrl } from '@/lib/mux.ts'; +import { getAuthHeaders, getMuxBaseUrl } from '@/lib/mux.ts'; import { parseSSEStream } from '@/lib/sse.ts'; import { buildSignedHeaders, getSigningSecret } from '@/lib/webhook-signing.ts'; @@ -61,7 +61,7 @@ export const listenCommand = new Command() } const authHeaders = await getAuthHeaders(); - const baseUrl = getMuxUrl(env); + const baseUrl = getMuxBaseUrl(env); const url = `${baseUrl}/system/v1/webhook-events/stream`; let signingSecret: string | undefined; diff --git a/src/lib/mux.test.ts b/src/lib/mux.test.ts index 4e5c886..8553d11 100644 --- a/src/lib/mux.test.ts +++ b/src/lib/mux.test.ts @@ -4,9 +4,9 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { getCurrentEnvironment } from './config.ts'; import { setEnvironment } from './config.ts'; -import { DEFAULT_BASE_URL, getMuxUrl } from './mux.ts'; +import { DEFAULT_BASE_URL, getMuxBaseUrl } from './mux.ts'; -describe('getMuxUrl', () => { +describe('getMuxBaseUrl', () => { let testConfigDir: string; let originalXdgConfigHome: string | undefined; let originalMuxBaseUrl: string | undefined; @@ -35,7 +35,7 @@ describe('getMuxUrl', () => { it('should return default when no env var or config', async () => { const env = await getCurrentEnvironment(); - expect(getMuxUrl(env)).toBe(DEFAULT_BASE_URL); + expect(getMuxBaseUrl(env)).toBe(DEFAULT_BASE_URL); }); it('should prefer MUX_BASE_URL env var over everything', async () => { @@ -47,7 +47,7 @@ describe('getMuxUrl', () => { }); const env = await getCurrentEnvironment(); - expect(getMuxUrl(env)).toBe('https://env-var.example.com'); + expect(getMuxBaseUrl(env)).toBe('https://env-var.example.com'); }); it('should use config baseUrl when no env var is set', async () => { @@ -58,7 +58,7 @@ describe('getMuxUrl', () => { }); const env = await getCurrentEnvironment(); - expect(getMuxUrl(env)).toBe('https://api.staging.mux.com'); + expect(getMuxBaseUrl(env)).toBe('https://api.staging.mux.com'); }); it('should fall back to default when config has no baseUrl', async () => { @@ -68,6 +68,6 @@ describe('getMuxUrl', () => { }); const env = await getCurrentEnvironment(); - expect(getMuxUrl(env)).toBe(DEFAULT_BASE_URL); + expect(getMuxBaseUrl(env)).toBe(DEFAULT_BASE_URL); }); }); diff --git a/src/lib/mux.ts b/src/lib/mux.ts index b5fe827..2c4bd4a 100644 --- a/src/lib/mux.ts +++ b/src/lib/mux.ts @@ -15,7 +15,7 @@ function getUserAgent(): string { * Resolve the Mux API base URL. * Priority: MUX_BASE_URL env var > config baseUrl > default */ -export function getMuxUrl( +export function getMuxBaseUrl( env?: { environment: { baseUrl?: string } } | null, ): string { return ( @@ -43,7 +43,7 @@ export async function getAuthContext(): Promise<{ Authorization: `Basic ${credentials}`, 'User-Agent': getUserAgent(), }, - baseUrl: getMuxUrl(env), + baseUrl: getMuxBaseUrl(env), }; } @@ -64,7 +64,7 @@ export async function createAuthenticatedMuxClient(): Promise { throw new Error("Not logged in. Please run 'mux login' to authenticate."); } - const baseURL = getMuxUrl(env); + const baseURL = getMuxBaseUrl(env); return new Mux({ tokenId: env.environment.tokenId, @@ -84,7 +84,8 @@ export async function validateCredentials( overrideBaseUrl?: string, ): Promise<{ valid: boolean; environmentId?: string; error?: string }> { try { - const baseUrl = overrideBaseUrl || getMuxUrl(await getCurrentEnvironment()); + const baseUrl = + overrideBaseUrl || getMuxBaseUrl(await getCurrentEnvironment()); const credentials = btoa(`${tokenId}:${tokenSecret}`); const response = await fetch(`${baseUrl}/system/v1/whoami`, { headers: {