Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
3cc0154
feat: auth.json v2 with profiles keyed by user ID
l2ysho Sep 11, 2026
f9a75de
test: update the API auth tests to the v2 file shape
l2ysho Sep 17, 2026
240387c
fix: keep the v1 backup readable only by the owner
l2ysho Sep 17, 2026
0452abf
fix: keep secrets out of the v1 backup
l2ysho Sep 17, 2026
4970bbc
test: skip the backup mode check on Windows
l2ysho Sep 23, 2026
7a09437
refactor: make the auth file migration a version step chain
l2ysho Sep 23, 2026
dc0d1b9
refactor: name the login writer for what it does
l2ysho Sep 23, 2026
2733ea8
refactor: drop the catch-all index signature from AuthFile
l2ysho Sep 23, 2026
47fdf53
fix: stop a newer auth file from blocking APIFY_TOKEN and logout
l2ysho Sep 23, 2026
8d66a2b
docs: trim the comments this branch added
l2ysho Sep 23, 2026
36a7004
fix: say the login still works when the migration cannot write
l2ysho Sep 23, 2026
fc89854
Merge branch 'master' into claude/auth-json-v2-1419
l2ysho Sep 24, 2026
6c73f45
docs: fix the stale and padded comments in auth-file
l2ysho Sep 24, 2026
fbe7744
feat: record loggedInAt, and drop the v1 snapshot on login
l2ysho Sep 24, 2026
9379ff5
docs: drop the loggedInAt docblock
l2ysho Sep 24, 2026
4749cd7
feat: key secrets by user ID on both backends
l2ysho Sep 24, 2026
85ac3fd
feat: let a profile record its own secrets backend
l2ysho Sep 24, 2026
819e533
feat: make token login additive across accounts
l2ysho Sep 24, 2026
ac5a596
fix: clear keyring entries only once the file write has succeeded
l2ysho Sep 24, 2026
d949e0a
fix: say the stored login cannot be read when the migration fails
l2ysho Sep 24, 2026
a1511d0
fix: point the migration failure at the directory, not the file
l2ysho Sep 24, 2026
a81d95f
Merge remote-tracking branch 'origin/claude/secret-storage-v2-1420' i…
l2ysho Sep 24, 2026
1f9f395
feat: add --profile, auth switch, auth list and per-account logout
l2ysho Sep 24, 2026
6262989
feat: give create the --profile flag for Git-source runs
l2ysho Sep 24, 2026
998cb61
fix: say logout found nothing to remove instead of reporting success
l2ysho Sep 25, 2026
cf226d6
refactor: share the no-accounts message across auth list, switch and …
l2ysho Sep 25, 2026
176451c
Merge branch 'master' into claude/auth-json-v2-1419
l2ysho Sep 28, 2026
100c39c
refactor: drop the secrets backend markers from auth.json
l2ysho Sep 29, 2026
af4f5a3
Merge branch 'master' into claude/auth-json-v2-1419
l2ysho Sep 30, 2026
b081602
Merge branch 'claude/auth-json-v2-1419' into claude/secret-storage-v2…
l2ysho Sep 30, 2026
82eebf3
fix: stop the secret migration overwriting a newer login
l2ysho Sep 30, 2026
201a958
refactor: name the credential migration order
l2ysho Sep 30, 2026
ffb3181
fix: clear the old keyring names on every login
l2ysho Sep 30, 2026
a6c17e1
fix: report keyring deletes that logout could not make
l2ysho Sep 30, 2026
c8e41d4
fix: point the API tests at the per-user secret helpers
l2ysho Sep 30, 2026
ad2ced9
fix: move the other secrets with a token that falls back to the file
l2ysho Sep 30, 2026
0ba785f
fix: report only the keyring secrets a delete left behind
l2ysho Sep 30, 2026
e554867
fix: stop one account claiming another account keyring secret
l2ysho Sep 30, 2026
b14fbd1
perf: read the keyed token only when a fixed name holds something
l2ysho Sep 30, 2026
49d9073
fix: name the keyring entries a logout or login left behind
l2ysho Sep 30, 2026
74bfe7f
fix: report a proxy password the keyring would not give up
l2ysho Oct 1, 2026
7135a76
docs: correct three comments and cut the rest back
l2ysho Oct 1, 2026
6b95b05
test: keep the suite off the real OS keyring
l2ysho Oct 1, 2026
0f73807
Merge remote-tracking branch 'origin/claude/secret-storage-v2-1420' i…
l2ysho Oct 1, 2026
858a150
Merge branch 'master' into claude/auth-json-v2-1419
l2ysho Oct 1, 2026
4083caa
Merge remote-tracking branch 'origin/claude/multi-account-login-1386'…
l2ysho Oct 1, 2026
9365abf
fix: address review findings on the multi-account commands
l2ysho Oct 1, 2026
b73f37c
Merge branch 'claude/auth-json-v2-1419' into claude/secret-storage-v2…
l2ysho Oct 2, 2026
f2bcced
Merge branch 'claude/secret-storage-v2-1420' into claude/multi-accoun…
l2ysho Oct 2, 2026
e13fb5d
fix: keep auth list off the OS keyring
l2ysho Oct 2, 2026
085c12f
refactor: drop the profile row from info
l2ysho Oct 2, 2026
abf7ecd
fix: keep logout --profile from suggesting deleting every account
l2ysho Oct 2, 2026
4ae7779
Merge branch 'claude/multi-account-login-1386' into claude/multi-acco…
l2ysho Oct 2, 2026
c4c8389
Merge remote-tracking branch 'origin/master' into claude/secret-stora…
l2ysho Oct 5, 2026
6d69eba
Merge remote-tracking branch 'origin/claude/secret-storage-v2-1420' i…
l2ysho Oct 5, 2026
852fbfb
Merge branch 'claude/multi-account-login-1386' into claude/multi-acco…
l2ysho Oct 8, 2026
c9e2513
Merge branch 'master' into claude/secret-storage-v2-1420
l2ysho Oct 8, 2026
e33d679
Merge commit 'c9e2513f' into claude/multi-account-login-1386
l2ysho Oct 8, 2026
97ca708
Merge remote-tracking branch 'origin/master' into claude/multi-accoun…
l2ysho Oct 8, 2026
6399174
Merge branch 'claude/multi-account-login-1386' into claude/multi-acco…
l2ysho Oct 8, 2026
d3af10d
Merge remote-tracking branch 'origin/master' into claude/multi-accoun…
l2ysho Oct 8, 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
391 changes: 284 additions & 107 deletions docs/reference.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions scripts/generate-cli-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const categories: Record<string, CommandsInCategory[]> = {
{ command: Commands.auth },
{ command: Commands.authLogin, aliases: [Commands.login] },
{ command: Commands.authLogout, aliases: [Commands.logout] },
{ command: Commands.authList },
{ command: Commands.authSwitch },
{ command: Commands.authToken },
{ command: Commands.info },
{ command: Commands.secrets },
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actors/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { getLocalConfig, getCurrentUserInfo, getLoggedClientOrThrow, TimestampFo
export class ActorsCallCommand extends ApifyCommand<typeof ActorsCallCommand> {
static override name = 'call' as const;

static override enableProfileFlag = true;

static override description =
'Executes Actor remotely using your authenticated account.\n' +
'Reads input from local key-value store by default.\n' +
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actors/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const payPerEventTable = new ResponsiveTable({
export class ActorsInfoCommand extends ApifyCommand<typeof ActorsInfoCommand> {
static override name = 'info' as const;

static override enableProfileFlag = true;

static override description = 'Get information about an Actor.';

static override examples = [
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actors/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ interface HydratedListData {
export class ActorsLsCommand extends ApifyCommand<typeof ActorsLsCommand> {
static override name = 'ls' as const;

static override enableProfileFlag = true;

static override description = 'Prints a list of recently executed Actors or Actors you own.';

static override examples = [
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actors/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const extractGitHubZip = async (url: string, directoryPath: string) => {
export class ActorsPullCommand extends ApifyCommand<typeof ActorsPullCommand> {
static override name = 'pull' as const;

static override enableProfileFlag = true;

static override description =
'Download Actor code to current directory. ' +
'Clones Git repositories or fetches Actor files based on the source type.';
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actors/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ const confirmGitSourceSwitch = async ({
export class ActorsPushCommand extends ApifyCommand<typeof ActorsPushCommand> {
static override name = 'push' as const;

static override enableProfileFlag = true;

static override description =
`Deploys Actor to Apify platform using settings from '${LOCAL_CONFIG_PATH}'.\n` +
`Files under '${MAX_MULTIFILE_BYTES / 1024 ** 2}' MB upload as "Multiple source files"; ` +
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actors/rm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { getLoggedClientOrThrow } from '../../lib/utils.js';
export class ActorsRmCommand extends ApifyCommand<typeof ActorsRmCommand> {
static override name = 'rm' as const;

static override enableProfileFlag = true;

static override description = 'Permanently removes an Actor from your account.';

static override interactive = true;
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actors/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { ActorsCallCommand } from './call.js';
export class ActorsStartCommand extends ApifyCommand<typeof ActorsStartCommand> {
static override name = 'start' as const;

static override enableProfileFlag = true;

static override description =
'Starts Actor remotely and returns run details immediately.\n' +
'Uses authenticated account and local key-value store for input.';
Expand Down
2 changes: 2 additions & 0 deletions src/commands/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export class ApiCommand extends ApifyCommand<typeof ApiCommand> {

static override name = 'api' as const;

static override enableProfileFlag = true;

static override description =
'Makes an authenticated HTTP request to the Apify API and prints the response.\n' +
'The endpoint can be a relative path (e.g. "acts", "v2/acts", or "/v2/acts"); ' +
Expand Down
12 changes: 10 additions & 2 deletions src/commands/auth/_index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { AuthListCommand } from './list.js';
import { AuthLoginCommand } from './login.js';
import { AuthLogoutCommand } from './logout.js';
import { AuthSwitchCommand } from './switch.js';
import { AuthTokenCommand } from './token.js';

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

static override description =
'Log in, log out, and inspect your stored Apify API token. Also available as `apify login` / `apify logout`.';
'Log in, log out, switch between stored accounts, and inspect your stored Apify API token. Also available as `apify login` / `apify logout`.';

static override group = 'Authentication';

static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-auth';

static override subcommands = [AuthLoginCommand, AuthLogoutCommand, AuthTokenCommand];
static override subcommands = [
AuthLoginCommand,
AuthLogoutCommand,
AuthListCommand,
AuthSwitchCommand,
AuthTokenCommand,
];

async run() {
this.printHelp();
Expand Down
110 changes: 110 additions & 0 deletions src/commands/auth/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import chalk from 'chalk';

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

import {
assertSupportedAuthFileVersion,
getActiveProfileId,
listProfiles,
profileLabel,
readAuthFile,
} from '../../lib/auth-file.js';
import { invalidEnvTokenMessage, NO_STORED_ACCOUNTS_MESSAGE, readEnvToken } from '../../lib/auth.js';
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { CompactMode, ResponsiveTable } from '../../lib/commands/responsive-table.js';
import { getBackend } from '../../lib/credentials.js';
import { simpleLog, warning } from '../../lib/outputs.js';
import { printJsonToStdout, TimestampFormatter } from '../../lib/utils.js';

const STORAGE_LABELS = { keyring: 'OS keyring', file: 'auth.json', none: chalk.gray('None') };

const table = new ResponsiveTable({
allColumns: ['Name', 'User ID', 'Type', 'Last login', 'Storage'],
mandatoryColumns: ['Name', 'User ID'],
});

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

static override aliases = ['ls'];

static override description =
'Lists the stored Apify accounts and marks the active one. Reads only the local login, so it works offline and does not check that the tokens are still valid.';

static override enableJsonFlag = true;

static override group = 'Authentication';

static override examples = [
{
description: 'List the stored accounts.',
command: 'apify auth list',
},
{
description: 'List the stored accounts as JSON, for scripts.',
command: 'apify auth list --json',
},
];

static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-auth-list';

async run() {
// No credential migration: it reads the OS keyring, and listing must not prompt for it.
assertSupportedAuthFileVersion();

const file = readAuthFile();
const activeId = getActiveProfileId();
const envToken = readEnvToken();
// Loads the keyring module without reading from it, so listing never prompts for the keychain.
const keyringBackend = await getBackend();

const profiles = listProfiles().map((profile) => ({
id: profile.id,
name: profileLabel(profile),
username: profile.username ?? null,
active: profile.id === activeId,
isOrganization: Boolean(profile.organizationOwnerUserId),
organizationOwnerUserId: profile.organizationOwnerUserId ?? null,
loggedInAt: profile.loggedInAt,
// Same rule as `backendFor`: a token in the file wins, and without the keyring nothing else holds one.
secretsBackend:
(profile.token ?? (profile.id === activeId ? file.token : undefined)) !== undefined
? ('file' as const)
: keyringBackend === 'keyring'
? keyringBackend
: null,
}));

if (this.flags.json) {
printJsonToStdout({ envTokenInUse: envToken.kind !== 'unset', profiles });
return;
}

if (envToken.kind === 'token') {
warning({
message: `${APIFY_ENV_VARS.TOKEN} is set, so commands use it instead of the active account.`,
});
} else if (envToken.kind === 'invalid') {
warning({ message: invalidEnvTokenMessage(envToken.raw) });
}

if (!profiles.length) {
simpleLog({ message: NO_STORED_ACCOUNTS_MESSAGE, stdout: true });
return;
}

for (const profile of profiles) {
table.pushRow({
Name: profile.active ? `${chalk.bold(profile.name)} ${chalk.green('(active)')}` : profile.name,
'User ID': chalk.gray(profile.id),
Type: profile.isOrganization ? 'Organization' : 'Personal',
'Last login': profile.loggedInAt
? TimestampFormatter.display(new Date(profile.loggedInAt))
: chalk.gray('Unknown'),
Storage: STORAGE_LABELS[profile.secretsBackend ?? 'none'],
});
}

simpleLog({ message: table.render(CompactMode.WebLikeCompact), stdout: true });
}
}
Loading
Loading