Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f3f5d43
test: pin auth behavior before the multi-account refactor
l2ysho Sep 10, 2026
8c9dce4
test: pin the marker branch and the account-switch merge
l2ysho Sep 10, 2026
1e646ca
test: trim comments in the new auth tests
l2ysho Sep 10, 2026
8072cee
refactor: stop credential reads from writing credentials
l2ysho Sep 11, 2026
ec223d2
feat: one token resolution order across every command
l2ysho Sep 11, 2026
1e7e56c
feat: say when APIFY_TOKEN overrides a login or is ignored
l2ysho Sep 11, 2026
0dc2296
fix: bound and reorder the account lookup the resolver added
l2ysho Sep 11, 2026
20f253f
feat: fail on a misconfigured APIFY_TOKEN instead of warning
l2ysho Sep 16, 2026
21702ca
fix: forget the old account's proxy password on re-login
l2ysho Sep 16, 2026
d0bb306
refactor: memoize the resolver and give APIFY_TOKEN one reader
l2ysho Sep 16, 2026
91ad46c
refactor: apply the remaining cleanup findings
l2ysho Sep 16, 2026
fdfcfa0
docs: drop the APIFY_TOKEN notes from login's --token flag
l2ysho Sep 17, 2026
1e8537d
docs: trim the auth comments
l2ysho Sep 17, 2026
d06c799
Merge branch 'master' into claude/token-resolution-1418
l2ysho Sep 22, 2026
6097e23
Merge branch 'master' into claude/token-resolution-1418
l2ysho Sep 22, 2026
fca7d1c
docs: regenerate reference after the master merge
l2ysho Sep 22, 2026
a8ef0fa
docs: bring the agent skill in line with the new token order
l2ysho Sep 22, 2026
563f061
Update src/commands/auth/token.ts
l2ysho Sep 23, 2026
1c724f3
Update src/commands/auth/token.ts
l2ysho Sep 23, 2026
ca021f0
Update src/commands/auth/login.ts
l2ysho Sep 23, 2026
63ae11e
Update src/commands/auth/login.ts
l2ysho Sep 23, 2026
79df35f
Update src/commands/auth/login.ts
l2ysho Sep 23, 2026
2d07fc2
fix: quote the auth token descriptions so they parse
l2ysho Sep 23, 2026
afe3d57
test: expect the token source row in apify info
l2ysho Sep 23, 2026
faf5a7c
fix: one message when APIFY_TOKEN blocks a login
l2ysho Sep 23, 2026
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
26 changes: 15 additions & 11 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ SUBCOMMANDS
to '~/.apify/auth.json'.
auth logout Removes authentication by deleting your API token and
account information from '~/.apify/auth.json'.
auth token Prints the current API token for the Apify CLI.
auth token Prints the API token the CLI authenticates with,
resolved from APIFY_TOKEN or the token from 'apify login'.
```

##### `apify auth login` / `apify login`
Expand All @@ -168,7 +169,8 @@ USAGE
FLAGS
-m, --method=<option> Method of logging in to Apify.
<options: console|manual>
-t, --token=<value> Apify API token.
-t, --token=<value> Apify API token to log in with and
save to your OS keyring.
```

##### `apify auth logout` / `apify logout`
Expand All @@ -187,7 +189,8 @@ USAGE

