sfdx-hardis
Version:
Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards
354 lines • 19.3 kB
JavaScript
import c from 'chalk';
import fs from 'fs-extra';
import * as path from 'path';
import { createTempDir, execCommand, execSfdxJson, getCurrentGitBranch, isCI, promptInstanceUrl, uxLog, } from './index.js';
import { CONSTANTS, getConfig } from '../../config/index.js';
import { SfError } from '@salesforce/core';
import { clearCache } from '../cache/index.js';
import { WebSocketClient } from '../websocketClient.js';
import { decryptFile } from '../cryptoUtils.js';
// (removed accidental default export)
// Authorize an org with sfdxAuthUrl, manually or with JWT
export async function authOrg(orgAlias, options) {
const isDevHub = orgAlias.includes('DevHub');
let doConnect = true;
let alias = null;
let setDefaultOrg = false;
let orgInfo = {};
if (!options.checkAuth) {
// Check if we are already authenticated
let orgDisplayCommand = 'sf org display';
if (options.forceUsername) {
orgDisplayCommand += ' --target-org ' + options.forceUsername;
setDefaultOrg = options.setDefault ?? false;
}
else if (orgAlias && (isCI || isDevHub) && !orgAlias.includes('force://')) {
orgDisplayCommand += ' --target-org ' + orgAlias;
setDefaultOrg = options.setDefault ?? (orgAlias !== 'TECHNICAL_ORG' ? true : false);
}
else {
const argv = options?.argv || [];
if (argv.includes('--target-org') ||
argv.includes('--targetusername') ||
argv.includes('-o') ||
argv.includes('-u')) {
const posUsername = argv.indexOf('--target-org') > -1
? argv.indexOf('--target-org') + 1
: argv.indexOf('--targetusername') > -1
? argv.indexOf('--targetusername') + 1
: argv.indexOf('-o') > -1
? argv.indexOf('-o') + 1
: argv.indexOf('-u') > -1
? argv.indexOf('-u') + 1 : null;
if (posUsername === null) {
throw new SfError("Unable to find alias (authUtils.authOrg)");
}
alias = argv[posUsername];
orgDisplayCommand += ' --target-org ' + alias;
}
}
const orgInfoResult = await execSfdxJson(orgDisplayCommand, this, {
fail: false,
output: false,
debug: options.debug,
});
if (orgInfoResult.result &&
orgInfoResult.result.connectedStatus !== 'RefreshTokenAuthError' &&
((orgInfoResult.result.connectedStatus && orgInfoResult.result.connectedStatus.includes('Connected')) ||
(options.scratch && orgInfoResult.result.connectedStatus.includes('Unknown')) ||
(orgInfoResult.result.alias === orgAlias && orgInfoResult.result.id != null) ||
(orgInfoResult.result.username === orgAlias && orgInfoResult.result.id != null) ||
(isDevHub && orgInfoResult.result.id != null))) {
orgInfo = orgInfoResult.result;
if (orgInfoResult.result.apiVersion) {
globalThis.currentOrgApiVersion = orgInfoResult.result.apiVersion;
}
// Set as default username or devhubusername
uxLog("log", this, `[sfdx-hardis] You are already ${c.green('connected')} as ${c.green(orgInfoResult.result.username)} on org ${c.green(orgInfoResult.result.instanceUrl)} (apiVersion ${globalThis.currentOrgApiVersion})`);
if (orgInfoResult.result.expirationDate) {
uxLog("action", this, c.cyan(`[sfdx-hardis] Org expiration date: ${c.yellow(orgInfoResult.result.expirationDate)}`));
}
if (!isCI) {
uxLog("warning", this, c.yellow(c.italic(`If this is NOT the org you want to play with, ${c.whiteBright(c.bold('hit CTRL+C'))}, then input ${c.whiteBright(c.bold('sf hardis:org:select'))}`)));
}
if (setDefaultOrg) {
const setDefaultOrgCommand = `sf config set ${alias ? alias : isDevHub ? 'target-dev-hub' : 'target-org'}=${orgInfoResult.result.username}`;
await execSfdxJson(setDefaultOrgCommand, this, { fail: false });
}
globalThis.justConnectedOrg = sanitizeOrg(orgInfoResult.result);
doConnect = false;
}
}
// Perform authentication
let updateSfCliCommandOrg = false;
if (doConnect) {
let logged = false;
const config = await getConfig('user');
// Manage auth with sfdxAuthUrl (CI & scratch org only)
const authUrlVarName = `SFDX_AUTH_URL_${orgAlias}`;
const authUrlVarNameUpper = `SFDX_AUTH_URL_${orgAlias.toUpperCase()}`;
let authUrl = process.env[authUrlVarName] || process.env[authUrlVarNameUpper] || orgAlias || '';
if (isDevHub) {
authUrl =
process.env[authUrlVarName] ||
process.env[authUrlVarNameUpper] ||
process.env.SFDX_AUTH_URL_DEV_HUB ||
orgAlias ||
'';
}
if (authUrl.includes('force://')) {
const authFile = path.join(await createTempDir(), 'sfdxScratchAuth.txt');
await fs.writeFile(authFile, authUrl, 'utf8');
const authCommand = `sf org login sfdx-url -f ${authFile}` +
(isDevHub ? ` --set-default-dev-hub` : (setDefaultOrg ? ` --set-default` : '')) +
(!orgAlias.includes('force://') ? ` --alias ${orgAlias}` : '');
await execCommand(authCommand, this, { fail: true, output: false });
uxLog("action", this, c.cyan('Successfully logged using sfdxAuthUrl'));
await fs.remove(authFile);
return true;
}
// Get auth variables, with priority CLI arguments, environment variables, then .sfdx-hardis.yml config file
const cmdFlags = options.Command?.flags || {};
let username = options.forceUsername ?
options.forceUsername :
typeof cmdFlags.targetusername === 'string'
? cmdFlags.targetusername
: process.env.TARGET_USERNAME || isDevHub
? config.devHubUsername
: config.targetUsername || null;
if (username == null && isCI) {
const gitBranchFormatted = await getCurrentGitBranch({ formatted: true });
console.error(c.yellow(`[sfdx-hardis][WARNING] You may have to define ${c.bold(isDevHub
? 'devHubUsername in .sfdx-hardis.yml'
: options.scratch
? 'cache between your CI jobs: folder ".cache/sfdx-hardis/.sfdx"'
: `targetUsername in config/branches/.sfdx-hardis.${gitBranchFormatted}.yml`)} `));
process.exit(1);
}
let instanceUrl = options.instanceUrl ?
options.instanceUrl :
typeof options.Command?.flags?.instanceurl === 'string' &&
(options.Command?.flags?.instanceurl || '').startsWith('https')
? options.Command.flags.instanceurl
: (process.env.INSTANCE_URL || '').startsWith('https')
? process.env.INSTANCE_URL
: config.instanceUrl
? config.instanceUrl
: 'https://login.salesforce.com';
// Get JWT items clientId and certificate key
const sfdxClientId = await getSfdxClientId(orgAlias, config);
const crtKeyfile = await getCertificateKeyFile(orgAlias, config);
const usernameArg = options.setDefault === false ? '' : isDevHub ? '--set-default-dev-hub' : '--set-default';
if (crtKeyfile && sfdxClientId && username) {
// Login with JWT
const loginCommand = 'sf org login jwt' +
` ${usernameArg}` +
` --client-id ${sfdxClientId}` +
` --jwt-key-file ${crtKeyfile}` +
` --username ${username}` +
` --instance-url ${instanceUrl}` +
(orgAlias && !options.forceUsername ? ` --alias ${orgAlias}` : '');
const jwtAuthRes = await execSfdxJson(loginCommand, this, {
fail: false,
output: false
});
// await fs.remove(crtKeyfile); // Delete private key file from temp folder TODO: move to postrun hook
logged = jwtAuthRes.status === 0;
if (!logged) {
console.error(c.red(`[sfdx-hardis][ERROR] JWT login error: \n${JSON.stringify(jwtAuthRes)}`));
process.exit(1);
}
}
else if (!isCI) {
// Login with web auth
const orgLabel = `org ${orgAlias}`;
console.warn(c.yellow(c.bold(`[sfdx-hardis] You must be connected to ${orgLabel} to perform this command. Please login in the open web browser`)));
if (isCI) {
console.error(c.red(`See CI authentication doc at ${CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-setup-auth/`));
throw new SfError(`In CI context, you may define:
- a .sfdx-hardis.yml file with instanceUrl and targetUsername properties (or INSTANCE_URL and TARGET_USERNAME repo variables)
- a repository secret variable SFDX_CLIENT_ID with consumer key of SF CLI connected app
- store server.key file within ssh folder
`);
}
const orgTypes = isDevHub ? ['login'] : ['login', 'test'];
instanceUrl = await promptInstanceUrl(orgTypes, orgAlias);
const configInfoUsr = await getConfig('user');
let loginResult = null;
uxLog("action", this, c.cyan("Authenticating using web login..."));
const loginCommand = 'sf org login web' +
(alias ? ` --alias ${alias}` : options.setDefault === false ? '' : isDevHub ? ' --set-default-dev-hub' : ' --set-default') +
` --instance-url ${instanceUrl}` +
(orgAlias && orgAlias !== configInfoUsr?.scratchOrgAlias ? ` --alias ${orgAlias}` : '');
try {
loginResult = await execSfdxJson(loginCommand, this, { output: false, fail: true, spinner: false });
}
catch (e) {
// Give instructions if server is unavailable
if ((e.message || '').includes('Cannot start the OAuth redirect server on port')) {
uxLog("warning", this, c.yellow(c.bold('You might have a ghost SF CLI command. Open Task Manager, search for Node.js processes, kill them, then try again')));
}
throw e;
}
await clearCache('sf org list');
logged = loginResult.status === 0;
username = loginResult?.result?.username || loginResult?.username || 'err';
instanceUrl = loginResult?.result?.instanceUrl || loginResult?.instanceUrl || instanceUrl;
updateSfCliCommandOrg = true;
}
else {
console.error(c.red(`[sfdx-hardis] Unable to connect to org ${orgAlias} with browser. Please try again 😊`));
}
if (logged) {
// Retrieve default username or dev hub username if not returned by command
if (username === 'err') {
// Using alias
if (alias) {
const configGetRes = await execSfdxJson(`sf org display --target-org ${alias}`, this, {
output: false,
fail: false,
});
username = configGetRes?.result?.username || '';
}
else {
// Using default org
const configGetRes = await execSfdxJson('sf config get ' + (isDevHub ? 'target-dev-hub' : 'target-org'), this, {
output: false,
fail: false,
});
username = configGetRes?.result[0]?.value || '';
}
}
uxLog("other", this, `Successfully logged to ${c.green(instanceUrl)} with ${c.green(username)}`);
WebSocketClient.sendRefreshStatusMessage();
// Assign org to SfCommands
// if (isDevHub) {
// options.Command.flags["target-org"] = username;
// options.Command.assignHubOrg(); // seems to be automatically done by SfCommand under the hook
// } else {
// options.Command.flags["target-dev-hub"] = username;
// options.Command.assignOrg(); // seems to be automatically done by SfCommand under the hook
// }
// Display warning message in case of local usage (not CI), and not login command
if (!(options?.Command?.id || "").startsWith("hardis:auth:login") && updateSfCliCommandOrg === true) {
uxLog("warning", this, c.yellow("*** IF YOU SEE AN AUTH ERROR PLEASE RUN AGAIN THE SAME COMMAND 😊 ***"));
}
globalThis.justConnectedOrg = sanitizeOrg({ username, instanceUrl, ...orgInfo });
}
else {
console.error(c.red('[sfdx-hardis][ERROR] You must be logged to an org to perform this action'));
throw new SfError(`You must be logged to an org to perform this action`);
// process.exit(1); // Exit because we should succeed to connect
}
return true;
}
// If we skipped connection because we were already connected
if (!doConnect) {
return true;
}
// Fallback: should not be reached, but satisfy the boolean return contract
return false;
}
// Get clientId for SFDX connected app
async function getSfdxClientId(orgAlias, config) {
// Try to find in global variables
const sfdxClientIdVarName = `SFDX_CLIENT_ID_${orgAlias}`;
if (process.env[sfdxClientIdVarName]) {
console.log(c.grey(`[sfdx-hardis] Using ${sfdxClientIdVarName.toUpperCase()} env variable`));
return process.env[sfdxClientIdVarName];
}
const sfdxClientIdVarNameUpper = sfdxClientIdVarName.toUpperCase();
if (process.env[sfdxClientIdVarNameUpper]) {
console.log(c.grey(`[sfdx-hardis] Using ${sfdxClientIdVarNameUpper} env variable`));
return process.env[sfdxClientIdVarNameUpper];
}
if (process.env.SFDX_CLIENT_ID) {
console.warn(c.yellow(`[sfdx-hardis] If you use CI on multiple branches & orgs, you should better define CI variable ${c.bold(sfdxClientIdVarNameUpper)} than SFDX_CLIENT_ID`));
console.warn(c.yellow(`See CI authentication doc at ${CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-setup-auth/`));
return process.env.SFDX_CLIENT_ID;
}
// Try to find in config files ONLY IN LOCAL MODE (in CI, it's supposed to be a CI variable)
if (!isCI && config.devHubSfdxClientId) {
console.log(c.grey(`[sfdx-hardis] Using devHubSfdxClientId config variable`));
return config.devHubSfdxClientId;
}
if (isCI) {
console.error(c.red(`[sfdx-hardis] You must set env variable ${c.bold(sfdxClientIdVarNameUpper)} with the Consumer Key value defined on SFDX Connected app`));
console.warn(c.yellow(`If you configured ${sfdxClientIdVarNameUpper} but still see this message, you may have forgotten to reference the variable name in your GitHub or Azure YML pipeline.`));
console.warn(c.yellow(`See CI authentication doc at ${CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-setup-auth/`));
}
return null;
}
// Get clientId for SFDX connected app
async function getKey(orgAlias, config) {
// Try to find in global variables
const sfdxClientKeyVarName = `SFDX_CLIENT_KEY_${orgAlias}`;
if (process.env[sfdxClientKeyVarName]) {
console.log(c.grey(`[sfdx-hardis] Using ${sfdxClientKeyVarName.toUpperCase()} env variable`));
return process.env[sfdxClientKeyVarName];
}
const sfdxClientKeyVarNameUpper = sfdxClientKeyVarName.toUpperCase();
if (process.env[sfdxClientKeyVarNameUpper]) {
console.log(c.grey(`[sfdx-hardis] Using ${sfdxClientKeyVarNameUpper} env variable`));
return process.env[sfdxClientKeyVarNameUpper];
}
if (process.env.SFDX_CLIENT_KEY) {
console.warn(c.yellow(`[sfdx-hardis] If you use CI on multiple branches & orgs, you should better define CI variable ${c.bold(sfdxClientKeyVarNameUpper)} than SFDX_CLIENT_KEY`));
console.warn(c.yellow(`See CI authentication doc at ${CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-setup-auth/`));
return process.env.SFDX_CLIENT_KEY;
}
// Try to find in config files ONLY IN LOCAL MODE (in CI, it's supposed to be a CI variable)
if (!isCI && config.devHubSfdxClientKey) {
console.log(c.grey(`[sfdx-hardis] Using devHubSfdxClientKey config variable`));
return config.devHubSfdxClientKey;
}
if (isCI) {
console.error(c.red(`[sfdx-hardis] You must set env variable ${c.bold(sfdxClientKeyVarNameUpper)} with the value of SSH private key encryption key`));
console.warn(c.yellow(`If you configured ${sfdxClientKeyVarNameUpper} but still see this message, you may have forgotten to reference the variable name in your GitHub or Azure YML pipeline.`));
console.warn(c.yellow(`See CI authentication doc at ${CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-setup-auth/`));
}
return null;
}
// Try to find certificate key file for SF CLI connected app in different locations
async function getCertificateKeyFile(orgAlias, config) {
const filesToTry = [
`./config/branches/.jwt/${orgAlias}.key`,
`./config/.jwt/${orgAlias}.key`,
`./ssh/${orgAlias}.key`,
`./.ssh/${orgAlias}.key`,
'./ssh/server.key',
];
// Check if we find multiple files
const filesFound = filesToTry.filter((file) => fs.existsSync(file));
if (filesFound.length > 1) {
console.warn(c.yellow(`[sfdx-hardis] Multiple certificate key files found: ${filesFound.join(', ')}. Please keep only one certificate key file. If you don't know which one, remove all and re-run the configuration command`));
}
for (const file of filesToTry) {
if (fs.existsSync(file)) {
// Decrypt SSH private key and write a temporary file
const sshKey = await getKey(orgAlias, config);
if (sshKey == null) {
continue;
}
const tmpSshKeyFile = path.join(await createTempDir(), `${orgAlias}.key`);
console.log(c.grey(`[sfdx-hardis] Decrypting key...`));
await decryptFile(file, tmpSshKeyFile, sshKey);
return tmpSshKeyFile;
}
}
if (isCI) {
console.error(c.red(`[sfdx-hardis] You must put a certificate key to connect via JWT.Possible locations:\n -${filesToTry.join('\n -')}`));
uxLog("error", this, c.red(`See CI authentication doc at ${CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-setup-auth/`));
}
return null;
}
function sanitizeOrg(org) {
if (org?.accessToken) {
org.accessToken = '***REDACTED***';
}
if (org?.refreshToken) {
org.refreshToken = '***REDACTED***';
}
return org;
}
//# sourceMappingURL=authUtils.js.map