From fa0ad3d98fbc73bd284309e208061279d448fdbe Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Mon, 18 May 2026 11:38:50 -0500 Subject: [PATCH 1/6] fix: remove secrets from org display --- src/commands/org/display.ts | 46 +++++++++++++++++++++++++--- test/unit/org/display.test.ts | 57 +++++++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/src/commands/org/display.ts b/src/commands/org/display.ts index 8f370a2d..40f90eb4 100644 --- a/src/commands/org/display.ts +++ b/src/commands/org/display.ts @@ -21,7 +21,7 @@ import { loglevel, orgApiVersionFlagWithDeprecations, } from '@salesforce/sf-plugins-core'; -import { AuthInfo, Messages, Org, SfError, trimTo15 } from '@salesforce/core'; +import { AuthInfo, envVars, Messages, Org, SfError, trimTo15 } from '@salesforce/core'; import { camelCaseToTitleCase } from '@salesforce/kit'; import { AuthFieldsFromFS, OrgDisplayReturn, ScratchOrgFields } from '../../shared/orgTypes.js'; import { getAliasByUsername } from '../../shared/utils.js'; @@ -54,12 +54,28 @@ export class OrgDisplayCommand extends SfCommand { this.org = flags['target-org']; this.org.getConnection(flags['api-version']); try { + // TODO: Once env var is removed, this refresh could be removed. // the auth file might have a stale access token. We want to refresh it before getting the fields await this.org.refreshAuth(); } catch (error) { // even if this fails, we want to display the information we can read from the auth file this.warn('unable to refresh auth for org'); } + + // New command names + const accessTokenCommand = 'sf org auth show-access-token'; + const sfdxAuthUrlCommand = 'sf org auth show-sfdx-auth-url'; + const userPasswordCommand = 'sf org auth show-user-password'; + + const accessTokenRedactedMessage = `[REDACTED] Use '${accessTokenCommand}' to view`; + const sfdxAuthUrlRedactedMessage = `[REDACTED] Use '${sfdxAuthUrlCommand}' to view`; + const passwordRedactedMessage = `[REDACTED] Use '${userPasswordCommand}' to view`; + + const SHOW_TOKENS_ENV = 'SF_TEMP_SHOW_SECRETS'; + const envVarAsATempWorkaroundMessage = `Secrets are now hidden from 'sf org display' output. Use the 'sf org auth show-*' commands instead. As a temporary workaround, you can set ${SHOW_TOKENS_ENV}=true to render these secrets. This workaround will be removed in an upcoming release`; + const showSecretsEnvVarIsSet = envVars.getBoolean(SHOW_TOKENS_ENV, false); + const envVarIsSetWarning = `The ${SHOW_TOKENS_ENV} env var is set. This is a temporary env var to continue to show secrets in the 'sf org display' command output. This workaround will be removed in an upcoming CLI release. Switch to use the 'sf org auth show-*' commands to avoid future disruption.`; + // translate to alias if necessary const authInfo = await AuthInfo.create({ username: this.org.getUsername() }); const fields = authInfo.getFields(true) as AuthFieldsFromFS; @@ -67,6 +83,14 @@ export class OrgDisplayCommand extends SfCommand { const isScratchOrg = Boolean(fields.devHubUsername); const scratchOrgInfo = isScratchOrg && fields.orgId ? await this.getScratchOrgInformation(fields) : {}; + const getSfdxAuthUrlOutput = (): string | undefined => { + if (flags.verbose && fields.refreshToken) { + // TODO: Remove env var workaround + return showSecretsEnvVarIsSet ? authInfo.getSfdxAuthUrl() : sfdxAuthUrlRedactedMessage; + } + return undefined; + }; + const returnValue: OrgDisplayReturn = { // renamed properties id: fields.orgId, @@ -74,22 +98,34 @@ export class OrgDisplayCommand extends SfCommand { // copied properties apiVersion: fields.instanceApiVersion, - accessToken: fields.accessToken, + // Access token will always exist, don't check for value first + // TODO: Remove env var workaround + accessToken: showSecretsEnvVarIsSet ? fields.accessToken : accessTokenRedactedMessage, instanceUrl: fields.instanceUrl, username: fields.username, clientId: fields.clientId, - password: fields.password, + // Password only exists if it was generated locally, check for value first + // TODO: Remove env var workaround + password: fields.password ? (showSecretsEnvVarIsSet ? fields.password : passwordRedactedMessage) : undefined, ...scratchOrgInfo, // properties with more complex logic connectedStatus: isScratchOrg ? undefined : await OrgListUtil.determineConnectedStatusForNonScratchOrg(fields.username), - sfdxAuthUrl: flags.verbose && fields.refreshToken ? authInfo.getSfdxAuthUrl() : undefined, + sfdxAuthUrl: getSfdxAuthUrlOutput(), alias: await getAliasByUsername(fields.username), + // TODO: deprecate these values clientApps: fields.clientApps ? Object.keys(fields.clientApps).join(',') : undefined, }; - this.warn(sharedMessages.getMessage('SecurityWarning')); + + if (showSecretsEnvVarIsSet) { + this.warn(envVarIsSetWarning); + this.warn(sharedMessages.getMessage('SecurityWarning')); + } else { + this.warn(envVarAsATempWorkaroundMessage); + } + this.print(returnValue); return returnValue; } diff --git a/test/unit/org/display.test.ts b/test/unit/org/display.test.ts index 7614d84b..ea78af24 100644 --- a/test/unit/org/display.test.ts +++ b/test/unit/org/display.test.ts @@ -37,7 +37,7 @@ describe('org:display', () => { const commonAssert = (result: OrgDisplayReturn) => { expect(result).to.have.property('username', testOrg.username); expect(result).to.have.property('id', testOrg.orgId); - expect(result).to.have.property('accessToken', testOrg.accessToken); + expect(result.accessToken).to.include('[REDACTED]'); expect(result).to.have.property('instanceUrl', testOrg.instanceUrl); expect(result).to.have.property('clientId', testOrg.clientId); }; @@ -63,9 +63,13 @@ describe('org:display', () => { value: testOrg.clientId, }); + const accessToken = data.find((row) => row.key === 'Access Token'); + expect(accessToken).to.exist; + expect(accessToken?.value).to.include('[REDACTED]'); + const authUrl = data.find((row) => row.key === 'Sfdx Auth Url'); expect(authUrl).to.exist; - expect(authUrl?.value).to.include(testOrg.clientId); + expect(authUrl?.value).to.include('[REDACTED]'); }); it('includes correct rows in non-json (table) mode', async () => { @@ -186,12 +190,13 @@ describe('org:display', () => { expect(result.alias).to.equal('nonscratchalias'); }); - it('displays authUrl when using refresh token AND verbose', async () => { + it('redacts authUrl when using refresh token AND verbose', async () => { testOrg.refreshToken = refreshToken; await $$.stubAuths(testOrg); const result = await OrgDisplayCommand.run(['--json', '--targetusername', testOrg.username, '--verbose']); - expect(result.sfdxAuthUrl).to.include(testOrg.refreshToken); + expect(result.sfdxAuthUrl).to.include('[REDACTED]'); + expect(result.sfdxAuthUrl).to.include('show-sfdx-auth-url'); }); it('omits authUrl when not using refresh token, despite verbose', async () => { @@ -220,13 +225,14 @@ describe('org:display', () => { expect(result.alias).to.be.undefined; }); - it('displays decrypted password if password exists', async () => { + it('redacts password if password exists', async () => { testOrg.password = 'encrypted'; await $$.stubAuths(testOrg); const result = await OrgDisplayCommand.run(['--json', '--targetusername', testOrg.username]); expect(commonAssert(result)); - expect(result.password).to.equal('encrypted'); + expect(result.password).to.include('[REDACTED]'); + expect(result.password).to.include('show-user-password'); }); it('queries server for scratch org info', async () => { @@ -261,4 +267,43 @@ describe('org:display', () => { it('displays good error when org is not connectable due to DNS'); it('displays scratch-org-only properties for scratch orgs'); it('displays no scratch-org-only properties for non-scratch orgs'); + + describe('secret redaction WITH env var (SF_TEMP_SHOW_SECRETS)', () => { + const SHOW_TOKENS_ENV = 'SF_TEMP_SHOW_SECRETS'; + + beforeEach(() => { + process.env[SHOW_TOKENS_ENV] = 'true'; + }); + + afterEach(() => { + delete process.env[SHOW_TOKENS_ENV]; + }); + + it('shows the real access token', async () => { + await $$.stubAuths(testOrg); + const result = await OrgDisplayCommand.run(['--json', '--targetusername', testOrg.username]); + expect(result.accessToken).to.equal(testOrg.accessToken); + }); + + it('shows the real sfdx auth url when --verbose and refresh token exist', async () => { + testOrg.refreshToken = refreshToken; + await $$.stubAuths(testOrg); + const result = await OrgDisplayCommand.run(['--json', '--targetusername', testOrg.username, '--verbose']); + expect(result.sfdxAuthUrl).to.include(refreshToken); + }); + + it('shows the real password when one exists', async () => { + testOrg.password = 'somepassword'; + await $$.stubAuths(testOrg); + const result = await OrgDisplayCommand.run(['--json', '--targetusername', testOrg.username]); + expect(result.password).to.equal('somepassword'); + }); + + it('emits the deprecation warning', async () => { + await $$.stubAuths(testOrg); + await OrgDisplayCommand.run(['--targetusername', testOrg.username]); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('will be removed'))).to.be.true; + }); + }); }); From 8f0a31b65eb77343a37aad6860f9d2a5ecdad991 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Mon, 18 May 2026 21:26:58 -0500 Subject: [PATCH 2/6] fix: remove sensitive values from output --- messages/secrets-redacted.md | 19 ++++++++++++ src/commands/org/create/scratch.ts | 35 +++++++++++++++++++++-- src/commands/org/display.ts | 21 ++++++-------- src/commands/org/list.ts | 24 +++++++++++++++- src/commands/org/resume/scratch.ts | 35 ++++++++++++++++++++++- src/shared/orgListUtil.ts | 32 ++++++++++++++++----- test/shared/orgListUtil.test.ts | 40 ++++++++++++++++++++++++++ test/unit/org/display.test.ts | 11 ++++++- test/unit/org/list.test.ts | 46 +++++++++++++++++++++++++++++- 9 files changed, 237 insertions(+), 26 deletions(-) create mode 100644 messages/secrets-redacted.md diff --git a/messages/secrets-redacted.md b/messages/secrets-redacted.md new file mode 100644 index 00000000..62bb08ab --- /dev/null +++ b/messages/secrets-redacted.md @@ -0,0 +1,19 @@ +# redacted.accessToken + +[REDACTED] Use 'sf org auth show-access-token' to view + +# redacted.sfdxAuthUrl + +[REDACTED] Use 'sf org auth show-sfdx-auth-url' to view + +# redacted.userPassword + +[REDACTED] Use 'sf org auth show-user-password' to view + +# temp.envVarWorkaround + +Secrets are now hidden from '%s' command output. Use the 'sf org auth show-\*' commands instead. As a temporary workaround, you can set SF_TEMP_SHOW_SECRETS=true to render these secrets. This workaround will be removed in an upcoming release. + +# temp.envVarIsSet + +The SF_TEMP_SHOW_SECRETS env var is set. This is a temporary env var to continue to show secrets in the '%s' command output. This workaround will be removed in an upcoming CLI release. Switch to use the 'sf org auth show-\*' commands to avoid future disruption. diff --git a/src/commands/org/create/scratch.ts b/src/commands/org/create/scratch.ts index 4e32af56..99170494 100644 --- a/src/commands/org/create/scratch.ts +++ b/src/commands/org/create/scratch.ts @@ -16,6 +16,7 @@ import { MultiStageOutput } from '@oclif/multi-stage-output'; import { + envVars, Lifecycle, Messages, Org, @@ -26,7 +27,7 @@ import { SfError, } from '@salesforce/core'; import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; -import { Duration } from '@salesforce/kit'; +import { Duration, omit } from '@salesforce/kit'; import terminalLink from 'terminal-link'; import { capitalCase } from 'change-case'; import { buildScratchOrgRequest } from '../../../shared/scratchOrgRequest.js'; @@ -34,6 +35,7 @@ import { ScratchCreateResponse } from '../../../shared/orgTypes.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'create_scratch'); +const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted'); const definitionFileHelpGroupName = 'Definition File Override'; @@ -251,7 +253,36 @@ export default class OrgCreateScratch extends SfCommand { this.logSuccess(messages.getMessage('success')); } - return { username, scratchOrgInfo, authFields, warnings, orgId: authFields?.orgId }; + // TODO: Remove env var workaround + const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false); + const accessTokenRedacted = secretsMessages.getMessage('redacted.accessToken'); + + const redactedAuthFields = authFields + ? { + ...authFields, + accessToken: showSecretsEnvVarIsSet ? authFields.accessToken : accessTokenRedacted, + refreshToken: undefined, + clientSecret: undefined, + } + : undefined; + + const redactedScratchOrgInfo = omit(scratchOrgInfo, ['AuthCode']); + + if (this.jsonEnabled()) { + if (showSecretsEnvVarIsSet) { + this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org create scratch --json'])); + } else { + this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org create scratch --json'])); + } + } + + return { + username, + scratchOrgInfo: redactedScratchOrgInfo, + authFields: redactedAuthFields, + warnings, + orgId: authFields?.orgId, + }; } catch (error) { mso.error(); if (error instanceof SfError && error.name === 'ScratchOrgInfoTimeoutError') { diff --git a/src/commands/org/display.ts b/src/commands/org/display.ts index 40f90eb4..c518c703 100644 --- a/src/commands/org/display.ts +++ b/src/commands/org/display.ts @@ -31,6 +31,8 @@ import { OrgListUtil } from '../../shared/orgListUtil.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'display'); const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); +const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted'); + export class OrgDisplayCommand extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); @@ -62,19 +64,13 @@ export class OrgDisplayCommand extends SfCommand { this.warn('unable to refresh auth for org'); } - // New command names - const accessTokenCommand = 'sf org auth show-access-token'; - const sfdxAuthUrlCommand = 'sf org auth show-sfdx-auth-url'; - const userPasswordCommand = 'sf org auth show-user-password'; - - const accessTokenRedactedMessage = `[REDACTED] Use '${accessTokenCommand}' to view`; - const sfdxAuthUrlRedactedMessage = `[REDACTED] Use '${sfdxAuthUrlCommand}' to view`; - const passwordRedactedMessage = `[REDACTED] Use '${userPasswordCommand}' to view`; + const accessTokenRedactedMessage = secretsMessages.getMessage('redacted.accessToken'); + const sfdxAuthUrlRedactedMessage = secretsMessages.getMessage('redacted.sfdxAuthUrl'); + const passwordRedactedMessage = secretsMessages.getMessage('redacted.userPassword'); - const SHOW_TOKENS_ENV = 'SF_TEMP_SHOW_SECRETS'; - const envVarAsATempWorkaroundMessage = `Secrets are now hidden from 'sf org display' output. Use the 'sf org auth show-*' commands instead. As a temporary workaround, you can set ${SHOW_TOKENS_ENV}=true to render these secrets. This workaround will be removed in an upcoming release`; - const showSecretsEnvVarIsSet = envVars.getBoolean(SHOW_TOKENS_ENV, false); - const envVarIsSetWarning = `The ${SHOW_TOKENS_ENV} env var is set. This is a temporary env var to continue to show secrets in the 'sf org display' command output. This workaround will be removed in an upcoming CLI release. Switch to use the 'sf org auth show-*' commands to avoid future disruption.`; + const envVarAsATempWorkaroundMessage = secretsMessages.getMessage('temp.envVarWorkaround', ['sf org display']); + const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false); + const envVarIsSetWarning = secretsMessages.getMessage('temp.envVarIsSet', ['sf org display']); // translate to alias if necessary const authInfo = await AuthInfo.create({ username: this.org.getUsername() }); @@ -108,7 +104,6 @@ export class OrgDisplayCommand extends SfCommand { // TODO: Remove env var workaround password: fields.password ? (showSecretsEnvVarIsSet ? fields.password : passwordRedactedMessage) : undefined, ...scratchOrgInfo, - // properties with more complex logic connectedStatus: isScratchOrg ? undefined diff --git a/src/commands/org/list.ts b/src/commands/org/list.ts index ae5c4e02..e58669ee 100644 --- a/src/commands/org/list.ts +++ b/src/commands/org/list.ts @@ -16,7 +16,17 @@ import { EOL } from 'node:os'; import { Flags, loglevel, SfCommand } from '@salesforce/sf-plugins-core'; -import { AuthInfo, ConfigAggregator, ConfigInfo, Connection, Org, SfError, Messages, Logger } from '@salesforce/core'; +import { + AuthInfo, + ConfigAggregator, + ConfigInfo, + Connection, + envVars, + Org, + SfError, + Messages, + Logger, +} from '@salesforce/core'; import { Interfaces } from '@oclif/core'; import ansis, { type Ansis } from 'ansis'; import { OrgListUtil, identifyActiveOrgByStatus } from '../../shared/orgListUtil.js'; @@ -25,6 +35,7 @@ import { ExtendedAuthFields, FullyPopulatedScratchOrgFields } from '../../shared Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'list'); +const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted'); export const defaultOrgEmoji = '🍁'; export const defaultHubEmoji = '🌳'; @@ -126,6 +137,17 @@ Legend: ${defaultHubEmoji}=Default DevHub, ${defaultOrgEmoji}=Default Org ${ }` ); + // TODO: Remove after env var workaround is removed + const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false); + + if (this.jsonEnabled()) { + if (showSecretsEnvVarIsSet) { + this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org list'])); + } else { + this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org list'])); + } + } + return result; } diff --git a/src/commands/org/resume/scratch.ts b/src/commands/org/resume/scratch.ts index 82d81290..beb66989 100644 --- a/src/commands/org/resume/scratch.ts +++ b/src/commands/org/resume/scratch.ts @@ -18,6 +18,7 @@ import { strict as assert } from 'node:assert'; import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; import { + envVars, Lifecycle, Messages, ScratchOrgCache, @@ -30,10 +31,12 @@ import { import terminalLink from 'terminal-link'; import { MultiStageOutput } from '@oclif/multi-stage-output'; import { capitalCase } from 'change-case'; +import { omit } from '@salesforce/kit'; import { ScratchCreateResponse } from '../../../shared/orgTypes.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'resume_scratch'); +const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted'); export default class OrgResumeScratch extends SfCommand { public static readonly summary = messages.getMessage('summary'); @@ -127,7 +130,37 @@ export default class OrgResumeScratch extends SfCommand { this.log(); this.logSuccess(messages.getMessage('success')); - return { username, scratchOrgInfo, authFields, warnings, orgId: authFields?.orgId }; + + // TODO: Remove env var workaround + const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false); + const accessTokenRedacted = secretsMessages.getMessage('redacted.accessToken'); + + const redactedAuthFields = authFields + ? { + ...authFields, + accessToken: showSecretsEnvVarIsSet ? authFields.accessToken : accessTokenRedacted, + refreshToken: undefined, + clientSecret: undefined, + } + : undefined; + + const redactedScratchOrgInfo = scratchOrgInfo ? omit(scratchOrgInfo, ['AuthCode']) : undefined; + + if (this.jsonEnabled()) { + if (showSecretsEnvVarIsSet) { + this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org resume scratch --json'])); + } else { + this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org resume scratch --json'])); + } + } + + return { + username, + scratchOrgInfo: redactedScratchOrgInfo, + authFields: redactedAuthFields, + warnings, + orgId: authFields?.orgId, + }; } catch (e) { mso.error(); diff --git a/src/shared/orgListUtil.ts b/src/shared/orgListUtil.ts index 1a0cad9f..ef7cd267 100644 --- a/src/shared/orgListUtil.ts +++ b/src/shared/orgListUtil.ts @@ -20,8 +20,10 @@ import fs from 'node:fs/promises'; import { Org, AuthInfo, + envVars, Global, Logger, + Messages, SfError, trimTo15, ConfigAggregator, @@ -101,12 +103,28 @@ export class OrgListUtil { OrgListUtil.processScratchOrgs(orgs.scratchOrgs), ]); + // TODO: Remove env var workaround + const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false); + const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted'); + const redactSecrets = (org: T): T => ({ + ...org, + accessToken: showSecretsEnvVarIsSet ? org.accessToken : secretsMessages.getMessage('redacted.accessToken'), + password: org.password + ? showSecretsEnvVarIsSet + ? org.password + : secretsMessages.getMessage('redacted.userPassword') + : undefined, + }); + + const redactedNonScratchOrgs = nonScratchOrgs.map(redactSecrets); + const redactedScratchOrgs = scratchOrgs.map(redactSecrets); + return { - nonScratchOrgs, - scratchOrgs, - sandboxes: nonScratchOrgs.filter(sandboxFilter), - other: nonScratchOrgs.filter(regularOrgFilter), - devHubs: nonScratchOrgs.filter(devHubFilter), + nonScratchOrgs: redactedNonScratchOrgs, + scratchOrgs: redactedScratchOrgs, + sandboxes: redactedNonScratchOrgs.filter(sandboxFilter), + other: redactedNonScratchOrgs.filter(regularOrgFilter), + devHubs: redactedNonScratchOrgs.filter(devHubFilter), }; } @@ -392,8 +410,8 @@ const identifyDefaultOrgs = ( */ const removeRestrictedInfoFromConfig = ( config: AuthFieldsFromFS, - properties: string[] = ['refreshToken', 'clientSecret'] -): AuthFieldsFromFS => omit>(config, properties); + properties: string[] = ['refreshToken', 'clientSecret', 'privateKey'] +): AuthFieldsFromFS => omit>(config, properties); const sandboxFilter = (org: AuthFieldsFromFS): boolean => Boolean(org.isSandbox); const regularOrgFilter = (org: AuthFieldsFromFS): boolean => !org.isSandbox && !org.isDevHub; diff --git a/test/shared/orgListUtil.test.ts b/test/shared/orgListUtil.test.ts index 0e3006bf..cc49d3c6 100644 --- a/test/shared/orgListUtil.test.ts +++ b/test/shared/orgListUtil.test.ts @@ -277,6 +277,46 @@ describe('orgListUtil tests', () => { expect(orgGroups.nonScratchOrgs[0].connectedStatus).to.equal('bad file'); expect(checkNonScratchOrgIsDevHub.called).to.be.false; }); + + describe('secret redaction', () => { + it('redacts access tokens in nonScratchOrgs', async () => { + const orgs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, false); + expect(orgs.nonScratchOrgs[0].accessToken).to.include('[REDACTED]'); + }); + + it('redacts access tokens in scratchOrgs', async () => { + const orgs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, false); + for (const org of orgs.scratchOrgs) { + expect(org.accessToken).to.include('[REDACTED]'); + } + }); + + it('redacts access tokens in devHubs', async () => { + const orgs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, false); + for (const org of orgs.devHubs) { + expect(org.accessToken).to.include('[REDACTED]'); + } + }); + }); + + describe('secret redaction WITH env var (SF_TEMP_SHOW_SECRETS)', () => { + const SHOW_TOKENS_ENV = 'SF_TEMP_SHOW_SECRETS'; + + beforeEach(() => { + process.env[SHOW_TOKENS_ENV] = 'true'; + }); + + afterEach(() => { + delete process.env[SHOW_TOKENS_ENV]; + }); + + it('shows real access tokens in scratchOrgs', async () => { + const orgs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, false); + for (const org of orgs.scratchOrgs) { + expect(org.accessToken).to.not.include('[REDACTED]'); + } + }); + }); }); describe('auth file reading tests', () => { diff --git a/test/unit/org/display.test.ts b/test/unit/org/display.test.ts index ea78af24..8605d208 100644 --- a/test/unit/org/display.test.ts +++ b/test/unit/org/display.test.ts @@ -262,6 +262,14 @@ describe('org:display', () => { expect(result.status).to.equal('Active'); }); + it('emits the workaround warning referencing sf org display', async () => { + await $$.stubAuths(testOrg); + await OrgDisplayCommand.run(['--targetusername', testOrg.username]); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org display'))).to.be.true; + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('SF_TEMP_SHOW_SECRETS'))).to.be.true; + }); + it('gets non-scratch org connectedStatus'); it('handles properly when username is an accessToken?'); it('displays good error when org is not connectable due to DNS'); @@ -299,11 +307,12 @@ describe('org:display', () => { expect(result.password).to.equal('somepassword'); }); - it('emits the deprecation warning', async () => { + it('emits the deprecation warning referencing sf org display', async () => { await $$.stubAuths(testOrg); await OrgDisplayCommand.run(['--targetusername', testOrg.username]); const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); expect(warnCalls.some((w) => typeof w === 'string' && w.includes('will be removed'))).to.be.true; + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org display'))).to.be.true; }); }); }); diff --git a/test/unit/org/list.test.ts b/test/unit/org/list.test.ts index 40a44f28..d3b197aa 100644 --- a/test/unit/org/list.test.ts +++ b/test/unit/org/list.test.ts @@ -28,10 +28,11 @@ describe('org:list', () => { // There is no need to call $$.restore() in afterEach() since that is // done automatically by the TestContext. const $$ = new TestContext(); + let sfCommandUxStubs: ReturnType; beforeEach(() => { // Stub the ux methods on SfCommand so that you don't get any command output in your tests. - stubSfCommandUx($$.SANDBOX); + sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); stubMethod($$.SANDBOX, AuthInfo, 'listAllAuthorizations').resolves([ 'Jimi Hendrix', 'SRV', @@ -94,4 +95,47 @@ describe('org:list', () => { expect(spies.get('orgRemove').callCount).to.equal(2); // there are 2 expired scratch orgs }); }); + + describe('secret redaction warnings', () => { + beforeEach(() => { + stubMethod($$.SANDBOX, OrgListUtil, 'readLocallyValidatedMetaConfigsGroupedByOrgType').resolves( + OrgListMock.AUTH_INFO + ); + }); + + it('emits the workaround warning referencing sf org list when --json is used', async () => { + await OrgListCommand.run(['--json']); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org list'))).to.be.true; + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org auth show-*'))).to.be.true; + }); + + it('does not emit the secrets warning without --json', async () => { + await OrgListCommand.run([]); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('SF_TEMP_SHOW_SECRETS'))).to.be.false; + }); + }); + + describe('secret redaction warnings WITH env var (SF_TEMP_SHOW_SECRETS)', () => { + const SHOW_TOKENS_ENV = 'SF_TEMP_SHOW_SECRETS'; + + beforeEach(() => { + process.env[SHOW_TOKENS_ENV] = 'true'; + stubMethod($$.SANDBOX, OrgListUtil, 'readLocallyValidatedMetaConfigsGroupedByOrgType').resolves( + OrgListMock.AUTH_INFO + ); + }); + + afterEach(() => { + delete process.env[SHOW_TOKENS_ENV]; + }); + + it('emits the deprecation warning referencing sf org list when --json is used', async () => { + await OrgListCommand.run(['--json']); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('will be removed'))).to.be.true; + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org list'))).to.be.true; + }); + }); }); From 49de264339753faeacc67d5aad656c2d1f801294 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Mon, 18 May 2026 21:35:21 -0500 Subject: [PATCH 3/6] chore: md format --- test/unit/org/list.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/org/list.test.ts b/test/unit/org/list.test.ts index d3b197aa..fdb856a5 100644 --- a/test/unit/org/list.test.ts +++ b/test/unit/org/list.test.ts @@ -107,6 +107,7 @@ describe('org:list', () => { await OrgListCommand.run(['--json']); const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org list'))).to.be.true; + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('SF_TEMP_SHOW_SECRETS'))).to.be.true; expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org auth show-*'))).to.be.true; }); @@ -136,6 +137,7 @@ describe('org:list', () => { const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); expect(warnCalls.some((w) => typeof w === 'string' && w.includes('will be removed'))).to.be.true; expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org list'))).to.be.true; + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org auth show-*'))).to.be.true; }); }); }); From e2698dd844ee6a96ce2c2af8e718981b3eb964f4 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Mon, 18 May 2026 21:46:01 -0500 Subject: [PATCH 4/6] chore: messaging --- messages/secrets-redacted.md | 4 ++-- test/unit/org/list.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/messages/secrets-redacted.md b/messages/secrets-redacted.md index 62bb08ab..898a7dbd 100644 --- a/messages/secrets-redacted.md +++ b/messages/secrets-redacted.md @@ -12,8 +12,8 @@ # temp.envVarWorkaround -Secrets are now hidden from '%s' command output. Use the 'sf org auth show-\*' commands instead. As a temporary workaround, you can set SF_TEMP_SHOW_SECRETS=true to render these secrets. This workaround will be removed in an upcoming release. +Secrets are now hidden from '%s' command output. Use the 'sf org auth' commands instead. As a temporary workaround, you can set SF_TEMP_SHOW_SECRETS=true to render these secrets. This workaround will be removed in an upcoming release. # temp.envVarIsSet -The SF_TEMP_SHOW_SECRETS env var is set. This is a temporary env var to continue to show secrets in the '%s' command output. This workaround will be removed in an upcoming CLI release. Switch to use the 'sf org auth show-\*' commands to avoid future disruption. +The SF_TEMP_SHOW_SECRETS env var is set. This is a temporary env var to continue to show secrets in the '%s' command output. This workaround will be removed in an upcoming CLI release. Switch to use the 'sf org auth' commands to avoid future disruption. diff --git a/test/unit/org/list.test.ts b/test/unit/org/list.test.ts index fdb856a5..ba5c3ea8 100644 --- a/test/unit/org/list.test.ts +++ b/test/unit/org/list.test.ts @@ -108,7 +108,7 @@ describe('org:list', () => { const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org list'))).to.be.true; expect(warnCalls.some((w) => typeof w === 'string' && w.includes('SF_TEMP_SHOW_SECRETS'))).to.be.true; - expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org auth show-*'))).to.be.true; + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org auth'))).to.be.true; }); it('does not emit the secrets warning without --json', async () => { @@ -137,7 +137,7 @@ describe('org:list', () => { const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); expect(warnCalls.some((w) => typeof w === 'string' && w.includes('will be removed'))).to.be.true; expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org list'))).to.be.true; - expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org auth show-*'))).to.be.true; + expect(warnCalls.some((w) => typeof w === 'string' && w.includes('sf org auth'))).to.be.true; }); }); }); From fa33e1d714b71cf0c4a48fbf561241ac52d2cf0b Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Mon, 18 May 2026 21:59:03 -0500 Subject: [PATCH 5/6] chore: revert --- src/shared/orgListUtil.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/orgListUtil.ts b/src/shared/orgListUtil.ts index ef7cd267..23a2027f 100644 --- a/src/shared/orgListUtil.ts +++ b/src/shared/orgListUtil.ts @@ -410,8 +410,8 @@ const identifyDefaultOrgs = ( */ const removeRestrictedInfoFromConfig = ( config: AuthFieldsFromFS, - properties: string[] = ['refreshToken', 'clientSecret', 'privateKey'] -): AuthFieldsFromFS => omit>(config, properties); + properties: string[] = ['refreshToken', 'clientSecret'] +): AuthFieldsFromFS => omit>(config, properties); const sandboxFilter = (org: AuthFieldsFromFS): boolean => Boolean(org.isSandbox); const regularOrgFilter = (org: AuthFieldsFromFS): boolean => !org.isSandbox && !org.isDevHub; @@ -422,7 +422,7 @@ const authErrorHandler = async (err: unknown, username: string): Promise const logger = await OrgListUtil.retrieveLogger(); logger.trace(`error refreshing auth for org: ${username}`); logger.trace(error); - // Orgs under maintenace return html as the error message. + // Orgs under maintenance return html as the error message. if (error.message.includes('maintenance')) return 'Down (Maintenance)'; // handle other potential html responses if (error.message.includes('') || error.message.includes('')) return 'Bad Response'; From c2f2bc639d7db7db9e23cb8e3c30a4a0411e29f2 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Mon, 18 May 2026 22:18:15 -0500 Subject: [PATCH 6/6] chore: move messages --- src/shared/orgListUtil.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shared/orgListUtil.ts b/src/shared/orgListUtil.ts index 23a2027f..751eccb4 100644 --- a/src/shared/orgListUtil.ts +++ b/src/shared/orgListUtil.ts @@ -40,6 +40,9 @@ import type { AuthFieldsFromFS, } from './orgTypes.js'; +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted'); + type OrgGroups = { nonScratchOrgs: ExtendedAuthFields[]; scratchOrgs: ExtendedAuthFieldsScratch[]; @@ -105,7 +108,6 @@ export class OrgListUtil { // TODO: Remove env var workaround const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false); - const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted'); const redactSecrets = (org: T): T => ({ ...org, accessToken: showSecretsEnvVarIsSet ? org.accessToken : secretsMessages.getMessage('redacted.accessToken'),