diff --git a/messages/web.login.md b/messages/web.login.md index e09c1908..870b3fc5 100644 --- a/messages/web.login.md +++ b/messages/web.login.md @@ -79,3 +79,7 @@ Unable to open the browser you specified (%s). # error.cannotOpenBrowser.actions - Ensure that %s is installed on your computer. Or specify a different browser using the --browser flag. + +# verificationCode + +- Your verification code is %s. Enter this in the browser window that just opened. SECURITY NOTE: Enter this PIN only if you initiated this login. diff --git a/src/commands/org/login/web.ts b/src/commands/org/login/web.ts index d3caf6a8..0e3c40a0 100644 --- a/src/commands/org/login/web.ts +++ b/src/commands/org/login/web.ts @@ -14,12 +14,20 @@ * limitations under the License. */ +import { createHash } from 'node:crypto'; import open, { apps, AppName } from 'open'; import { Flags, SfCommand, loglevel } from '@salesforce/sf-plugins-core'; import { AuthFields, AuthInfo, Logger, Messages, OAuth2Config, SfError, WebOAuthServer } from '@salesforce/core'; import { Env } from '@salesforce/kit'; import common from '../../../common.js'; +export const CODE_BUILDER_STATE_ENV_VAR = 'CODE_BUILDER_STATE'; + +export const getVerificationCode = (codeBuilderState: string): string => { + const hash = createHash('sha256').update(codeBuilderState, 'utf8').digest('hex'); + return hash.substring(0, 4); +}; + Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-auth', 'web.login'); const commonMessages = Messages.loadMessages('@salesforce/plugin-auth', 'messages'); @@ -112,6 +120,15 @@ export default class LoginWeb extends SfCommand { throw new SfError(messages.getMessage('error.headlessWebAuth')); } + // Display verification code for Code Builder mode if env is set + const env = new Env(); + const codeBuilderState = env.getString(CODE_BUILDER_STATE_ENV_VAR); + if (codeBuilderState) { + const verificationCode = getVerificationCode(codeBuilderState); + + this.log(messages.getMessage('verificationCode', [verificationCode])); + } + if (await common.shouldExitCommand(flags['no-prompt'])) return {}; // Add ca/eca to already existing auth info. diff --git a/test/commands/org/login/login.web.test.ts b/test/commands/org/login/login.web.test.ts index 3c7f9a5a..2fcbbe96 100644 --- a/test/commands/org/login/login.web.test.ts +++ b/test/commands/org/login/login.web.test.ts @@ -17,15 +17,21 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { Config } from '@oclif/core'; -import { AuthFields, AuthInfo, SfError } from '@salesforce/core'; +import { AuthFields, AuthInfo, SfError, Messages } from '@salesforce/core'; import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; import { StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon'; import { assert, expect } from 'chai'; import { Env } from '@salesforce/kit'; import { SfCommand, Ux } from '@salesforce/sf-plugins-core'; -import LoginWeb, { ExecuteLoginFlowParams } from '../../../../src/commands/org/login/web.js'; +import LoginWeb, { + ExecuteLoginFlowParams, + CODE_BUILDER_STATE_ENV_VAR, + getVerificationCode, +} from '../../../../src/commands/org/login/web.js'; describe('org:login:web', () => { + Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); + const messages = Messages.loadMessages('@salesforce/plugin-auth', 'web.login'); const $$ = new TestContext(); const testData = new MockTestOrgData(); const config = stubInterface($$.SANDBOX, { @@ -302,4 +308,51 @@ describe('org:login:web', () => { expect(callArgs.clientApp?.username).to.equal('test@example.com'); expect(callArgs.scopes).to.be.undefined; }); + + it('should display verification code when CODE_BUILDER_STATE env var is set', async () => { + const codeBuilderState = 'CODE_BUILDER_STATE'; + const envStub = $$.SANDBOX.stub(Env.prototype, 'getString'); + envStub.withArgs(CODE_BUILDER_STATE_ENV_VAR).returns(codeBuilderState); + envStub.returns(''); // Default for other calls + + $$.SANDBOX.stub(Env.prototype, 'getBoolean').returns(false); // Prevent container mode checks + + const logStub = stubMethod($$.SANDBOX, SfCommand.prototype, 'log'); + + const login = await createNewLoginCommand([], false, undefined); + await login.run(); + + // Verify that log was called with the verification code message + const verificationCode = getVerificationCode(codeBuilderState); + const calls = logStub.getCalls(); + const verificationCodeCall = calls.find( + (call) => typeof call.args[0] === 'string' && call.args[0].includes(verificationCode) + ); + expect(verificationCodeCall).to.exist; + expect(verificationCode).to.match(/^[0-9a-f]{4}$/); + expect(verificationCodeCall?.args[0]).to.include(messages.getMessage('verificationCode', [verificationCode])); + }); + + it('should not display verification code when CODE_BUILDER_STATE env var is not set', async () => { + const envStub = stubMethod($$.SANDBOX, Env.prototype, 'getString'); + envStub.withArgs('CODE_BUILDER_STATE').returns(undefined); + envStub.returns(''); + + $$.SANDBOX.stub(Env.prototype, 'getBoolean').returns(false); // Prevent container mode checks + + const logStub = $$.SANDBOX.stub(SfCommand.prototype, 'log'); + const logSuccessStub = $$.SANDBOX.stub(SfCommand.prototype, 'logSuccess'); + + const login = await createNewLoginCommand([], false, undefined); + await login.run(); + + // Verify that log was NOT called for verification code + expect(logStub.callCount).to.equal(0); + const calls = logSuccessStub.getCalls(); + const verificationCodeCall = calls.find( + (call) => call.args[0]?.includes('verification code') || call.args[0]?.includes('Enter this') + ); + expect(verificationCodeCall).to.not.exist; + expect(logSuccessStub.callCount).to.equal(1); + }); });