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
6 changes: 3 additions & 3 deletions src/services/api-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class ApiService {

if (!this.token && !options.allowNoToken) {
throw new VizzlyError(
'No API token provided. Set VIZZLY_TOKEN environment variable or run "vizzly login".'
'No API token provided. Set VIZZLY_TOKEN environment variable or link a project in the TDD dashboard.'
);
}
}
Expand Down Expand Up @@ -118,13 +118,13 @@ export class ApiService {
}

throw new AuthError(
'Invalid or expired API token. Please run "vizzly login" to authenticate.'
'Invalid or expired API token. Link a project via "vizzly project:select" or set VIZZLY_TOKEN.'
);
}

if (response.status === 401) {
throw new AuthError(
'Invalid or expired API token. Please run "vizzly login" to authenticate.'
'Invalid or expired API token. Link a project via "vizzly project:select" or set VIZZLY_TOKEN.'
);
}

Expand Down
10 changes: 1 addition & 9 deletions src/utils/config-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cosmiconfigSync } from 'cosmiconfig';
import { resolve } from 'path';
import { getApiToken, getApiUrl, getParallelId } from './environment-config.js';
import { validateVizzlyConfigWithDefaults } from './config-schema.js';
import { getAccessToken, getProjectMapping } from './global-config.js';
import { getProjectMapping } from './global-config.js';
import * as output from './output.js';

let DEFAULT_CONFIG = {
Expand Down Expand Up @@ -102,14 +102,6 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
}
}

// 3.5. Check global config for user access token (if no CLI flag)
if (!config.apiKey && !cliOverrides.token) {
let globalToken = await getAccessToken();
if (globalToken) {
config.apiKey = globalToken;
}
}

// 4. Override with environment variables (higher priority than fallbacks)
let envApiKey = getApiToken();
let envApiUrl = getApiUrl();
Expand Down
27 changes: 15 additions & 12 deletions tests/unit/config-loader-token-resolution.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* Tests for config-loader token resolution priority
* Priority order: CLI flag > Env var > Project mapping > User access token
* Priority order: CLI flag > Env var > Project mapping
* Note: User access tokens (JWTs from login) are NOT used as API keys
* They are only for user-level operations, not SDK endpoints
*/

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
Expand Down Expand Up @@ -160,13 +162,16 @@ describe('Config Loader - Token Resolution Priority', () => {
});
});

describe('Priority 4: User access token (lowest)', () => {
it('should use user access token when no other sources', async () => {
describe('User access tokens are NOT used as API keys', () => {
it('should NOT use user access token as API key (security fix)', async () => {
// User access tokens (JWTs) should not be used for SDK endpoints
// They have different format and permissions than project tokens
globalConfig.__setMockAccessToken('user_access_token_abc');

let config = await loadConfig(null, {});

expect(config.apiKey).toBe('user_access_token_abc');
// apiKey should be undefined, NOT the user access token
expect(config.apiKey).toBeUndefined();
});

it('should return undefined apiKey when no token sources available', async () => {
Expand All @@ -177,33 +182,31 @@ describe('Config Loader - Token Resolution Priority', () => {
});

describe('Edge cases', () => {
it('should handle empty CLI overrides object', async () => {
globalConfig.__setMockAccessToken('user_access_token_abc');

it('should handle empty CLI overrides object with no tokens', async () => {
let config = await loadConfig(null, {});

expect(config.apiKey).toBe('user_access_token_abc');
expect(config.apiKey).toBeUndefined();
});

it('should handle null project mapping', async () => {
globalConfig.__setMockProjectMapping(null);
globalConfig.__setMockAccessToken('user_access_token_abc');

let config = await loadConfig(null, {});

expect(config.apiKey).toBe('user_access_token_abc');
// Without project mapping or env var, apiKey should be undefined
expect(config.apiKey).toBeUndefined();
});

it('should handle project mapping without token field', async () => {
globalConfig.__setMockProjectMapping({
projectSlug: 'test-project',
organizationSlug: 'test-org',
});
globalConfig.__setMockAccessToken('user_access_token_abc');

let config = await loadConfig(null, {});

expect(config.apiKey).toBe('user_access_token_abc');
// Project mapping without token should result in undefined apiKey
expect(config.apiKey).toBeUndefined();
});

it('should skip project mapping lookup when CLI token provided', async () => {
Expand Down