Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ RESULTS=$(valyu answer "latest rate decision" --quiet)
VALYU_API_KEY=your_key valyu search web "query" --json
```

> **Key precedence:** `--api-key` flag → your `valyu login` key → `VALYU_API_KEY`. A logged-in
> key takes precedence over the env var, so you don't need to unset `VALYU_API_KEY` after
> running `valyu login`. `VALYU_API_KEY` still applies when you haven't logged in (e.g. CI).

### Multiple profiles

```bash
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@valyu/cli",
"version": "1.2.0",
"version": "1.2.1",
"description": "The search CLI for knowledge workers",
"license": "MIT",
"repository": {
Expand Down
65 changes: 57 additions & 8 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import { describe, it, expect, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { resolveApiKey, maskKey } from '../lib/config.js';

describe('resolveApiKey', () => {
const originalEnv = process.env.VALYU_API_KEY;
const originalXdg = process.env.XDG_CONFIG_HOME;
let tmpRoot: string;

// Point the config dir at a fresh empty temp dir so tests are hermetic and
// never read the developer's real ~/.config/valyu/credentials.json.
beforeEach(() => {
tmpRoot = mkdtempSync(join(tmpdir(), 'valyu-cli-test-'));
process.env.XDG_CONFIG_HOME = tmpRoot;
delete process.env.VALYU_API_KEY;
});

afterEach(() => {
if (originalEnv === undefined) {
delete process.env.VALYU_API_KEY;
} else {
process.env.VALYU_API_KEY = originalEnv;
}
if (originalEnv === undefined) delete process.env.VALYU_API_KEY;
else process.env.VALYU_API_KEY = originalEnv;
if (originalXdg === undefined) delete process.env.XDG_CONFIG_HOME;
else process.env.XDG_CONFIG_HOME = originalXdg;
rmSync(tmpRoot, { recursive: true, force: true });
});

// Write a credentials.json into the temp config dir (mimics `valyu login`).
const writeLogin = (apiKey: string, profile = 'default', active = profile) => {
const dir = join(tmpRoot, 'valyu');
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, 'credentials.json'),
JSON.stringify({ active_profile: active, profiles: { [profile]: { api_key: apiKey } } }),
);
};

it('returns flag value when provided', () => {
delete process.env.VALYU_API_KEY;
const result = resolveApiKey('flag-key-value-here-1234');
expect(result).toEqual({ key: 'flag-key-value-here-1234', source: 'flag' });
});

it('returns env value when VALYU_API_KEY is set', () => {
it('returns env value when VALYU_API_KEY is set and there is no login', () => {
process.env.VALYU_API_KEY = 'env-key-value-here-5678';
const result = resolveApiKey(undefined);
expect(result).toEqual({ key: 'env-key-value-here-5678', source: 'env' });
Expand All @@ -30,6 +52,33 @@ describe('resolveApiKey', () => {
expect(result?.source).toBe('flag');
expect(result?.key).toBe('flag-key-value-here-1234');
});

it('prefers the logged-in key (config) over VALYU_API_KEY', () => {
// The headline fix: a `valyu login` key must win over an ambient env var so
// users do not have to unset VALYU_API_KEY.
writeLogin('val_login_key_1234567890');
process.env.VALYU_API_KEY = 'env-key-value-here-5678';
const result = resolveApiKey(undefined);
expect(result).toEqual({ key: 'val_login_key_1234567890', source: 'config' });
});

it('prefers flag over both the logged-in key and the env var', () => {
writeLogin('val_login_key_1234567890');
process.env.VALYU_API_KEY = 'env-key-value-here-5678';
const result = resolveApiKey('flag-key-value-here-1234');
expect(result).toEqual({ key: 'flag-key-value-here-1234', source: 'flag' });
});

it('falls back to env when the requested profile has no stored key', () => {
writeLogin('val_login_key_1234567890', 'default');
process.env.VALYU_API_KEY = 'env-key-value-here-5678';
const result = resolveApiKey(undefined, 'ci');
expect(result).toEqual({ key: 'env-key-value-here-5678', source: 'env' });
});

it('returns null when no flag, no login, and no env var', () => {
expect(resolveApiKey(undefined)).toBeNull();
});
});

describe('maskKey', () => {
Expand Down
14 changes: 14 additions & 0 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ const API_KEYS_URL = `${PLATFORM_URL}/user/account/apikeys`;

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

// Reassure the user that a pre-existing VALYU_API_KEY no longer shadows the key
// they just minted (the logged-in key takes precedence - see resolveApiKey).
function noteEnvPrecedence(): void {
if (process.env.VALYU_API_KEY) {
console.log(
pc.dim(
' Note: VALYU_API_KEY is set in your environment - your logged-in key now takes precedence and will be used.',
),
);
}
}

export const loginCommand = new Command('login')
.description('Authenticate the CLI (device flow by default; mints a scoped Valyu key)')
.option('--key <key>', 'Authenticate with an existing API key (manual / CI mode)')
Expand Down Expand Up @@ -149,6 +161,7 @@ async function deviceLogin(
p.outro(
`Logged in as ${pc.cyan(token.key_prefix)} - key stored for profile ${pc.cyan(`'${profile}'`)}`,
);
noteEnvPrecedence();
return;
}

Expand Down Expand Up @@ -254,5 +267,6 @@ async function manualLogin(
outputResult({ success: true, config_path: configPath, profile: profileName }, { json: true });
} else {
p.outro(`Logged in. API key stored for profile ${pc.cyan(`'${profileName}'`)}`);
noteEnvPrecedence();
}
}
19 changes: 14 additions & 5 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,24 @@ export function writeCredentials(data: CredentialsFile): void {
}

export function resolveApiKey(flagValue?: string, profile?: string): ResolvedKey | null {
// Precedence: explicit flag > local login (config) > ambient VALYU_API_KEY env.
//
// A `valyu login` is a stronger, deliberate signal of intent than an ambient
// env var, so the logged-in key wins - otherwise a user who already has
// VALYU_API_KEY set would run `valyu login`, mint a scoped key, and silently
// keep using the old env-var key. The env var stays as the fallback for the
// no-login case (CI, unauthenticated), so those flows are unchanged. `--api-key`
// remains the ultimate override for one-off / scripted use.
if (flagValue) return { key: flagValue, source: 'flag' };
if (process.env.VALYU_API_KEY) return { key: process.env.VALYU_API_KEY, source: 'env' };

const creds = readCredentials();
if (!creds) return null;
if (creds) {
const profileName = profile ?? creds.active_profile ?? 'default';
const p = creds.profiles[profileName];
if (p?.api_key) return { key: p.api_key, source: 'config' };
}

const profileName = profile ?? creds.active_profile ?? 'default';
const p = creds.profiles[profileName];
if (p?.api_key) return { key: p.api_key, source: 'config' };
if (process.env.VALYU_API_KEY) return { key: process.env.VALYU_API_KEY, source: 'env' };

return null;
}
Expand Down