Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/commands/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
17 changes: 15 additions & 2 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -48,6 +49,8 @@ export async function parseEnvFile(filePath: string): Promise<EnvVars> {
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;
}
}
}
Expand Down Expand Up @@ -80,6 +83,10 @@ 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) {
// Read from .env file
console.log(`Reading credentials from ${options.envFile}...`);
Expand All @@ -95,6 +102,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;
} else {
// Interactive prompts
console.log('Enter your Mux API credentials.');
Expand All @@ -111,13 +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(),
validationUrl,
);

if (!validation.valid) {
Expand All @@ -131,6 +143,7 @@ export const loginCommand = new Command()
tokenId: tokenId.trim(),
tokenSecret: tokenSecret.trim(),
environmentId: validation.environmentId,
...(baseUrl && baseUrl !== DEFAULT_BASE_URL && { baseUrl }),
});

console.log(
Expand Down
2 changes: 1 addition & 1 deletion src/commands/webhooks/listen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const listenCommand = new Command()
}

const authHeaders = await getAuthHeaders();
const baseUrl = getMuxBaseUrl();
const baseUrl = await getMuxBaseUrl();
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
const url = `${baseUrl}/system/v1/webhook-events/stream`;

let signingSecret: string | undefined;
Expand Down
7 changes: 5 additions & 2 deletions src/commands/whoami.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
const response = await fetch(`${baseUrl}/system/v1/whoami`, { headers });

if (!response.ok) {
Expand All @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface Environment {
tokenId: string;
tokenSecret: string;
environmentId?: string;
baseUrl?: string;
signingKeyId?: string;
signingPrivateKey?: string;
forwardUrl?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
68 changes: 68 additions & 0 deletions src/lib/mux.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
25 changes: 20 additions & 5 deletions src/lib/mux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<string> {
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;
Comment thread
jrmann100 marked this conversation as resolved.
Outdated
}

/**
Expand Down Expand Up @@ -46,9 +56,13 @@ export async function createAuthenticatedMuxClient(): Promise<Mux> {
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;
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

return new Mux({
tokenId: env.environment.tokenId,
tokenSecret: env.environment.tokenSecret,
...(baseURL !== DEFAULT_BASE_URL && { baseURL }),
defaultHeaders: { 'User-Agent': getUserAgent() },
});
}
Expand All @@ -60,9 +74,10 @@ export async function createAuthenticatedMuxClient(): Promise<Mux> {
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: {
Expand Down
Loading