```sh
DESCRIPTION
Prints the current API token for the Apify CLI.
Prints the API token the CLI authenticates with, resolved from APIFY_TOKEN or
the token from 'apify login'.

USAGE
$ apify auth token
Expand Down Expand Up @@ -315,13 +318,14 @@ FLAGS
dependencies.
--skip-git-init Skip initializing a git
repository in the Actor directory.
--source=<option> How the Actor source
code is set up. "apify" keeps everything on this
machine and uploads nothing, so you deploy it with
"apify push". With a Git provider, Apify creates a
private repository on your connected account from
the template, clones it into the Actor directory,
and creates an Actor that builds from it.
--source=<option> Where the Actor source
code lives. With "apify", the code stays on your
machine and nothing is uploaded. You can still
deploy your Actor with "apify push". With a Git
provider, Apify creates a private repository from
the template on your connected account, clones the
repository into the Actor directory, and builds the
Actor from it.
<options: apify|github|gitlab|bitbucket>
-t, --template=<value> Template for the
Actor. If not provided, the command will prompt for
Expand Down Expand Up @@ -1801,7 +1805,7 @@ ARGUMENTS

FLAGS
-t, --token=<value> Apify API token to embed in the config.
Defaults to the token from 'apify login'.
Defaults to APIFY_TOKEN, then the token from 'apify login'.
--tools=<value> Comma-separated tool IDs or Actor full names
to expose. Forwarded as a '?tools=' query parameter.
--url=<value> Apify MCP server URL.
Expand Down
8 changes: 5 additions & 3 deletions skills/apify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ If a command's help shows an "interactive note", it lists exactly which flags ma

See https://apify.com/auth.md for how to authenticate. Do not assume `APIFY_TOKEN` is already set in the environment and use it implicitly — confirm with the user first.

- Persist a session explicitly: `apify login --token <token>`.
- Verify auth: `apify info` (prints the logged-in user; non-zero exit / error if not authenticated).
- Print the stored token: `apify auth token`.
`APIFY_TOKEN` wins over a stored login, for every command. When it is set, `apify login` refuses to run unless `--token` passes the same token — unset `APIFY_TOKEN` first, or skip the login.

- Persist a session explicitly: `apify login --token <token>`. Exits non-zero on a rejected token, so `apify login --token <t> && apify push` is safe to chain.
- Verify auth: `apify info` (prints the username, user ID, and which source the token came from; non-zero exit / error if not authenticated).
- Print the token in use: `apify auth token` (the `APIFY_TOKEN` one when set, otherwise the stored login's).

## Structured output

Expand Down
9 changes: 2 additions & 7 deletions src/commands/actor/charge.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { APIFY_ENV_VARS } from '@apify/consts';

import { getApifyTokenFromEnvOrAuthFile } from '../../lib/actor.js';
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Args } from '../../lib/command-framework/args.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { info } from '../../lib/outputs.js';
import { getLoggedClient } from '../../lib/utils.js';
import { getLoggedClientOrThrow } from '../../lib/utils.js';

/**
* This command can be used to charge for a specific event in the pay-per-event Actor run.
Expand Down Expand Up @@ -86,11 +85,7 @@ export class ActorChargeCommand extends ApifyCommand<typeof ActorChargeCommand>
return;
}

const apifyToken = await getApifyTokenFromEnvOrAuthFile();
const apifyClient = await getLoggedClient(apifyToken);
if (!apifyClient) {
throw new Error('Apify token is not set. Please set it using the environment variable APIFY_TOKEN.');
}
const apifyClient = await getLoggedClientOrThrow();
const runId = process.env[APIFY_ENV_VARS.ACTOR_RUN_ID];

if (!runId) {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/actors/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { runActorOrTaskOnCloud, SharedRunOnCloudFlags } from '../../lib/commands
import { finalizeRun, runUrl } from '../../lib/commands/run-result.js';
import { CommandExitCodes, LOCAL_CONFIG_PATH } from '../../lib/consts.js';
import { error, simpleLog } from '../../lib/outputs.js';
import { getLocalConfig, getLocalUserInfo, getLoggedClientOrThrow, TimestampFormatter } from '../../lib/utils.js';
import { getLocalConfig, getCurrentUserInfo, getLoggedClientOrThrow, TimestampFormatter } from '../../lib/utils.js';

export class ActorsCallCommand extends ApifyCommand<typeof ActorsCallCommand> {
static override name = 'call' as const;
Expand Down Expand Up @@ -102,7 +102,7 @@ export class ActorsCallCommand extends ApifyCommand<typeof ActorsCallCommand> {
const cwd = process.cwd();
const localConfig = getLocalConfig(cwd) || {};
const apifyClient = await getLoggedClientOrThrow();
const userInfo = await getLocalUserInfo();
const userInfo = await getCurrentUserInfo();
const usernameOrId = userInfo.username || (userInfo.id as string);

if (this.flags.json && this.flags.outputDataset) {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/actors/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Flags } from '../../lib/command-framework/flags.js';
import { CommandExitCodes, LOCAL_CONFIG_PATH } from '../../lib/consts.js';
import { useActorConfig } from '../../lib/hooks/useActorConfig.js';
import { error, success } from '../../lib/outputs.js';
import { downloadZip, getLocalUserInfo, getLoggedClientOrThrow } from '../../lib/utils.js';
import { downloadZip, getCurrentUserInfo, getLoggedClientOrThrow } from '../../lib/utils.js';

const extractGitHubZip = async (url: string, directoryPath: string) => {
const zipFile = await downloadZip(url);
Expand Down Expand Up @@ -80,8 +80,8 @@ export class ActorsPullCommand extends ApifyCommand<typeof ActorsPullCommand> {

const { config: actorConfig } = actorConfigResult.unwrap();

const userInfo = await getLocalUserInfo();
const apifyClient = await getLoggedClientOrThrow();
const userInfo = await getCurrentUserInfo();

const isActorAutomaticallyDetected = !this.args.actorId;
const usernameOrId = userInfo.username || userInfo.id;
Expand Down
4 changes: 2 additions & 2 deletions src/commands/actors/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
createActZip,
createSourceFiles,
getActorLocalFilePaths,
getLocalUserInfo,
getCurrentUserInfo,
getLoggedClientOrThrow,
outputJobLog,
parseWaitForFinishMillis,
Expand Down Expand Up @@ -286,7 +286,7 @@ export class ActorsPushCommand extends ApifyCommand<typeof ActorsPushCommand> {

const { config: actorConfig } = actorConfigResult.unwrap();

const userInfo = await getLocalUserInfo();
const userInfo = await getCurrentUserInfo();
const isOrganizationLoggedIn = !!userInfo.organizationOwnerUserId;
const redirectUrlPart = isOrganizationLoggedIn ? `/organization/${userInfo.id}` : '';

Expand Down
7 changes: 3 additions & 4 deletions src/commands/actors/search.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { ApifyClient } from 'apify-client';
import chalk from 'chalk';

import { getAnonymousApifyClientOptions } from '../../lib/auth.js';
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Args } from '../../lib/command-framework/args.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { CompactMode, ResponsiveTable } from '../../lib/commands/responsive-table.js';
import { CommandExitCodes } from '../../lib/consts.js';
import { error, info, simpleLog } from '../../lib/outputs.js';
import { getApifyClientOptions, printJsonToStdout } from '../../lib/utils.js';
import { printJsonToStdout } from '../../lib/utils.js';

const pricingModelLabels: Record<string, string> = {
FREE: 'Free',
Expand Down Expand Up @@ -96,9 +97,7 @@ export class ActorsSearchCommand extends ApifyCommand<typeof ActorsSearchCommand
const { query } = this.args;
const { json, sortBy, category, username, pricingModel, limit, offset } = this.flags;

const clientOptions = await getApifyClientOptions();
delete clientOptions.token;
const client = new ApifyClient(clientOptions);
const client = new ApifyClient(getAnonymousApifyClientOptions());

let result;

Expand Down
4 changes: 2 additions & 2 deletions src/commands/actors/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { runActorOrTaskOnCloud, SharedRunOnCloudFlags } from '../../lib/commands
import { getConsoleUrl } from '../../lib/console-url.js';
import { LOCAL_CONFIG_PATH } from '../../lib/consts.js';
import { simpleLog } from '../../lib/outputs.js';
import { getLocalConfig, getLocalUserInfo, getLoggedClientOrThrow, printJsonToStdout } from '../../lib/utils.js';
import { getLocalConfig, getCurrentUserInfo, getLoggedClientOrThrow, printJsonToStdout } from '../../lib/utils.js';
import { ActorsCallCommand } from './call.js';

export class ActorsStartCommand extends ApifyCommand<typeof ActorsStartCommand> {
Expand Down Expand Up @@ -73,7 +73,7 @@ export class ActorsStartCommand extends ApifyCommand<typeof ActorsStartCommand>
const cwd = process.cwd();
const localConfig = getLocalConfig(cwd) || {};
const apifyClient = await getLoggedClientOrThrow();
const userInfo = await getLocalUserInfo();
const userInfo = await getCurrentUserInfo();
const usernameOrId = userInfo.username || (userInfo.id as string);

const {
Expand Down
32 changes: 24 additions & 8 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import chalk from 'chalk';
import computerName from 'computer-name';
import open from 'open';

import { APIFY_ENV_VARS } from '@apify/consts';
import { cryptoRandomObjectId } from '@apify/utilities';

import { invalidEnvTokenMessage, loginWithToken, readEnvToken } from '../../lib/auth.js';
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { getConsoleIntegrationsUrl, getConsoleUrl } from '../../lib/console-url.js';
import { AUTH_FILE_PATH } from '../../lib/consts.js';
import { AUTH_FILE_PATH, CommandExitCodes } from '../../lib/consts.js';
import { getBackend } from '../../lib/credentials.js';
import { updateUserId } from '../../lib/hooks/telemetry/useTelemetryState.js';
import { useMaskedInput } from '../../lib/hooks/user-confirmations/useMaskedInput.js';
import { useSelectFromList } from '../../lib/hooks/user-confirmations/useSelectFromList.js';
import { createLocalApiServer } from '../../lib/local-api-server.js';
import { error, info, success } from '../../lib/outputs.js';
import { getLocalUserInfo, getLoggedClient, tildify } from '../../lib/utils.js';
import { error, info, success, warning } from '../../lib/outputs.js';
import { tildify } from '../../lib/utils.js';

// When logging in against a local Console instance (local platform development), validate the token
// against the local API rather than production.
Expand All @@ -28,10 +30,10 @@ const API_VERSION = 'v1';

const tryToLogin = async (token: string) => {
const apiBaseUrl = getConsoleUrl().includes('localhost') ? LOCAL_API_BASE_URL : undefined;
const isUserLogged = await getLoggedClient(token, apiBaseUrl);
const userInfo = await getLocalUserInfo();
const result = await loginWithToken(token, apiBaseUrl);

if (isUserLogged) {
if (result) {
const { userInfo } = result;
await updateUserId(userInfo.id!);

const backend = await getBackend();
Expand All @@ -47,11 +49,12 @@ const tryToLogin = async (token: string) => {
message: `You are logged in to Apify as ${userInfo.username || userInfo.id}. ${chalk.gray(`Your token is stored in ${tokenLocation}.`)}`,
});
} else {
process.exitCode = CommandExitCodes.MissingAuth;
error({
message: 'Login to Apify failed, the provided API token is not valid.',
});
}
return isUserLogged;
return result;
};

export class AuthLoginCommand extends ApifyCommand<typeof AuthLoginCommand> {
Expand Down Expand Up @@ -85,7 +88,7 @@ export class AuthLoginCommand extends ApifyCommand<typeof AuthLoginCommand> {
static override flags = {
token: Flags.string({
char: 't',
description: 'Apify API token.',
description: 'Apify API token to log in with and save to your OS keyring.',
required: false,
}),
method: Flags.string({
Expand All @@ -99,6 +102,19 @@ export class AuthLoginCommand extends ApifyCommand<typeof AuthLoginCommand> {
async run() {
const { token, method } = this.flags;

const envToken = readEnvToken();
if (envToken.kind === 'invalid') {
warning({ message: invalidEnvTokenMessage(envToken.raw) });
}

if (envToken.kind === 'token' && envToken.token !== token?.trim()) {
error({
message: `${APIFY_ENV_VARS.TOKEN} is set, so other commands will ignore this login. Unset ${APIFY_ENV_VARS.TOKEN} and try again.`,
});
process.exitCode = CommandExitCodes.InvalidInput;
return;
}

if (token) {
await tryToLogin(token);
return;
Expand Down
14 changes: 13 additions & 1 deletion src/commands/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { APIFY_ENV_VARS } from '@apify/consts';

import { invalidEnvTokenMessage, readEnvToken } from '../../lib/auth.js';
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { AUTH_FILE_PATH } from '../../lib/consts.js';
import { clearKeyringSecrets } from '../../lib/credentials.js';
import { rimrafPromised } from '../../lib/files.js';
import { updateUserId } from '../../lib/hooks/telemetry/useTelemetryState.js';
import { success } from '../../lib/outputs.js';
import { success, warning } from '../../lib/outputs.js';
import { tildify } from '../../lib/utils.js';

export class AuthLogoutCommand extends ApifyCommand<typeof AuthLogoutCommand> {
Expand Down Expand Up @@ -31,5 +34,14 @@ export class AuthLogoutCommand extends ApifyCommand<typeof AuthLogoutCommand> {
await updateUserId(null);

success({ message: 'You are logged out from your Apify account.' });

const envToken = readEnvToken();
if (envToken.kind === 'token') {
warning({
message: `${APIFY_ENV_VARS.TOKEN} is still set, so commands stay authenticated with that token.`,
});
} else if (envToken.kind === 'invalid') {
warning({ message: invalidEnvTokenMessage(envToken.raw) });
}
}
}
13 changes: 6 additions & 7 deletions src/commands/auth/token.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { resolveAuth } from '../../lib/auth.js';
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { simpleLog } from '../../lib/outputs.js';
import { getLocalUserInfo, getLoggedClientOrThrow } from '../../lib/utils.js';
import { getLoggedClientOrThrow } from '../../lib/utils.js';

export class AuthTokenCommand extends ApifyCommand<typeof AuthTokenCommand> {
static override name = 'token' as const;

static override description = 'Prints the current API token for the Apify CLI.';
static override description = `Prints the API token the CLI authenticates with, resolved from APIFY_TOKEN or the token from 'apify login'.`;

static override examples = [
{
description: 'Print the stored API token to stdout (use with care — it is a secret).',
description: `Print the resolved API token to stdout. Be careful with the output, it's a secret.`,
command: 'apify auth token',
},
];
Expand All @@ -18,10 +19,8 @@ export class AuthTokenCommand extends ApifyCommand<typeof AuthTokenCommand> {

async run() {
await getLoggedClientOrThrow();
const userInfo = await getLocalUserInfo();
const auth = await resolveAuth();

if (userInfo.token) {
simpleLog({ message: userInfo.token, stdout: true });
}
simpleLog({ message: auth!.token, stdout: true });
}
}
5 changes: 2 additions & 3 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { LANGUAGE_FLAG_CHOICES, USE_CASE_FLAG_CHOICES } from '../lib/templates/c
import {
downloadAndUnzip,
getJsonFileContent,
getLocalUserInfo,
getCurrentUserInfo,
getLoggedClientOrThrow,
isNodeVersionSupported,
isPythonVersionSupported,
Expand Down Expand Up @@ -318,8 +318,7 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
? {
provider: gitProvider,
client: await getLoggedClientOrThrow(),
// Read after the client, which refreshes auth.json from the token the run resolved.
account: toGitAccount(await getLocalUserInfo()),
account: toGitAccount(await getCurrentUserInfo()),
// Omitted means on: the webhook is what makes a Git-sourced Actor rebuild on a push.
autoBuild: this.flags.autoBuild !== 'off',
...parseGitRepoFlag(gitRepo, actorName),
Expand Down
4 changes: 2 additions & 2 deletions src/commands/datasets/get-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Args } from '../../lib/command-framework/args.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { error, simpleLog } from '../../lib/outputs.js';
import { getLocalUserInfo, getLoggedClientOrThrow } from '../../lib/utils.js';
import { getCurrentUserInfo, getLoggedClientOrThrow } from '../../lib/utils.js';

const downloadFormatToContentType: Record<DownloadItemsFormat, string> = {
[DownloadItemsFormat.JSON]: 'application/json',
Expand Down Expand Up @@ -106,7 +106,7 @@ export class DatasetsGetItems extends ApifyCommand<typeof DatasetsGetItems> {
};
}

const info = await getLocalUserInfo();
const info = await getCurrentUserInfo();

const byName = await client
.dataset(`${info.username!}/${datasetId}`)
Expand Down
Loading
Loading