@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
457 lines • 25.4 kB
JavaScript
"use strict";
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateRevisionCounterToZero = exports.resolveUrl = exports.deploySettings = exports.pollForScratchOrgInfo = exports.requestScratchOrgCreation = exports.authorizeScratchOrg = exports.queryScratchOrgInfo = void 0;
const node_util_1 = require("node:util");
const kit_1 = require("@salesforce/kit");
const ts_retry_promise_1 = require("ts-retry-promise");
const logger_1 = require("../logger/logger");
const messages_1 = require("../messages");
const sfError_1 = require("../sfError");
const sfdcUrl_1 = require("../util/sfdcUrl");
const pollingClient_1 = require("../status/pollingClient");
const myDomainResolver_1 = require("../status/myDomainResolver");
const lifecycleEvents_1 = require("../lifecycleEvents");
const mapKeys_1 = __importDefault(require("../util/mapKeys"));
const authInfo_1 = require("./authInfo");
const org_1 = require("./org");
const scratchOrgErrorCodes_1 = require("./scratchOrgErrorCodes");
const scratchOrgLifecycleEvents_1 = require("./scratchOrgLifecycleEvents");
;
const messages = new messages_1.Messages('@salesforce/core', 'scratchOrgInfoApi', new Map([["SignupFieldsMissingError", "Required fields are missing for org creation: [%s]"], ["SignupDuplicateSettingsSpecifiedError", "You cannot use 'settings' and `'orgPreferences' in your scratch definition file, please specify one or the other."], ["DeprecatedPrefFormat", "We've deprecated OrgPreferences. Update the scratch org definition file to replace OrgPreferences with their corresponding settings."], ["SourceStatusResetFailureError", "Successfully created org with ID: %s and name: %s. Unfortunately, source tracking isn\u2019t working as expected. If you run force:source:status, the results may be incorrect. Try again by creating another scratch org."], ["hubOrgIsNotDevHub", "Make sure that the org with username %s and ID %s is enabled as a Dev Hub. To enable it, open the org in your browser, navigate to the Dev Hub page in Setup, and click Enable.\nIf you still see this error after enabling the Dev Hub feature, then re-authenticate to the org."]]));
const errorCodes = new messages_1.Messages('@salesforce/core', 'scratchOrgErrorCodes', new Map([["SignupFailedError", "The request to create a scratch org failed with error code: %s."], ["SignupFailedUnknownError", "An unknown server error occurred. Please try again. If you still see this error, contact Salesforce support for assistance. Include the information from 'sfdx force:data:record:get -s ScratchOrgInfo -i %s -u %s'."], ["SignupFailedActionError", "See https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_error_codes.htm for information on error code %s."], ["SignupUnexpectedError", "The request to create a scratch org returned an unexpected status"], ["StillInProgressError", "The scratch org is not ready yet (Status = %s)."], ["action.StillInProgress", "Wait for a few minutes, and then try the command again"], ["NoScratchOrgInfoError", "No ScratchOrgInfo object found in the Dev Hub you specified. Check that the ID and the Dev Hub are correct."], ["ScratchOrgDeletedError", "That scratch org has been deleted, so you can't connect to it anymore."], ["INVALID_ID_FIELD", "Provide a valid template ID, in the format 0TTxxxxxxxxxxxx."], ["T-0002", "We couldn\u2019t find a template or snapshot with the ID or name specified in the scratch org definition. If you\u2019re sure the ID is correct, contact Salesforce Support."], ["T-0003", "The template specified in the scratch org definition is unapproved. Contact Salesforce Support to request template approval."], ["T-0004", "The Trialforce Source Organization (TSO) for the template doesn\u2019t exist or has expired."], ["S-1006", "Provide a valid email address in your scratch org definition or your %s file."], ["S-2006", "Provide a valid country code in your scratch org definition or your %s file."], ["S-1017", "Specify a namespace that\u2019s used by a release org associated with your Dev Hub org."], ["S-1018", "Provide a valid My Domain value. This value can\u2019t include double hyphens, end in a hyphen, include restricted words, or be more than 40 characters long."], ["S-1019", "The My Domain value you chose is already in use."], ["S-1026", "Provide a valid namespace value. This value must begin with a letter. It can\u2019t include consecutive underscores, end in an underscore, be more than 15 characters long, or be a restricted or reserved namespace. Only alphanumeric characters and underscores are allowed."], ["S-9999", "A fatal signup error occurred. Please try again. If you still see this error, contact Salesforce Support for assistance."], ["SH-0001", "Can\u2019t create scratch org. Contact the source org admin to add your Dev Hub org ID to Setup > Org Shape. Then try again."], ["SH-0002", "Can\u2019t create scratch org. No org shape exists for the specified sourceOrg. Create an org shape and try again."], ["SH-0003", "Can\u2019t create scratch org from org shape. The org shape version is outdated. Recreate the org shape and try again."], ["C-1007", "The username provided to the org:create command is already in use. Run 'force:org:list --clean' to remove stale org authentications or create the org with a different username."], ["C-1015", "We encountered a problem while registering your My Domain value with the DNS provider. Please try again."], ["C-1016", "We encountered a problem while attempting to configure and approve the Connected App for your org. Verify the Connected App configuration with your Salesforce admin."], ["C-1017", "Provide a valid namespace prefix. This value must begin with a letter. It can\u2019t include consecutive underscores, end in an underscore, be more than 15 characters long, or be a restricted or reserved namespace. Only alphanumeric characters and underscores are allowed."], ["C-1020", "We couldn't find a template with the ID specified in the scratch org definition. If you\u2019re sure the ID is correct, contact Salesforce Support."], ["C-1027", "The template specified in the Scratch Definition isn\u2019t supported. Specify a generic edition (such as Developer or Enterprise), or specify a template ID."], ["C-9999", "A fatal signup error occurred. Please try again. If you still see this error, contact Salesforce Support for assistance"]]));
/**
* Returns the url to be used to authorize into the new scratch org
*
* @param scratchOrgInfoComplete The completed ScratchOrgInfo
* @param hubOrgLoginUrl the hun org login url
* @param signupTargetLoginUrl the login url
* @returns {string}
*/
const getOrgInstanceAuthority = function (scratchOrgInfoComplete, hubOrgLoginUrl, signupTargetLoginUrl) {
const createdOrgInstance = scratchOrgInfoComplete.SignupInstance;
if (createdOrgInstance === 'utf8') {
return hubOrgLoginUrl;
}
let altUrl;
// For non-Falcon (ie - instance names not ending in -s) sandboxes, use the instance URL
if (createdOrgInstance && !createdOrgInstance.toLowerCase().endsWith('s')) {
altUrl = `https://${createdOrgInstance}.salesforce.com`;
}
else {
// For Falcon sandboxes, try the LoginURL instead; createdOrgInstance will not yield a valid URL
altUrl = scratchOrgInfoComplete.LoginUrl;
}
return signupTargetLoginUrl ?? altUrl;
};
/**
* Returns OAuth2Options object
*
* @returns {OAuth2Options, number, number, number} options, retries, timeout, delay
* @param options
*/
const buildOAuth2Options = async (options) => {
const logger = await logger_1.Logger.child('buildOAuth2Options');
const isJwtFlow = !!options.hubOrg.getConnection().getAuthInfoFields().privateKey;
const oauth2Options = {
loginUrl: getOrgInstanceAuthority(options.scratchOrgInfoComplete, options.hubOrg.getField(org_1.Org.Fields.LOGIN_URL), options.signupTargetLoginUrl),
};
logger.debug(`isJwtFlow: ${isJwtFlow}`);
logger.debug(`using resolved loginUrl: ${oauth2Options.loginUrl}`);
if (isJwtFlow && !process.env.SFDX_CLIENT_SECRET) {
oauth2Options.privateKeyFile = options.hubOrg.getConnection().getAuthInfoFields().privateKey;
const retries = options?.retry ?? kit_1.env.getNumber('SFDX_JWT_AUTH_RETRY_ATTEMPTS') ?? 0;
const timeoutInSeconds = kit_1.env.getNumber('SFDX_JWT_AUTH_RETRY_TIMEOUT') ?? 300;
const timeout = kit_1.Duration.seconds(timeoutInSeconds).milliseconds;
const delay = retries ? timeout / retries : 1000;
return {
options: oauth2Options,
retries,
timeout,
delay,
};
}
else {
// Web Server OAuth "auth code exchange" flow
if (process.env.SFDX_CLIENT_SECRET) {
oauth2Options.clientSecret = process.env.SFDX_CLIENT_SECRET;
}
else if (options.clientSecret) {
oauth2Options.clientSecret = options.clientSecret;
}
oauth2Options.redirectUri = options.scratchOrgInfoComplete.ConnectedAppCallbackUrl;
oauth2Options.authCode = options.scratchOrgInfoComplete.AuthCode;
return {
options: oauth2Options,
retries: 0,
};
}
};
/**
* Returns OAuth2Options object
*
* hubOrg the environment hub org
* username The OAuth client secret. May be null for JWT OAuth flow.
* oauth2Options The completed ScratchOrgInfo which should contain an access token.
* retries auth retry a
* timeout the login url
* delay the login url
*
* @returns {OAuth2Options, number, number, number} options, retries, timeout, delay
*/
const getAuthInfo = async (options) => {
const logger = await logger_1.Logger.child('getAuthInfo');
const retryAuthorize = (0, ts_retry_promise_1.retryDecorator)(async (opts) => authInfo_1.AuthInfo.create(opts), {
timeout: options.timeout,
delay: options.delay,
retries: options.retries,
});
if (options.retries) {
try {
return await retryAuthorize({
username: options.username,
parentUsername: options.hubOrg.getUsername(),
oauth2Options: options.oauth2Options,
});
}
catch (err) {
const error = err;
logger.error(error);
throw error.lastError || error;
}
}
else {
return authInfo_1.AuthInfo.create({
username: options.username,
parentUsername: options.hubOrg.getUsername(),
oauth2Options: options.oauth2Options,
});
}
};
/**
*
* @param hubOrg Org
* @param id Record ID for the ScratchOrgInfoObject
* @returns Promise<ScratchOrgInfo>
*/
const queryScratchOrgInfo = async (hubOrg, id) => (await hubOrg.getConnection().sobject('ScratchOrgInfo').retrieve(id));
exports.queryScratchOrgInfo = queryScratchOrgInfo;
/**
* after we successfully signup an org we need to trade the auth token for access and refresh token.
*
* scratchOrgInfoComplete - The completed ScratchOrgInfo which should contain an access token.
* hubOrg - the environment hub org
* clientSecret - The OAuth client secret. May be null for JWT OAuth flow.
* signupTargetLoginUrl - Login url override
* retry - auth retry attempts
*
* @returns {Promise<AuthInfo>}
*/
const authorizeScratchOrg = async (options) => {
const { scratchOrgInfoComplete, hubOrg, clientSecret, signupTargetLoginUrl, retry } = options;
await (0, scratchOrgLifecycleEvents_1.emit)({ stage: 'authenticate', scratchOrgInfo: scratchOrgInfoComplete });
const logger = await logger_1.Logger.child('authorizeScratchOrg');
logger.debug(`scratchOrgInfoComplete: ${JSON.stringify(scratchOrgInfoComplete, null, 4)}`);
// if we didn't have it marked as a devhub but just successfully used it as one, this will update the authFile, fix cache, etc
if (!hubOrg.isDevHubOrg()) {
await hubOrg.determineIfDevHubOrg(true);
}
const oAuth2Options = await buildOAuth2Options({
hubOrg,
clientSecret,
scratchOrgInfoComplete,
retry,
signupTargetLoginUrl,
});
let authInfo;
try {
// This will use the authCode from the scratch org signup to exchange for an auth token via OAuth.
authInfo = await getAuthInfo({
hubOrg,
username: scratchOrgInfoComplete.SignupUsername,
oauth2Options: oAuth2Options.options,
retries: oAuth2Options.retries,
timeout: oAuth2Options.timeout,
delay: oAuth2Options.delay,
});
}
catch (err1) {
// If we didn't already try authenticating with the LoginUrl from ScratchOrgInfo object,
// try the oauth flow again using it now.
if (scratchOrgInfoComplete.LoginUrl && oAuth2Options.options.loginUrl !== scratchOrgInfoComplete.LoginUrl) {
logger.debug(`Auth failed with loginUrl ${oAuth2Options.options.loginUrl} so trying with ${scratchOrgInfoComplete.LoginUrl}`);
oAuth2Options.options = { ...oAuth2Options.options, ...{ loginUrl: scratchOrgInfoComplete.LoginUrl } };
try {
authInfo = await getAuthInfo({
hubOrg,
username: scratchOrgInfoComplete.SignupUsername,
oauth2Options: oAuth2Options.options,
retries: oAuth2Options.retries,
timeout: oAuth2Options.timeout,
delay: oAuth2Options.delay,
});
}
catch (err2) {
// Log this error but throw the original error
logger.debug(`Auth failed with ScratchOrgInfo.LoginUrl ${scratchOrgInfoComplete.LoginUrl}\n${(0, node_util_1.inspect)(err2)}`);
throw err1;
}
}
else {
throw err1;
}
}
await authInfo.save({
devHubUsername: hubOrg.getUsername(),
created: new Date(scratchOrgInfoComplete.CreatedDate ?? new Date()).valueOf().toString(),
expirationDate: scratchOrgInfoComplete.ExpirationDate,
clientId: scratchOrgInfoComplete.ConnectedAppConsumerKey,
createdOrgInstance: scratchOrgInfoComplete.SignupInstance,
isDevHub: false,
isScratch: true,
isSandbox: false,
// omit optional fields unless they are present
...(scratchOrgInfoComplete.Namespace ? { namespacePrefix: scratchOrgInfoComplete.Namespace } : {}),
...(scratchOrgInfoComplete.Snapshot ? { snapshot: scratchOrgInfoComplete.Snapshot } : {}),
});
return authInfo;
};
exports.authorizeScratchOrg = authorizeScratchOrg;
const checkOrgDoesntExist = async (scratchOrgInfo) => {
const usernameKey = Object.keys(scratchOrgInfo).find((key) => key.toUpperCase() === 'USERNAME');
if (!usernameKey) {
return;
}
const username = scratchOrgInfo[usernameKey];
if (username && username.length > 0) {
try {
await authInfo_1.AuthInfo.create({ username: username.toLowerCase() });
}
catch (error) {
const sfError = sfError_1.SfError.wrap(error);
// if an AuthInfo couldn't be created that means no AuthFile exists.
if (sfError.name === 'NamedOrgNotFoundError') {
return;
}
// Something unexpected
throw sfError;
}
// An org file already exists
throw errorCodes.createError('C-1007');
}
};
/**
* This extracts orgPrefs/settings from the user input and performs a basic scratchOrgInfo request.
*
* @param hubOrg - the environment hub org
* @param scratchOrgRequest - An object containing the fields of the ScratchOrgInfo
* @param settings - An object containing org settings
* @returns {Promise<SaveResult>}
*/
const requestScratchOrgCreation = async (hubOrg, scratchOrgRequest, settings) => {
if (!hubOrg.isDevHubOrg()) {
throw messages.createError('hubOrgIsNotDevHub', [hubOrg.getUsername(), hubOrg.getOrgId()]);
}
// We do not allow you to specify the old and the new way of doing post create settings
if (scratchOrgRequest.orgPreferences && settings.hasSettings()) {
// This is not allowed
throw new sfError_1.SfError('SignupDuplicateSettingsSpecifiedError');
}
// deprecated old style orgPreferences
if (scratchOrgRequest.orgPreferences) {
throw new sfError_1.SfError(messages.getMessage('DeprecatedPrefFormat'));
}
const scratchOrgInfo = (0, mapKeys_1.default)(
// If these were present, they were already used to initialize the scratchOrgSettingsGenerator.
// They shouldn't be submitted as part of the scratchOrgInfo.
(0, kit_1.omit)(scratchOrgRequest, ['settings', 'objectSettings']), kit_1.upperFirst, true);
if (typeof scratchOrgInfo.Username === 'string') {
scratchOrgInfo.Username = scratchOrgInfo.Username.toLowerCase();
}
await checkOrgDoesntExist(scratchOrgInfo); // throw if it does exist.
try {
await (0, scratchOrgLifecycleEvents_1.emit)({ stage: 'send request' });
// return await will cause this catch block to run instead of the caller's catch block
return await hubOrg.getConnection().sobject('ScratchOrgInfo').create(scratchOrgInfo);
}
catch (error) {
// this is a jsforce error which contains the property "fields" which regular error don't
const jsForceError = error;
if (jsForceError.errorCode === 'REQUIRED_FIELD_MISSING') {
throw new sfError_1.SfError(messages.getMessage('SignupFieldsMissingError', [jsForceError.fields.toString()]));
}
throw sfError_1.SfError.wrap(jsForceError);
}
};
exports.requestScratchOrgCreation = requestScratchOrgCreation;
/**
* This retrieves the ScratchOrgInfo, polling until the status is Active or Error
*
* @param hubOrg
* @param scratchOrgInfoId - the id of the scratchOrgInfo that we are retrieving
* @param timeout - A Duration object
* @returns {Promise<ScratchOrgInfo>}
*/
const pollForScratchOrgInfo = async (hubOrg, scratchOrgInfoId,
// org:create specifies a default timeout of 6. This longer default is for other consumers
timeout = kit_1.Duration.minutes(15)) => {
const logger = await logger_1.Logger.child('scratchOrgInfoApi-pollForScratchOrgInfo');
logger.debug(`PollingTimeout in minutes: ${timeout.minutes}`);
const pollingOptions = {
async poll() {
try {
const resultInProgress = await (0, exports.queryScratchOrgInfo)(hubOrg, scratchOrgInfoId);
logger.debug(`polling client result: ${JSON.stringify(resultInProgress, null, 4)}`);
// Once it's "done" we can return it
if (resultInProgress.Status === 'Active' || resultInProgress.Status === 'Error') {
return {
completed: true,
payload: resultInProgress,
};
}
await (0, scratchOrgLifecycleEvents_1.emit)({ stage: 'wait for org', scratchOrgInfo: resultInProgress });
logger.debug(`Scratch org status is ${resultInProgress.Status}`);
return {
completed: false,
};
}
catch (error) {
logger.debug(`An error occurred trying to retrieve scratchOrgInfo for ${scratchOrgInfoId}`);
logger.debug(`Error: ${error.message}`);
logger.debug('Re-trying deploy check again....');
return {
completed: false,
};
}
},
timeout,
frequency: kit_1.Duration.seconds(1),
timeoutErrorName: 'ScratchOrgInfoTimeoutError',
};
const client = await pollingClient_1.PollingClient.create(pollingOptions);
try {
const resultInProgress = await client.subscribe();
return await (0, scratchOrgErrorCodes_1.checkScratchOrgInfoForErrors)(resultInProgress, hubOrg.getUsername());
}
catch (error) {
if (error instanceof Error) {
const sfError = sfError_1.SfError.wrap(error);
sfError.setData({
username: hubOrg.getUsername(),
orgId: hubOrg.getOrgId(),
scratchOrgInfoId,
});
throw sfError;
}
throw new sfError_1.SfError(`The scratch org did not complete within ${timeout.minutes} minutes`, 'orgCreationTimeout', [
'Try your force:org:create command again with a longer --wait value',
]);
}
};
exports.pollForScratchOrgInfo = pollForScratchOrgInfo;
/**
* Deploy settings to the newly created scratch org
*
* @param scratchOrg an instance of the Org class
* @param orgSettings an instance of the SettingsGenerator class
* @param apiVersion the api version (used when created the package.xml)
*/
const deploySettings = async (scratchOrg, orgSettings, apiVersion, timeout = kit_1.Duration.minutes(10)) => {
const logger = await logger_1.Logger.child('scratchOrgInfoApi-deploySettings');
if (orgSettings.hasSettings()) {
// deploy the settings to the newly created scratch org
logger.debug(`deploying scratch org settings with apiVersion ${apiVersion}`);
try {
await orgSettings.createDeploy();
await orgSettings.deploySettingsViaFolder(scratchOrg, apiVersion, timeout);
// updating the revision num to zero during org:creation if source members are created during org:create.
// This only happens for some specific scratch org definition file.
await (0, exports.updateRevisionCounterToZero)(scratchOrg);
logger.trace('Settings deployed to org');
}
catch (error) {
throw sfError_1.SfError.wrap(error);
}
}
};
exports.deploySettings = deploySettings;
/**
* Makes sure the scratch org's instanceUrl is resolvable (that is, DNS is ready)
*
* @param scratchOrgAuthInfo an AuthInfo class from the scratch org
* @returns AuthInfo
*/
const resolveUrl = async (scratchOrgAuthInfo) => {
const logger = await logger_1.Logger.child('scratchOrgInfoApi-resolveUrl');
const { instanceUrl } = scratchOrgAuthInfo.getFields();
if (!instanceUrl) {
throw sfError_1.SfError.create({
message: 'Org does not have instanceUrl',
data: {
orgId: scratchOrgAuthInfo.getFields().orgId,
username: scratchOrgAuthInfo.getFields().username,
instanceUrl,
},
});
}
logger.debug(`processScratchOrgInfoResult - resultData.instanceUrl: ${instanceUrl}`);
const options = {
timeout: kit_1.Duration.minutes(3),
frequency: kit_1.Duration.seconds(10),
url: new sfdcUrl_1.SfdcUrl(instanceUrl),
};
try {
const resolver = await myDomainResolver_1.MyDomainResolver.create(options);
await resolver.resolve();
return scratchOrgAuthInfo;
}
catch (error) {
const sfError = sfError_1.SfError.wrap(error);
logger.debug('processScratchOrgInfoResult - err: %s', error);
if (sfError.name === 'MyDomainResolverTimeoutError') {
sfError.setData({
orgId: scratchOrgAuthInfo.getFields().orgId,
username: scratchOrgAuthInfo.getFields().username,
instanceUrl,
});
logger.debug('processScratchOrgInfoResult - err data: %s', sfError.data);
}
throw sfError;
}
};
exports.resolveUrl = resolveUrl;
const updateRevisionCounterToZero = async (scratchOrg) => {
const conn = scratchOrg.getConnection();
const queryResult = await conn.tooling.sobject('SourceMember').find({ RevisionCounter: { $gt: 0 } }, ['Id']);
if (queryResult.length === 0) {
return;
}
try {
// jsforce has a bug in its `update` code such that tooling#update doesn't work right
// https://github.com/jsforce/jsforce/blob/265eba5c734439dd7b77610c05b63bde7d4b1e83/src/connection.ts#L1082
// will result in `this._ensureVersion is not a function`
// so until that is resolved, we hit the API with singular records
// once that's fixed, you can use the following code for a single API call
// await conn.tooling
// .sobject('SourceMember')
// .update(queryResult.map((record) => ({ Id: record.Id, RevisionCounter: 0 })));
await Promise.all(queryResult.map((record) => conn.tooling.sobject('SourceMember').update({ Id: record.Id, RevisionCounter: 0 })));
}
catch (err) {
await lifecycleEvents_1.Lifecycle.getInstance().emitWarning(messages.getMessage('SourceStatusResetFailureError', [
scratchOrg.getOrgId(),
scratchOrg.getUsername(),
err instanceof Error ? err.message : '',
]));
}
};
exports.updateRevisionCounterToZero = updateRevisionCounterToZero;
//# sourceMappingURL=scratchOrgInfoApi.js.map