Skip to content
Open
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
16 changes: 15 additions & 1 deletion src/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { processInput } from './input.js';
import { ActorsMcpServer } from './mcp/server.js';
import { getTelemetryEnv } from './telemetry.js';
import type { ApifyRequestParams, Input, TelemetryEnv, ToolSelector, UiMode } from './types.js';
import { isApiTokenRequired } from './utils/auth.js';
import { parseCommaSeparatedList } from './utils/generic.js';
import { loadToolsFromInput } from './utils/tools-loader.js';

Expand Down Expand Up @@ -160,8 +161,20 @@ log.error = (...args: Parameters<typeof log.error>) => {
// Get token from environment or auth file
const apifyToken = process.env.APIFY_TOKEN || getTokenFromAuthFile();

// Determine if authentication is required based on requested tools
// Only public tools (like docs) can run without a token
const requiresAuthentication = isApiTokenRequired({
toolCategoryKeys,
actorList,
enableAddingActors,
});

if (!requiresAuthentication) {
log.info('Running in unauthenticated mode (only public tools requested)');
}

// Validate environment
if (!apifyToken) {
if (requiresAuthentication && !apifyToken) {
log.error('APIFY_TOKEN is required but not set in the environment variables or in ~/.apify/auth.json');
process.exit(1);
}
Expand All @@ -175,6 +188,7 @@ async function main() {
},
token: apifyToken,
uiMode: argv.ui,
allowUnauthMode: !requiresAuthentication,
});

// Create an Input object from CLI arguments
Expand Down
44 changes: 44 additions & 0 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { getUnauthEnabledToolCategories, toolCategories, unauthEnabledTools } from '../tools/index.js';

/**
* Determines if an API token is required based on requested tools and actors.
*/
export function isApiTokenRequired(params: {
toolCategoryKeys?: string[];
actorList?: string[];
enableAddingActors?: boolean;
}): boolean {
const { toolCategoryKeys, actorList, enableAddingActors } = params;

// If no tools or categories specified, default to requiring token
// (This matches current requirement for full server start)
if (!toolCategoryKeys || toolCategoryKeys.length === 0) {
return true;
}

const unauthTokenSet = new Set(unauthEnabledTools);
const unauthCategorySet = new Set(getUnauthEnabledToolCategories());

const areAllToolsSafe = toolCategoryKeys.every((key) => {
// If it is a safe category
if (unauthCategorySet.has(key as any)) return true;
// If it is a safe tool
if (unauthTokenSet.has(key)) return true;

// If it is a known category but not safe -> unsafe
if (key in toolCategories) return false;

// Otherwise it is likely an Actor name -> unsafe
return false;
});

const isActorsEmpty = !actorList || actorList.length === 0;

// Only bypass token if all requested tools are public AND no specific actors requested
// AND adding actors at runtime is disabled.
if (areAllToolsSafe && isActorsEmpty && !enableAddingActors) {
return false;
}

return true;
}
51 changes: 51 additions & 0 deletions tests/unit/utils.auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import { isApiTokenRequired } from '../../src/utils/auth.js';

describe('isApiTokenRequired', () => {
it('should require token if no tools are specified', () => {
expect(isApiTokenRequired({})).toBe(true);
expect(isApiTokenRequired({ toolCategoryKeys: [] })).toBe(true);
});

it('should NOT require token for only public tools', () => {
expect(isApiTokenRequired({
toolCategoryKeys: ['search-actors'],
})).toBe(false);

expect(isApiTokenRequired({
toolCategoryKeys: ['search-apify-docs', 'fetch-apify-docs'],
})).toBe(false);
});

it('should require token if any private tool is included', () => {
expect(isApiTokenRequired({
toolCategoryKeys: ['search-actors', 'call-actor'],
})).toBe(true);
});

it('should require token if any non-public category is used', () => {
expect(isApiTokenRequired({
toolCategoryKeys: ['actors'],
})).toBe(true);
});

it('should require token if specifically requested actors subset', () => {
expect(isApiTokenRequired({
toolCategoryKeys: ['search-actors'],
actorList: ['apify/web-scraper'],
})).toBe(true);
});

it('should require token if enableAddingActors is true', () => {
expect(isApiTokenRequired({
toolCategoryKeys: ['search-actors'],
enableAddingActors: true,
})).toBe(true);
});

it('should handle unknown keys as potentially unsafe (requiring token)', () => {
expect(isApiTokenRequired({
toolCategoryKeys: ['some-unknown-potential-actor-name'],
})).toBe(true);
});
});