@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
1,092 lines • 59.1 kB
JavaScript
"use strict";
/*
* Copyright (c) 2020, 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
*/
/* eslint-disable class-methods-use-this */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Org = exports.sandboxIsResumable = exports.SandboxEvents = exports.OrgTypes = void 0;
const path_1 = require("path");
const fs = require("fs");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const config_1 = require("../config/config");
const configAggregator_1 = require("../config/configAggregator");
const orgUsersConfig_1 = require("../config/orgUsersConfig");
const global_1 = require("../global");
const lifecycleEvents_1 = require("../lifecycleEvents");
const logger_1 = require("../logger/logger");
const sfError_1 = require("../sfError");
const sfdc_1 = require("../util/sfdc");
const webOAuthServer_1 = require("../webOAuthServer");
const messages_1 = require("../messages");
const stateAggregator_1 = require("../stateAggregator");
const pollingClient_1 = require("../status/pollingClient");
const connection_1 = require("./connection");
const authInfo_1 = require("./authInfo");
const scratchOrgCreate_1 = require("./scratchOrgCreate");
const orgConfigProperties_1 = require("./orgConfigProperties");
const messages = new messages_1.Messages('@salesforce/core', 'org', new Map([["notADevHub", "The provided dev hub username %s is not a valid dev hub."], ["noUsernameFound", "No username found."], ["noDevHubFound", "Unable to associate this scratch org with a DevHub."], ["deleteOrgHubError", "The Dev Hub org cannot be deleted."], ["insufficientAccessToDelete", "You do not have the appropriate permissions to delete a scratch org. Please contact your Salesforce admin."], ["scratchOrgNotFound", "Attempting to delete an expired or deleted org"], ["sandboxDeleteFailed", "The sandbox org deletion failed with a result of %s."], ["sandboxNotFound", "We can't find a SandboxProcess for the sandbox %s."], ["sandboxInfoCreateFailed", "The sandbox org creation failed with a result of %s."], ["missingAuthUsername", "The sandbox %s does not have an authorized username."], ["orgPollingTimeout", "Sandbox status is %s; timed out waiting for completion."], ["NotFoundOnDevHub", "The scratch org does not belong to the dev hub username %s."], ["AuthInfoOrgIdUndefined", "AuthInfo orgId is undefined."], ["sandboxCreateNotComplete", "The sandbox creation has not completed."], ["SandboxProcessNotFoundBySandboxName", "We can't find a SandboxProcess with the SandboxName %s."], ["MultiSandboxProcessNotFoundBySandboxName", "We found more than one SandboxProcess with the SandboxName %s."], ["sandboxNotResumable", "The sandbox %s cannot resume with status of %s."]]));
var OrgTypes;
(function (OrgTypes) {
OrgTypes["Scratch"] = "scratch";
OrgTypes["Sandbox"] = "sandbox";
})(OrgTypes = exports.OrgTypes || (exports.OrgTypes = {}));
var SandboxEvents;
(function (SandboxEvents) {
SandboxEvents["EVENT_STATUS"] = "status";
SandboxEvents["EVENT_ASYNC_RESULT"] = "asyncResult";
SandboxEvents["EVENT_RESULT"] = "result";
SandboxEvents["EVENT_AUTH"] = "auth";
SandboxEvents["EVENT_RESUME"] = "resume";
})(SandboxEvents = exports.SandboxEvents || (exports.SandboxEvents = {}));
const resumableSandboxStatus = ['Activating', 'Pending', 'Pending Activation', 'Processing', 'Sampling', 'Completed'];
function sandboxIsResumable(value) {
return resumableSandboxStatus.includes(value);
}
exports.sandboxIsResumable = sandboxIsResumable;
/**
* Provides a way to manage a locally authenticated Org.
*
* **See** {@link AuthInfo}
*
* **See** {@link Connection}
*
* **See** {@link Aliases}
*
* **See** {@link Config}
*
* ```
* // Email username
* const org1: Org = await Org.create({ aliasOrUsername: 'foo@example.com' });
* // The target-org config property
* const org2: Org = await Org.create();
* // Full Connection
* const org3: Org = await Org.create({
* connection: await Connection.create({
* authInfo: await AuthInfo.create({ username: 'username' })
* })
* });
* ```
*
* **See** https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_usernames_orgs.htm
*/
class Org extends kit_1.AsyncOptionalCreatable {
/**
* @ignore
*/
constructor(options) {
super(options);
this.status = Org.Status.UNKNOWN;
this.options = options ?? {};
}
/**
* create a sandbox from a production org
* 'this' needs to be a production org with sandbox licenses available
*
* @param sandboxReq SandboxRequest options to create the sandbox with
* @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling
*/
async createSandbox(sandboxReq, options = {
wait: kit_1.Duration.minutes(6),
async: false,
interval: kit_1.Duration.seconds(30),
}) {
this.logger.debug(sandboxReq, 'CreateSandbox called with SandboxRequest');
const createResult = await this.connection.tooling.create('SandboxInfo', sandboxReq);
this.logger.debug(createResult, 'Return from calling tooling.create');
if (Array.isArray(createResult) || !createResult.success) {
throw messages.createError('sandboxInfoCreateFailed', [JSON.stringify(createResult)]);
}
const sandboxCreationProgress = await this.querySandboxProcessBySandboxInfoId(createResult.id);
this.logger.debug(sandboxCreationProgress, 'Return from calling singleRecordQuery with tooling');
const isAsync = !!options.async;
if (isAsync) {
// The user didn't want us to poll, so simply return the status
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxCreationProgress);
return sandboxCreationProgress;
}
const [wait, pollInterval] = this.validateWaitOptions(options);
this.logger.debug(sandboxCreationProgress, `create - pollStatusAndAuth sandboxProcessObj, max wait time of ${wait.minutes} minutes`);
return this.pollStatusAndAuth({
sandboxProcessObj: sandboxCreationProgress,
wait,
pollInterval,
});
}
/**
*
* @param sandboxReq SandboxRequest options to create the sandbox with
* @param sourceSandboxName the name of the sandbox that your new sandbox will be based on
* @param options Wait: The amount of time to wait before timing out, defaults to 0, Interval: The time interval between polling defaults to 30 seconds
* @returns {SandboxProcessObject} the newly created sandbox process object
*/
async cloneSandbox(sandboxReq, sourceSandboxName, options) {
sandboxReq.SourceId = (await this.querySandboxProcessBySandboxName(sourceSandboxName)).SandboxInfoId;
this.logger.debug(`Clone sandbox sourceId ${sandboxReq.SourceId}`);
return this.createSandbox(sandboxReq, options);
}
/**
* resume a sandbox creation from a production org
* 'this' needs to be a production org with sandbox licenses available
*
* @param resumeSandboxRequest SandboxRequest options to create the sandbox with
* @param options Wait: The amount of time to wait (default: 30 minutes) before timing out,
* Interval: The time interval (default: 30 seconds) between polling
*/
async resumeSandbox(resumeSandboxRequest, options = {
wait: kit_1.Duration.minutes(0),
async: false,
interval: kit_1.Duration.seconds(30),
}) {
this.logger.debug(resumeSandboxRequest, 'ResumeSandbox called with ResumeSandboxRequest');
let sandboxCreationProgress;
// seed the sandboxCreationProgress via the resumeSandboxRequest options
if (resumeSandboxRequest.SandboxName) {
sandboxCreationProgress = await this.querySandboxProcessBySandboxName(resumeSandboxRequest.SandboxName);
}
else if (resumeSandboxRequest.SandboxProcessObjId) {
sandboxCreationProgress = await this.querySandboxProcessById(resumeSandboxRequest.SandboxProcessObjId);
}
else {
throw messages.createError('sandboxNotFound', [
resumeSandboxRequest.SandboxName ?? resumeSandboxRequest.SandboxProcessObjId,
]);
}
this.logger.debug(sandboxCreationProgress, 'Return from calling singleRecordQuery with tooling');
if (!sandboxIsResumable(sandboxCreationProgress.Status)) {
throw messages.createError('sandboxNotResumable', [
sandboxCreationProgress.SandboxName,
sandboxCreationProgress.Status,
]);
}
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_RESUME, sandboxCreationProgress);
const [wait, pollInterval] = this.validateWaitOptions(options);
// if wait is 0, return the sandboxCreationProgress immediately
if (wait.seconds === 0) {
if (sandboxCreationProgress.Status === 'Completed') {
// check to see if sandbox can authenticate via sandboxAuth endpoint
const sandboxInfo = await this.sandboxSignupComplete(sandboxCreationProgress);
if (sandboxInfo) {
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_AUTH, sandboxInfo);
try {
this.logger.debug(sandboxInfo, 'sandbox signup complete');
await this.writeSandboxAuthFile(sandboxCreationProgress, sandboxInfo);
return sandboxCreationProgress;
}
catch (err) {
// eat the error, we don't want to throw an error if we can't write the file
}
}
}
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxCreationProgress);
throw messages.createError('sandboxCreateNotComplete');
}
this.logger.debug(sandboxCreationProgress, `resume - pollStatusAndAuth sandboxProcessObj, max wait time of ${wait.minutes} minutes`);
return this.pollStatusAndAuth({
sandboxProcessObj: sandboxCreationProgress,
wait,
pollInterval,
});
}
/**
* Creates a scratchOrg
* 'this' needs to be a valid dev-hub
*
* @param {options} ScratchOrgCreateOptions
* @returns {ScratchOrgCreateResult}
*/
async scratchOrgCreate(options) {
return (0, scratchOrgCreate_1.scratchOrgCreate)({ ...options, hubOrg: this });
}
/**
* Reports sandbox org creation status. If the org is ready, authenticates to the org.
*
* @param {sandboxname} string the sandbox name
* @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling
* @returns {SandboxProcessObject} the sandbox process object
*/
async sandboxStatus(sandboxname, options) {
return this.authWithRetriesByName(sandboxname, options);
}
/**
* Clean all data files in the org's data path. Usually <workspace>/.sfdx/orgs/<username>.
*
* @param orgDataPath A relative path other than "orgs/".
* @param throwWhenRemoveFails Should the remove org operations throw an error on failure?
*/
async cleanLocalOrgData(orgDataPath, throwWhenRemoveFails = false) {
let dataPath;
try {
dataPath = await this.getLocalDataDir(orgDataPath);
this.logger.debug(`cleaning data for path: ${dataPath}`);
}
catch (err) {
if (err instanceof Error && err.name === 'InvalidProjectWorkspaceError') {
// If we aren't in a project dir, we can't clean up data files.
// If the user unlink this org outside of the workspace they used it in,
// data files will be left over.
return;
}
throw err;
}
return this.manageDelete(async () => fs.promises.rmdir(dataPath), dataPath, throwWhenRemoveFails);
}
/**
* @ignore
*/
async retrieveOrgUsersConfig() {
return orgUsersConfig_1.OrgUsersConfig.create(orgUsersConfig_1.OrgUsersConfig.getOptions(this.getOrgId()));
}
/**
* Cleans up all org related artifacts including users, sandbox config (if a sandbox), source tracking files, and auth file.
*
* @param throwWhenRemoveFails Determines if the call should throw an error or fail silently.
*/
async remove(throwWhenRemoveFails = false) {
// If deleting via the access token there shouldn't be any auth config files
// so just return;
if (this.getConnection().isUsingAccessToken()) {
return Promise.resolve();
}
await this.removeSandboxConfig();
await this.removeUsers(throwWhenRemoveFails);
await this.removeUsersConfig();
// An attempt to remove this org's auth file occurs in this.removeUsersConfig. That's because this org's usersname is also
// included in the OrgUser config file.
//
// So, just in case no users are added to this org we will try the remove again.
await this.removeAuth();
await this.removeSourceTrackingFiles();
}
/**
* Check if org is a sandbox org by checking its SandboxOrgConfig.
*
*/
async isSandbox() {
return (await stateAggregator_1.StateAggregator.getInstance()).sandboxes.hasFile(this.getOrgId());
}
/**
* Check that this org is a scratch org by asking the dev hub if it knows about it.
*
* **Throws** *{@link SfError}{ name: 'NotADevHubError' }* Not a Dev Hub.
*
* **Throws** *{@link SfError}{ name: 'NoResultsError' }* No results.
*
* @param devHubUsernameOrAlias The username or alias of the dev hub org.
*/
async checkScratchOrg(devHubUsernameOrAlias) {
let aliasOrUsername = devHubUsernameOrAlias;
if (!aliasOrUsername) {
aliasOrUsername = this.configAggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB);
}
const devHubConnection = (await Org.create({ aliasOrUsername })).getConnection();
const thisOrgAuthConfig = this.getConnection().getAuthInfoFields();
const trimmedId = (0, sfdc_1.trimTo15)(thisOrgAuthConfig.orgId);
const DEV_HUB_SOQL = `SELECT CreatedDate,Edition,ExpirationDate FROM ActiveScratchOrg WHERE ScratchOrg='${trimmedId}'`;
try {
const results = await devHubConnection.query(DEV_HUB_SOQL);
if (results.records.length !== 1) {
throw new sfError_1.SfError('No results', 'NoResultsError');
}
}
catch (err) {
if (err instanceof Error && err.name === 'INVALID_TYPE') {
throw messages.createError('notADevHub', [devHubConnection.getUsername()]);
}
throw err;
}
return thisOrgAuthConfig;
}
/**
* Returns the Org object or null if this org is not affiliated with a Dev Hub (according to the local config).
*/
async getDevHubOrg() {
if (this.isDevHubOrg()) {
return this;
}
else if (this.getField(Org.Fields.DEV_HUB_USERNAME)) {
const devHubUsername = (0, ts_types_1.ensureString)(this.getField(Org.Fields.DEV_HUB_USERNAME));
return Org.create({
connection: await connection_1.Connection.create({
authInfo: await authInfo_1.AuthInfo.create({ username: devHubUsername }),
}),
});
}
}
/**
* Returns `true` if the org is a Dev Hub.
*
* **Note** This relies on a cached value in the auth file. If that property
* is not cached, this method will **always return false even if the org is a
* dev hub**. If you need accuracy, use the {@link Org.determineIfDevHubOrg} method.
*/
isDevHubOrg() {
const isDevHub = this.getField(Org.Fields.IS_DEV_HUB);
if ((0, ts_types_1.isBoolean)(isDevHub)) {
return isDevHub;
}
else {
return false;
}
}
/**
* Will delete 'this' instance remotely and any files locally
*
* @param controllingOrg username or Org that 'this.devhub' or 'this.production' refers to. AKA a DevHub for a scratch org, or a Production Org for a sandbox
*/
async deleteFrom(controllingOrg) {
if (typeof controllingOrg === 'string') {
controllingOrg = await Org.create({
aggregator: this.configAggregator,
aliasOrUsername: controllingOrg,
});
}
if (await this.isSandbox()) {
await this.deleteSandbox(controllingOrg);
}
else {
await this.deleteScratchOrg(controllingOrg);
}
}
/**
* Will delete 'this' instance remotely and any files locally
*/
async delete() {
const username = (0, ts_types_1.ensureString)(this.getUsername());
// unset any aliases referencing this org
const stateAgg = await stateAggregator_1.StateAggregator.getInstance();
const existingAliases = stateAgg.aliases.getAll(username);
await stateAgg.aliases.unsetValuesAndSave(existingAliases);
// unset any configs referencing this org
[...existingAliases, username].flatMap((name) => this.configAggregator.unsetByValue(name));
if (await this.isSandbox()) {
await this.deleteSandbox();
}
else {
await this.deleteScratchOrg();
}
}
/**
* Returns `true` if the org is a Dev Hub.
*
* Use a cached value. If the cached value is not set, then check access to the
* ScratchOrgInfo object to determine if the org is a dev hub.
*
* @param forceServerCheck Ignore the cached value and go straight to the server
* which will be required if the org flips on the dev hub after the value is already
* cached locally.
*/
async determineIfDevHubOrg(forceServerCheck = false) {
const cachedIsDevHub = this.getField(Org.Fields.IS_DEV_HUB);
if (!forceServerCheck && (0, ts_types_1.isBoolean)(cachedIsDevHub)) {
return cachedIsDevHub;
}
if (this.isDevHubOrg()) {
return true;
}
this.logger.debug('isDevHub is not cached - querying server...');
const conn = this.getConnection();
let isDevHub = false;
try {
await conn.query('SELECT Id FROM ScratchOrgInfo limit 1');
isDevHub = true;
}
catch (err) {
/* Not a dev hub */
}
const username = (0, ts_types_1.ensure)(this.getUsername());
const authInfo = await authInfo_1.AuthInfo.create({ username });
await authInfo.save({ isDevHub });
// Reset the connection with the updated auth file
this.connection = await connection_1.Connection.create({ authInfo });
return isDevHub;
}
/**
* Returns `true` if the org is a scratch org.
*
* **Note** This relies on a cached value in the auth file. If that property
* is not cached, this method will **always return false even if the org is a
* scratch org**. If you need accuracy, use the {@link Org.determineIfScratch} method.
*/
isScratch() {
const isScratch = this.getField(Org.Fields.IS_SCRATCH);
if ((0, ts_types_1.isBoolean)(isScratch)) {
return isScratch;
}
else {
return false;
}
}
/**
* Returns `true` if the org uses source tracking.
* Side effect: updates files where the property doesn't currently exist
*/
async tracksSource() {
// use the property if it exists
const tracksSource = this.getField(Org.Fields.TRACKS_SOURCE);
if ((0, ts_types_1.isBoolean)(tracksSource)) {
return tracksSource;
}
// scratch orgs with no property use tracking by default
if (await this.determineIfScratch()) {
// save true for next time to avoid checking again
await this.setTracksSource(true);
return true;
}
if (await this.determineIfSandbox()) {
// does the sandbox know about the SourceMember object?
const supportsSourceMembers = await this.supportsSourceTracking();
await this.setTracksSource(supportsSourceMembers);
return supportsSourceMembers;
}
// any other non-sandbox, non-scratch orgs won't use tracking
await this.setTracksSource(false);
return false;
}
/**
* Set the tracking property on the org's auth file
*
* @param value true or false (whether the org should use source tracking or not)
*/
async setTracksSource(value) {
const originalAuth = await authInfo_1.AuthInfo.create({ username: this.getUsername() });
return originalAuth.handleAliasAndDefaultSettings({
setDefault: false,
setDefaultDevHub: false,
setTracksSource: value,
});
}
/**
* Returns `true` if the org is a scratch org.
*
* Use a cached value. If the cached value is not set, then check
* `Organization.IsSandbox == true && Organization.TrialExpirationDate != null`
* using {@link Org.retrieveOrganizationInformation}.
*/
async determineIfScratch() {
let cache = this.getField(Org.Fields.IS_SCRATCH);
if (!cache) {
await this.updateLocalInformation();
cache = this.getField(Org.Fields.IS_SCRATCH);
}
return cache;
}
/**
* Returns `true` if the org is a sandbox.
*
* Use a cached value. If the cached value is not set, then check
* `Organization.IsSandbox == true && Organization.TrialExpirationDate == null`
* using {@link Org.retrieveOrganizationInformation}.
*/
async determineIfSandbox() {
let cache = this.getField(Org.Fields.IS_SANDBOX);
if (!cache) {
await this.updateLocalInformation();
cache = this.getField(Org.Fields.IS_SANDBOX);
}
return cache;
}
/**
* Retrieve a handful of fields from the Organization table in Salesforce. If this does not have the
* data you need, just use {@link Connection.singleRecordQuery} with `SELECT <needed fields> FROM Organization`.
*
* @returns org information
*/
async retrieveOrganizationInformation() {
return this.getConnection().singleRecordQuery('SELECT Name, InstanceName, IsSandbox, TrialExpirationDate, NamespacePrefix FROM Organization');
}
/**
* Some organization information is locally cached, such as if the org name or if it is a scratch org.
* This method populates/updates the filesystem from information retrieved from the org.
*/
async updateLocalInformation() {
const username = this.getUsername();
if (username) {
const organization = await this.retrieveOrganizationInformation();
const isScratch = organization.IsSandbox && Boolean(organization.TrialExpirationDate);
const isSandbox = organization.IsSandbox && !organization.TrialExpirationDate;
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
stateAggregator.orgs.update(username, {
[Org.Fields.NAME]: organization.Name,
[Org.Fields.INSTANCE_NAME]: organization.InstanceName,
[Org.Fields.NAMESPACE_PREFIX]: organization.NamespacePrefix,
[Org.Fields.IS_SANDBOX]: isSandbox,
[Org.Fields.IS_SCRATCH]: isScratch,
[Org.Fields.TRIAL_EXPIRATION_DATE]: organization.TrialExpirationDate,
});
await stateAggregator.orgs.write(username);
}
}
/**
* Refreshes the auth for this org's instance by calling HTTP GET on the baseUrl of the connection object.
*/
async refreshAuth() {
this.logger.debug('Refreshing auth for org.');
const requestInfo = {
url: this.getConnection().baseUrl(),
method: 'GET',
};
const conn = this.getConnection();
await conn.request(requestInfo);
}
/**
* Reads and returns the content of all user auth files for this org as an array.
*/
async readUserAuthFiles() {
const config = await this.retrieveOrgUsersConfig();
const contents = await config.read();
const thisUsername = (0, ts_types_1.ensure)(this.getUsername());
const usernames = (0, ts_types_1.ensureJsonArray)(contents.usernames ?? [thisUsername]);
return Promise.all(usernames.map((username) => {
if (username === thisUsername) {
return authInfo_1.AuthInfo.create({
username: this.getConnection().getUsername(),
});
}
else {
return authInfo_1.AuthInfo.create({ username: (0, ts_types_1.ensureString)(username) });
}
}));
}
/**
* Adds a username to the user config for this org. For convenience `this` object is returned.
*
* ```
* const org: Org = await Org.create({
* connection: await Connection.create({
* authInfo: await AuthInfo.create('foo@example.com')
* })
* });
* const userAuth: AuthInfo = await AuthInfo.create({
* username: 'bar@example.com'
* });
* await org.addUsername(userAuth);
* ```
*
* @param {AuthInfo | string} auth The AuthInfo for the username to add.
*/
async addUsername(auth) {
if (!auth) {
throw new sfError_1.SfError('Missing auth info', 'MissingAuthInfo');
}
const authInfo = (0, ts_types_1.isString)(auth) ? await authInfo_1.AuthInfo.create({ username: auth }) : auth;
this.logger.debug(`adding username ${authInfo.getFields().username}`);
const orgConfig = await this.retrieveOrgUsersConfig();
const contents = await orgConfig.read();
// TODO: This is kind of screwy because contents values can be `AnyJson | object`...
// needs config refactoring to improve
const usernames = contents.usernames ?? [];
if (!(0, ts_types_1.isArray)(usernames)) {
throw new sfError_1.SfError('Usernames is not an array', 'UnexpectedDataFormat');
}
let shouldUpdate = false;
const thisUsername = (0, ts_types_1.ensure)(this.getUsername());
if (!usernames.includes(thisUsername)) {
usernames.push(thisUsername);
shouldUpdate = true;
}
const username = authInfo.getFields().username;
if (username) {
usernames.push(username);
shouldUpdate = true;
}
if (shouldUpdate) {
orgConfig.set('usernames', usernames);
await orgConfig.write();
}
return this;
}
/**
* Removes a username from the user config for this object. For convenience `this` object is returned.
*
* **Throws** *{@link SfError}{ name: 'MissingAuthInfoError' }* Auth info is missing.
*
* @param {AuthInfo | string} auth The AuthInfo containing the username to remove.
*/
async removeUsername(auth) {
if (!auth) {
throw new sfError_1.SfError('Missing auth info', 'MissingAuthInfoError');
}
const authInfo = (0, ts_types_1.isString)(auth) ? await authInfo_1.AuthInfo.create({ username: auth }) : auth;
this.logger.debug(`removing username ${authInfo.getFields().username}`);
const orgConfig = await this.retrieveOrgUsersConfig();
const contents = await orgConfig.read();
const targetUser = authInfo.getFields().username;
const usernames = (contents.usernames ?? []);
contents.usernames = usernames.filter((username) => username !== targetUser);
await orgConfig.write();
return this;
}
/**
* set the sandbox config related to this given org
*
* @param orgId {string} orgId of the sandbox
* @param config {SandboxFields} config of the sandbox
*/
async setSandboxConfig(orgId, config) {
(await stateAggregator_1.StateAggregator.getInstance()).sandboxes.set(orgId, config);
return this;
}
/**
* get the sandbox config for the given orgId
*
* @param orgId {string} orgId of the sandbox
*/
async getSandboxConfig(orgId) {
return (await stateAggregator_1.StateAggregator.getInstance()).sandboxes.read(orgId);
}
/**
* Retrieves the highest api version that is supported by the target server instance. If the apiVersion configured for
* Sfdx is greater than the one returned in this call an api version mismatch occurs. In the case of the CLI that
* results in a warning.
*/
async retrieveMaxApiVersion() {
return this.getConnection().retrieveMaxApiVersion();
}
/**
* Returns the admin username used to create the org.
*/
getUsername() {
return this.getConnection().getUsername();
}
/**
* Returns the orgId for this org.
*/
getOrgId() {
return this.orgId ?? this.getField(Org.Fields.ORG_ID);
}
/**
* Returns for the config aggregator.
*/
getConfigAggregator() {
return this.configAggregator;
}
/**
* Returns an org field. Returns undefined if the field is not set or invalid.
*/
getField(key) {
/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
// @ts-ignore Legacy. We really shouldn't be doing this.
const ownProp = this[key];
if (ownProp && typeof ownProp !== 'function')
return ownProp;
// @ts-ignore
return this.getConnection().getAuthInfoFields()[key];
/* eslint-enable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
}
/**
* Returns a map of requested fields.
*/
getFields(keys) {
const json = {};
return keys.reduce((map, key) => {
map[key] = this.getField(key);
return map;
}, json);
}
/**
* Returns the JSForce connection for the org.
* side effect: If you pass it an apiVersion, it will set it on the Org
* so that future calls to getConnection() will also use that version.
*
* @param apiVersion The API version to use for the connection.
*/
getConnection(apiVersion) {
if (apiVersion) {
if (this.connection.getApiVersion() === apiVersion) {
this.logger.warn(`Default API version is already ${apiVersion}`);
}
else {
this.connection.setApiVersion(apiVersion);
}
}
return this.connection;
}
async supportsSourceTracking() {
if (this.isScratch()) {
return true;
}
try {
await this.getConnection().tooling.sobject('SourceMember').describe();
return true;
}
catch (err) {
if (err.message.includes('The requested resource does not exist')) {
return false;
}
throw err;
}
}
/**
* query SandboxProcess via sandbox name
*
* @param name SandboxName to query for
*/
async querySandboxProcessBySandboxName(name) {
return this.querySandboxProcess(`SandboxName='${name}'`);
}
/**
* query SandboxProcess via SandboxInfoId
*
* @param id SandboxInfoId to query for
*/
async querySandboxProcessBySandboxInfoId(id) {
return this.querySandboxProcess(`SandboxInfoId='${id}'`);
}
/**
* query SandboxProcess via Id
*
* @param id SandboxProcessId to query for
*/
async querySandboxProcessById(id) {
return this.querySandboxProcess(`Id='${id}'`);
}
/**
* query SandboxProcess via SandboxOrganization (sandbox Org ID)
*
* @param sandboxOrgId SandboxOrganization ID to query for
*/
async querySandboxProcessByOrgId(sandboxOrgId) {
// Must query with a 15 character Org ID
return this.querySandboxProcess(`SandboxOrganization='${(0, sfdc_1.trimTo15)(sandboxOrgId)}'`);
}
/**
* Initialize async components.
*/
async init() {
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
this.logger = (await logger_1.Logger.child('Org')).getRawLogger();
this.configAggregator = this.options.aggregator ? this.options.aggregator : await configAggregator_1.ConfigAggregator.create();
if (!this.options.connection) {
if (this.options.aliasOrUsername == null) {
this.configAggregator = this.getConfigAggregator();
const aliasOrUsername = this.options.isDevHub
? this.configAggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB)
: this.configAggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG);
this.options.aliasOrUsername = aliasOrUsername ?? undefined;
}
const username = stateAggregator.aliases.resolveUsername(this.options.aliasOrUsername);
if (!username) {
throw messages.createError('noUsernameFound');
}
this.connection = await connection_1.Connection.create({
// If no username is provided or resolvable from an alias, AuthInfo will throw an SfError.
authInfo: await authInfo_1.AuthInfo.create({ username, isDevHub: this.options.isDevHub }),
});
}
else {
this.connection = this.options.connection;
}
this.orgId = this.getField(Org.Fields.ORG_ID);
}
/**
* **Throws** *{@link SfError}{ name: 'NotSupportedError' }* Throws an unsupported error.
*/
// eslint-disable-next-line class-methods-use-this
getDefaultOptions() {
throw new sfError_1.SfError('Not Supported', 'NotSupportedError');
}
async getLocalDataDir(orgDataPath) {
const rootFolder = await config_1.Config.resolveRootFolder(false);
return (0, path_1.join)(rootFolder, global_1.Global.SFDX_STATE_FOLDER, orgDataPath ? orgDataPath : 'orgs');
}
/**
* Gets the sandboxProcessObject and then polls for it to complete.
*
* @param sandboxProcessName sanbox process name
* @param options { wait?: Duration; interval?: Duration }
* @returns {SandboxProcessObject} The SandboxProcessObject for the sandbox
*/
async authWithRetriesByName(sandboxProcessName, options) {
return this.authWithRetries(await this.queryLatestSandboxProcessBySandboxName(sandboxProcessName), options);
}
/**
* Polls the sandbox org for the sandboxProcessObject.
*
* @param sandboxProcessObj: The in-progress sandbox signup request
* @param options { wait?: Duration; interval?: Duration }
* @returns {SandboxProcessObject}
*/
async authWithRetries(sandboxProcessObj, options = {
wait: kit_1.Duration.minutes(0),
interval: kit_1.Duration.seconds(30),
}) {
const [wait, pollInterval] = this.validateWaitOptions(options);
this.logger.debug(sandboxProcessObj, `AuthWithRetries sandboxProcessObj, max wait time of ${wait.minutes} minutes`);
return this.pollStatusAndAuth({
sandboxProcessObj,
wait,
pollInterval,
});
}
/**
* Query the sandbox for the SandboxProcessObject by sandbox name
*
* @param sandboxName The name of the sandbox to query
* @returns {SandboxProcessObject} The SandboxProcessObject for the sandbox
*/
async queryLatestSandboxProcessBySandboxName(sandboxNameIn) {
const { tooling } = this.getConnection();
this.logger.debug(`QueryLatestSandboxProcessBySandboxName called with SandboxName: ${sandboxNameIn}`);
const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE SandboxName='${sandboxNameIn}' AND Status != 'D' ORDER BY CreatedDate DESC LIMIT 1`;
const queryResult = await tooling.query(queryStr);
this.logger.debug(queryResult, 'Return from calling queryToolingApi');
if (queryResult?.records?.length === 1) {
return queryResult.records[0];
}
else if (queryResult.records && queryResult.records.length > 1) {
throw messages.createError('MultiSandboxProcessNotFoundBySandboxName', [sandboxNameIn]);
}
else {
throw messages.createError('SandboxProcessNotFoundBySandboxName', [sandboxNameIn]);
}
}
// eslint-disable-next-line class-methods-use-this
async queryProduction(org, field, value) {
return org.connection.singleRecordQuery(`SELECT SandboxInfoId FROM SandboxProcess WHERE ${field} ='${value}' AND Status NOT IN ('D', 'E')`, { tooling: true });
}
async destroySandbox(org, id) {
return org.getConnection().tooling.delete('SandboxInfo', id);
}
async destroyScratchOrg(org, id) {
return org.getConnection().delete('ActiveScratchOrg', id);
}
/**
* this method will delete the sandbox org from the production org and clean up any local files
*
* @param prodOrg - Production org associated with this sandbox
* @private
*/
async deleteSandbox(prodOrg) {
const sandbox = await this.getSandboxConfig(this.getOrgId());
prodOrg ??= await Org.create({
aggregator: this.configAggregator,
aliasOrUsername: sandbox?.prodOrgUsername,
});
let sandboxInfoId = sandbox?.sandboxInfoId;
if (!sandboxInfoId) {
let result;
try {
// grab sandboxName from config or try to calculate from the sandbox username
const sandboxName = sandbox?.sandboxName ?? (this.getUsername() ?? '').split(`${prodOrg.getUsername()}.`)[1];
if (!sandboxName) {
this.logger.debug('Sandbox name is not available');
// jump to query by orgId
throw new Error();
}
this.logger.debug(`attempting to locate sandbox with sandbox ${sandboxName}`);
try {
result = await this.queryProduction(prodOrg, 'SandboxName', sandboxName);
}
catch (err) {
this.logger.debug(`Failed to find sandbox with sandbox name: ${sandboxName}`);
// jump to query by orgId
throw err;
}
}
catch {
// if an error is thrown, don't panic yet. we'll try querying by orgId
const trimmedId = (0, sfdc_1.trimTo15)(this.getOrgId());
this.logger.debug(`defaulting to trimming id from ${this.getOrgId()} to ${trimmedId}`);
try {
result = await this.queryProduction(prodOrg, 'SandboxOrganization', trimmedId);
sandboxInfoId = result.SandboxInfoId;
}
catch {
// eating exceptions when trying to find sandbox process record by orgId
// allows idempotent cleanup of sandbox orgs
this.logger.debug(`Failed find a SandboxProcess for the sandbox org: ${this.getOrgId()}`);
}
}
}
if (sandboxInfoId) {
const deleteResult = await this.destroySandbox(prodOrg, sandboxInfoId);
this.logger.debug(deleteResult, 'Return from calling tooling.delete');
}
// cleanup remaining artifacts
await this.remove();
}
/**
* If this Org is a scratch org, calling this method will delete the scratch org from the DevHub and clean up any local files
*
* @param devHub - optional DevHub Org of the to-be-deleted scratch org
* @private
*/
async deleteScratchOrg(devHub) {
// if we didn't get a devHub, we'll get it from the this org
devHub ??= await this.getDevHubOrg();
if (!devHub) {
throw messages.createError('noDevHubFound');
}
if (devHub.getOrgId() === this.getOrgId()) {
// we're attempting to delete a DevHub
throw messages.createError('deleteOrgHubError');
}
try {
const devHubConn = devHub.getConnection();
const username = this.getUsername();
const activeScratchOrgRecordId = (await devHubConn.singleRecordQuery(`SELECT Id FROM ActiveScratchOrg WHERE SignupUsername='${username}'`)).Id;
this.logger.trace(`found matching ActiveScratchOrg with SignupUsername: ${username}. Deleting...`);
await this.destroyScratchOrg(devHub, activeScratchOrgRecordId);
await this.remove();
}
catch (err) {
this.logger.info(err instanceof Error ? err.message : err);
if (err instanceof Error && (err.name === 'INVALID_TYPE' || err.name === 'INSUFFICIENT_ACCESS_OR_READONLY')) {
// most likely from devHubConn.delete
this.logger.info('Insufficient privilege to access ActiveScratchOrgs.');
throw messages.createError('insufficientAccessToDelete');
}
if (err instanceof Error && err.name === connection_1.SingleRecordQueryErrors.NoRecords) {
// most likely from singleRecordQuery
this.logger.info('The above error can be the result of deleting an expired or already deleted org.');
this.logger.info('attempting to cleanup the auth file');
await this.removeAuth();
throw messages.createError('scratchOrgNotFound');
}
throw err;
}
}
/**
* Delete an auth info file from the local file system and any related cache information for
* this Org. You don't want to call this method directly. Instead consider calling Org.remove()
*/
async removeAuth() {
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
const username = this.getUsername();
// If there is no username, it has already been removed from the globalInfo.
// We can skip the unset and just ensure that globalInfo is updated.
if (username) {
this.logger.debug(`Removing auth for user: ${username}`);
this.logger.debug(`Clearing auth cache for user: ${username}`);
await stateAggregator.orgs.remove(username);
}
}
/**
* Deletes the users config file
*/
async removeUsersConfig() {
const config = await this.retrieveOrgUsersConfig();
if (await config.exists()) {
this.logger.debug(`Removing org users config at: ${config.getPath()}`);
await config.unlink();
}
}
async manageDelete(cb, dirPath, throwWhenRemoveFails) {
return cb().catch((e) => {
if (throwWhenRemoveFails) {
throw e;
}
else {
this.logger.warn(`failed to read directory ${dirPath}`);
return;
}
});
}
/**
* Remove the org users auth file.
*
* @param throwWhenRemoveFails true if manageDelete should throw or not if the deleted fails.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async removeUsers(throwWhenRemoveFails) {
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
this.logger.debug(`Removing users associate with org: ${this.getOrgId()}`);
const config = await this.retrieveOrgUsersConfig();
this.logger.debug(`using path for org users: ${config.getPath()}`);
const usernames = (await this.readUserAuthFiles()).map((auth) => auth.getFields().username).filter(ts_types_1.isString);
await Promise.all(usernames.map(async (username) => {
const orgForUser = username === this.getUsername()
? this
: await Org.create({
connection: await connection_1.Connection.create({ authInfo: await authInfo_1.AuthInfo.create({ username }) }),
});
const orgType = this.isDevHubOrg() ? orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB : orgConfigProperties_1.OrgConfigProperties.TARGET_ORG;
const configInfo = orgForUser.configAggregator.getInfo(orgType);
const needsConfigUpdate = (configInfo.isGlobal() || configInfo.isLocal()) &&
(configInfo.value === username || stateAggregator.aliases.get(configInfo.value) === username);
return [
orgForUser.removeAuth(),
needsConfigUpdate ? config_1.Config.update(configInfo.isGlobal(), orgType, undefined) : undefined,
].filter(Boolean);
}));
// now that we're done with all the aliases, we can unset those
await stateAggregator.aliases.unsetValuesAndSave(usernames);
}
async removeSandboxConfig() {
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
await stateAggregator.sandboxes.remove(this.getOrgId());
}
async writeSandboxAuthFile(sandboxProcessObj, sandboxRes) {
this.logger.debug(sandboxProcessObj, 'writeSandboxAuthFile sandboxProcessObj');
this.logger.debug(sandboxRes, 'writeSandboxAuthFile sandboxRes');
if (sandboxRes.authUserName) {
const productionAuthFields = this.connection.getAuthInfoFields();
this.logger.debug(productionAuthFields, 'Result from getAuthInfoFields: AuthFields');
// let's do headless auth via jwt (if we have privateKey) or web auth
const oauth2Options = {
loginUrl: sandboxRes.loginUrl,
instanceUrl: sandboxRes.instanceUrl,
username: sandboxRes.authUserName,
};
// If we don't have a privateKey then we assume it's web auth.
if (!productionAuthFields.privateKey) {
oauth2Options.redirectUri = `http://localhost:${await webOAuthServer_1.WebOAuthServer.determineOauthPort()}/OauthRedirect`;
oauth2Options.authCode = sandboxRes.authCode;
}
else {
oauth2Options.privateKey = productionAuthFields.privateKey;
oauth2Options.clientId = productionAuthFields.clientId;
}
const authInfo = await authInfo_1.AuthInfo.create({
username: sandboxRes.authUserName,
oauth2Options,
parentUsername: productionAuthFields.username,
});
this.logger.debug({
sandboxResAuthUsername: sandboxRes.authUserName,
productionAuthFieldsUsername: productionAuthFields.username,
...oauth2Options,
}, 'Creating AuthInfo for sandbox');
// save auth info for new sandbox
await authInfo.save();
const sandboxOrgId = authInfo.getFields().orgId;
if (!sandboxOrgId) {
throw messages.createError('AuthInfoOrgIdUndefined');
}
// set the sandbox config value
const sfSandbox = {
sandboxUsername: sandboxRes.authUserName,
sandboxOrgId,
prodOrgUsername: this.getUsername(),
sandboxName: sandboxProcessObj.SandboxName,
sandboxProcessId: sandboxProcessObj.Id,
sandboxInfoId: sandboxProcessObj.SandboxInfoId,
timestamp: new Date().toISOString(),
};
await this.setSandboxConfig(sandboxOrgId, sfSandbox);
await (await stateAggregator_1.StateAggregator.getInstance()).sandboxes.write(sandboxOrgId);
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_RESULT, {
sandboxProcessObj,
sandboxRes,
});
}
else {
// no authed sandbox user, error
throw messages.createError('missingAuthUsername', [sandboxProcessObj.SandboxName]);
}
}
async pollStatusAndAuth(options) {
this.logger.debug(options, 'PollStatusAndAuth called with SandboxProcessObject');
let remainingWait = options.wait;
let waitingOnAuth = false;
const pollingClient = await pollingClient_1.PollingClient.create({
poll: async () => {
const sandboxProcessObj = await this.querySandboxProcessBySandboxInfoId(options.sandboxProcessObj.SandboxInfoId);
// check to see if sandbox can authenticate via sandboxAuth endpoint
const sandboxInfo = await this.sandboxSignupComplete(sandboxProcessObj);
if (sandboxInfo) {
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_AUTH, sandb