@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
1,634 lines • 75.8 kB
JavaScript
"use strict";
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable class-methods-use-this */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Org = exports.SandboxEvents = exports.OrgTypes = void 0;
exports.sandboxIsResumable = sandboxIsResumable;
const path = __importStar(require("node:path"));
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const fs_1 = require("../fs/fs");
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/stateAggregator");
const pollingClient_1 = require("../status/pollingClient");
const connection_1 = require("./connection");
const determineOrg_1 = require("./determineOrg");
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."], ["sandboxInfoRefreshFailed", "The sandbox org refresh 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."], ["sandboxAuthNotComplete", "The sandbox org completed successfully, but the local authentication file could not be written: %s. Run \"org resume sandbox\" again to retry authentication."], ["sandboxAuthIncomplete", "The sandbox org completed, but the authentication endpoint is not ready yet (INVALID_STATUS: %s). Run \"org resume sandbox\" to retry."], ["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."], ["FrontdoorURLError", "Failed to generate a frontdoor URL."], ["FlowIdNotFound", "ID not found for Flow %s."], ["CustomObjectIdNotFound", "ID not found for custom object %s."], ["ApexClassIdNotFound", "ID not found for Apex class %s."]]));
var OrgTypes;
(function (OrgTypes) {
OrgTypes["Scratch"] = "scratch";
OrgTypes["Sandbox"] = "sandbox";
})(OrgTypes || (exports.OrgTypes = 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["EVENT_MULTIPLE_SBX_PROCESSES"] = "multipleMatchingSbxProcesses";
})(SandboxEvents || (exports.SandboxEvents = SandboxEvents = {}));
const resumableSandboxStatus = ['Activating', 'Pending', 'Pending Activation', 'Processing', 'Sampling', 'Completed'];
function sandboxIsResumable(value) {
return resumableSandboxStatus.includes(value);
}
const sandboxProcessFields = [
'Id',
'Status',
'SandboxName',
'SandboxInfoId',
'LicenseType',
'CreatedDate',
'CopyProgress',
'SandboxOrganization',
'SourceId',
'Description',
'EndDate',
'Features',
];
const sandboxInfoFields = [
'Id',
'IsDeleted',
'CreatedDate',
'CreatedById',
'LastModifiedDate',
'LastModifiedById',
'SandboxName',
'LicenseType',
'TemplateId',
'HistoryDays',
'CopyChatter',
'AutoActivate',
'ApexClassId',
'Description',
'SourceId',
'ActivationUserGroupId',
'Features',
'PostCopyConfig',
];
/**
* 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 {
status = Org.Status.UNKNOWN;
configAggregator;
// Initialized in create
logger;
connection;
options;
orgId;
/**
* @ignore
*/
constructor(options) {
super(options);
this.options = options ?? {};
}
/**
* Generate a URL to a metadata UI builder/setup section in an org.
*
* Bot: open in Agentforce Builder
* ApexPage: opens page
* Flow: open in Flow Builder
* FlexiPage: open in Lightning App Builder
* CustomObject: open in Object Manager
* ApexClass: open in Setup -> Apex Classes UI
*
* if you pass any other metadata type you'll get a path to Lightning App Builder
*
* @example
* // use SDR resolver:
* import { MetadataResolver } from '@salesforce/source-deploy-retrieve';
*
* const metadataResolver = new MetadataResolver();
* const components = metadataResolver.getComponentsFromPath(filePath);
* const typeName = components[0]?.type?.name;
*
* const metadataBuilderUrl = await org.getMetadataUIURL(typeName, filePath);
*
* @typeName Bot | ApexPage | Flow | FlexiPage | CustomObject | ApexClass
* @file Absolute file path to the metadata file
*/
async getMetadataUIURL(typeName, file) {
const botFileNameToId = async (conn, filePath) => (await conn.singleRecordQuery(`SELECT id FROM BotDefinition WHERE DeveloperName='${path.basename(filePath, '.bot-meta.xml')}'`)).Id;
/** query flexipage via toolingAPI to get its ID (starts with 0M0) */
const flexiPageFilenameToId = async (conn, filePath) => (await conn.singleRecordQuery(`SELECT id FROM flexipage WHERE DeveloperName='${path.basename(filePath, '.flexipage-meta.xml')}'`, { tooling: true })).Id;
/** query the rest API to turn a flow's filepath into a FlowId (starts with 301) */
const flowFileNameToId = async (conn, filePath) => {
try {
const flow = await conn.singleRecordQuery(`SELECT DurableId FROM FlowVersionView WHERE FlowDefinitionView.ApiName = '${path.basename(filePath, '.flow-meta.xml')}' ORDER BY VersionNumber DESC LIMIT 1`);
return flow.DurableId;
}
catch (error) {
throw messages.createError('FlowIdNotFound', [filePath]);
}
};
const customObjectFileNameToId = async (conn, filePath) => {
try {
const customObject = await conn.singleRecordQuery(`SELECT Id FROM CustomObject WHERE DeveloperName = '${path.basename(filePath.replace(/__c/g, ''), '.object-meta.xml')}'`, {
tooling: true,
});
return customObject.Id;
}
catch (error) {
throw messages.createError('CustomObjectIdNotFound', [filePath]);
}
};
const apexClassFileNameToId = async (conn, filePath) => {
try {
const apexClass = await conn.singleRecordQuery(`SELECT Id FROM ApexClass WHERE Name = '${path.basename(filePath, '.cls')}'`, {
tooling: true,
});
return apexClass.Id;
}
catch (error) {
throw messages.createError('ApexClassIdNotFound', [filePath]);
}
};
let redirectUri = '';
switch (typeName) {
case 'ApexClass':
redirectUri = `lightning/setup/ApexClasses/page?address=%2F${await apexClassFileNameToId(this.connection, file)}`;
break;
case 'CustomObject':
redirectUri = `lightning/setup/ObjectManager/${await customObjectFileNameToId(this.connection, file)}/Details/view`;
break;
case 'Bot':
redirectUri = `/AiCopilot/copilotStudio.app#/copilot/builder?copilotId=${await botFileNameToId(this.connection, file)}`;
break;
case 'ApexPage':
redirectUri = `/apex/${path.basename(file).replace('.page-meta.xml', '').replace('.page', '')}`;
break;
case 'Flow':
redirectUri = `/builder_platform_interaction/flowBuilder.app?flowId=${await flowFileNameToId(this.connection, file)}`;
break;
case 'FlexiPage':
redirectUri = `/visualEditor/appBuilder.app?pageId=${await flexiPageFilenameToId(this.connection, file)}`;
break;
default:
redirectUri = '/lightning/setup/FlexiPageList/home';
break;
}
return this.getFrontDoorUrl(redirectUri);
}
/**
* Get a Frontdoor URL
*
* This uses the UI Bridge API to generate a single-use Frontdoor URL:
* https://help.salesforce.com/s/articleView?id=xcloud.frontdoor_singleaccess.htm&type=5
*/
async getFrontDoorUrl(redirectUri) {
// the `singleaccess` endpoint returns 403 when using an expired token and jsforce only triggers a token refresh on 401 so we check if it's valid first
await this.refreshAuth();
const singleAccessUrl = new URL('/services/oauth2/singleaccess', this.connection.instanceUrl);
if (redirectUri) {
singleAccessUrl.searchParams.append('redirect_uri', redirectUri);
}
const response = await this.connection.requestGet(singleAccessUrl.toString());
if (response.frontdoor_uri)
return response.frontdoor_uri;
throw new sfError_1.SfError(messages.getMessage('FrontdoorURLError')).setData(response);
}
/**
* 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');
// The tooling create API expects a string, not an array, so convert.
if (sandboxReq.Features && Array.isArray(sandboxReq.Features)) {
sandboxReq.Features = JSON.stringify(sandboxReq.Features);
}
if (sandboxReq.PostCopyConfig && Array.isArray(sandboxReq.PostCopyConfig)) {
sandboxReq.PostCopyConfig = JSON.stringify(sandboxReq.PostCopyConfig);
}
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,
});
}
/**
* Refresh (update) a sandbox from a production org.
* 'this' needs to be a production org with sandbox licenses available
*
* @param sandboxInfo SandboxInfo to update the sandbox with
* @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling
*/
async refreshSandbox(sandboxInfo, options = {
wait: kit_1.Duration.minutes(6),
async: false,
interval: kit_1.Duration.seconds(30),
}) {
this.logger.debug(sandboxInfo, 'RefreshSandbox called with SandboxInfo');
if (sandboxInfo.PostCopyConfig && Array.isArray(sandboxInfo.PostCopyConfig)) {
sandboxInfo.PostCopyConfig = JSON.stringify(sandboxInfo.PostCopyConfig);
}
const refreshResult = await this.connection.tooling.update('SandboxInfo', sandboxInfo);
this.logger.debug(refreshResult, 'Return from calling tooling.update');
if (!refreshResult.success) {
throw messages.createError('sandboxInfoRefreshFailed', [JSON.stringify(refreshResult)]);
}
const soql = `SELECT ${sandboxProcessFields.join(',')} FROM SandboxProcess WHERE SandboxName='${sandboxInfo.SandboxName}' ORDER BY CreatedDate DESC`;
const sbxProcessObjects = (await this.connection.tooling.query(soql)).records.filter((item) => !item.Status.startsWith('Del'));
this.logger.debug(sbxProcessObjects, `SandboxProcesses for ${sandboxInfo.SandboxName}`);
// throw if none found
if (sbxProcessObjects?.length === 0) {
throw new Error(`No SandboxProcesses found for: ${sandboxInfo.SandboxName}`);
}
const sandboxRefreshProgress = sbxProcessObjects[0];
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, sandboxRefreshProgress);
return sandboxRefreshProgress;
}
const [wait, pollInterval] = this.validateWaitOptions(options);
this.logger.debug(sandboxRefreshProgress, `refresh - pollStatusAndAuth sandboxProcessObj, max wait time of ${wait.minutes} minutes`);
return this.pollStatusAndAuth({
sandboxProcessObj: sandboxRefreshProgress,
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) {
const SourceId = (await this.querySandboxProcessBySandboxName(sourceSandboxName)).SandboxInfoId;
this.logger.debug(`Clone sandbox sourceId ${SourceId}`);
return this.createSandbox({ ...sandboxReq, SourceId }, options);
}
/**
* Resume a sandbox create or refresh from a production org.
* `this` needs to be a production org with sandbox licenses available.
*
* @param resumeSandboxRequest SandboxRequest options to create/refresh the sandbox with
* @param options Wait: The amount of time to wait (default: 0 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.SandboxProcessObjId) {
sandboxCreationProgress = await this.querySandboxProcessById(resumeSandboxRequest.SandboxProcessObjId);
}
else if (resumeSandboxRequest.SandboxName) {
try {
// There can be multiple sandbox processes returned when querying by name. Use the most recent
// process and fire a warning event with all processes.
sandboxCreationProgress = await this.querySandboxProcessBySandboxName(resumeSandboxRequest.SandboxName);
}
catch (err) {
if (err instanceof sfError_1.SfError && err.name === 'SingleRecordQuery_MultipleRecords' && err.data) {
const sbxProcesses = err.data;
// 0 index will always be the most recently created process since the query sorts on created date desc.
sandboxCreationProgress = sbxProcesses[0];
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, sbxProcesses);
}
else {
throw err;
}
}
}
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) {
const authError = err instanceof Error ? err : new Error(String(err));
this.logger.debug('Failed to write sandbox auth file', authError.message);
const sfError = messages.createError('sandboxAuthNotComplete', [authError.message]);
sfError.cause = authError;
throw sfError;
}
}
}
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_1.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 ?? '<undefined>'}'`;
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) {
const resolvedOrg = typeof controllingOrg === 'string'
? await Org.create({
aggregator: this.configAggregator,
aliasOrUsername: controllingOrg,
})
: controllingOrg;
if (await this.isSandbox()) {
await this.deleteSandbox(resolvedOrg);
}
else {
await this.deleteScratchOrg(resolvedOrg);
}
}
/**
* 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
await Promise.all([...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() {
const cache = this.getField(Org.Fields.IS_SCRATCH);
if (cache !== undefined) {
return cache;
}
const updated = await this.updateLocalInformation();
return updated?.isScratch === true;
}
/**
* 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() {
const cache = this.getField(Org.Fields.IS_SANDBOX);
if (cache !== undefined) {
return cache;
}
const updated = await this.updateLocalInformation();
return updated?.isSandbox === true;
}
/**
* 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, OrganizationType FROM Organization');
}
async querySandboxInfo(by) {
if (by.id) {
// Validate that the ID is a valid Salesforce ID
if (!(0, sfdc_1.validateSalesforceId)(by.id)) {
throw new sfError_1.SfError(`Invalid Salesforce ID format: ${by.id}`, 'InvalidSalesforceId');
}
}
const whereClause = by.id ? `Id='${by.id}'` : `SandboxName='${by.name ?? '<undefined>'}'`;
const buildSoql = (fields) => `SELECT ${fields.join(',')} FROM SandboxInfo WHERE ${whereClause} ORDER BY CreatedDate DESC`;
let soql = buildSoql(sandboxInfoFields);
let records;
try {
records = (await this.connection.tooling.query(soql)).records;
}
catch (err) {
// PostCopyConfig is only available on orgs running a release that exposes the field on
// the Tooling API. Older orgs reject the SOQL with INVALID_FIELD ("No such column ...").
// Retry once without PostCopyConfig so existing customers aren't broken.
const message = err instanceof Error ? err.message : String(err);
const isInvalidField = (err instanceof Error && err.name === 'INVALID_FIELD') || message.includes('No such column');
if (isInvalidField && message.includes('PostCopyConfig')) {
this.logger.debug('PostCopyConfig not supported on this org; retrying SandboxInfo query without it');
soql = buildSoql(sandboxInfoFields.filter((f) => f !== 'PostCopyConfig'));
records = (await this.connection.tooling.query(soql)).records;
}
else {
throw err;
}
}
const result = records.filter((item) => !item.IsDeleted);
if (result.length === 0) {
throw new sfError_1.SfError(`No record found for ${soql}`, connection_1.SingleRecordQueryErrors.NoRecords);
}
if (result.length > 1) {
const err = new sfError_1.SfError('The query returned more than 1 record', connection_1.SingleRecordQueryErrors.MultipleRecords);
err.data = result;
throw err;
}
return result[0];
}
/**
* 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 [stateAggregator, organization] = await Promise.all([
stateAggregator_1.StateAggregator.getInstance(),
this.retrieveOrganizationInformation(),
]);
const updateFields = {
[Org.Fields.NAME]: organization.Name,
[Org.Fields.INSTANCE_NAME]: organization.InstanceName,
[Org.Fields.NAMESPACE_PREFIX]: organization.NamespacePrefix,
[Org.Fields.IS_SANDBOX]: organization.IsSandbox && !organization.TrialExpirationDate,
[Org.Fields.IS_SCRATCH]: organization.IsSandbox && Boolean(organization.TrialExpirationDate),
[Org.Fields.TRIAL_EXPIRATION_DATE]: organization.TrialExpirationDate,
[Org.Fields.ORG_EDITION]: organization.OrganizationType,
};
stateAggregator.orgs.update(username, updateFields);
await stateAggregator.orgs.write(username);
return updateFields;
}
}
/**
* Executes a GET request on the baseUrl to force an auth refresh.
* This is useful for the raw methods (request, requestRaw) that use the accessToken directly and don't handle refreshes.
*
* This method issues a request using the current access token to check if it is still valid.
* If the request returns 200, no refresh happens, and we keep the token.
* If it returns 401, jsforce will request a new token and set it in the connection instance.
*/
async refreshAuth() {
await this.getConnection().refreshAuth();
}
/**
* 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) => authInfo_1.AuthInfo.create({
username: username === thisUsername ? this.getConnection().getUsername() : (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 ?? '<undefined>'}`);
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 (!Array.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 ?? '<undefined>'}`);
const orgConfig = await this.retrieveOrgUsersConfig();
const contents = await orgConfig.read();
const targetUser = authInfo.getFields().username;
const usernames = (contents.usernames ?? []).filter((username) => username !== targetUser);
orgConfig.set('usernames', usernames);
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-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-assignment */
}
/**
* Returns a map of requested fields.
*/
getFields(keys) {
return Object.fromEntries(keys.map((key) => [key, this.getField(key)]));
}
/**
* 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() {
this.logger = (await logger_1.Logger.child('Org')).getRawLogger();
this.configAggregator = this.options.aggregator ?? (await configAggregator_1.ConfigAggregator.create());
if (!this.options.connection) {
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
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);
try {
await (0, determineOrg_1.determineOrg)(this.getConnection().getAuthInfo());
}
catch {
// best effort — connection may not support getAuthInfo (e.g. passed-in mock)
}
}
/**
* **Throws** *{@link SfError}{ name: 'NotSupportedError' }* Throws an unsupported error.
*/
getDefaultOptions() {
throw new sfError_1.SfError('Not Supported', 'NotSupportedError');
}
async getLocalDataDir(orgDataPath) {
const rootFolder = await config_1.Config.resolveRootFolder(false);
return path.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 ${sandboxProcessFields.join(',')} 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]);
}
}
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());
const resolvedProdOrg = 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(`${resolvedProdOrg.getUsername() ?? '<undefined>'}.`)[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(resolvedProdOrg, '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(resolvedProdOrg, '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(resolvedProdOrg, 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
const resolvedDevHub = devHub ?? (await this.getDevHubOrg());
if (!resolvedDevHub) {
throw messages.createError('noDevHubFound');
}
if (resolvedDevHub.getOrgId() === this.getOrgId()) {
// we're attempting to delete a DevHub
throw messages.createError('deleteOrgHubError');
}
try {
const devHubConn = resolvedDevHub.getConnection();
const username = (0, ts_types_1.ensureString)(this.getUsername(), 'org is missing username');
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(resolvedDevHub, 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;
}
// Before creating the AuthInfo, delete any existing auth files for the sandbox.
// This is common when refreshing sandboxes, and will cause AuthInfo to throw
// because it doesn't want to overwrite existing auth files.
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
try {
await stateAggregator.orgs.read(sandboxRes.authUserName);
await stateAggregator.orgs.remove(sandboxRes.authUserName);
}
catch (e) {
// ignore since this is only for deleting existing auth files.
}
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 sandbox
await authInfo.save({
isScratch: false,
isSandbox: true,
});
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.querySandboxProcessById(options.sandboxProcessObj.Id);
let sandboxInfo;
try {
// check to see if sandbox can authenticate via sandboxAuth endpoint
sandboxInfo = await this.sandboxSignupComplete(sandboxProcessObj);
}
catch (err) {
const error = err instanceof Error ? err : sfError_1.SfError.wrap((0, ts_types_1.isString)(err) ? err : 'unknown');
if (error.name === 'SandboxAuthIncompleteError') {
this.logger.debug('Sandbox auth returned INVALID_STATUS, will retry', error.message);
waitingOnAuth = true;
}
else {
throw error;
}
}
if (sandboxInfo) {
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_AUTH, sandboxInfo);
try {
this.logger.debug(sandboxInfo, 'sandbox signup complete with');
await this.writeSandboxAuthFile(sandboxProcessObj, sandboxInfo);
return { completed: true, payload: sandboxProcessObj };
}
catch (err) {
const error = err;
this.logger.debug('Exception while calling writeSandboxAuthFile', err);
if (error?.name === 'JwtAuthError' && error?.stack?.includes("user hasn't approved")) {
waitingOnAuth = true;
}
else {
throw sfError_1.SfError.wrap(error);
}
}
}
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_STATUS, {
sandboxProcessObj,
remainingWait: remainingWait.seconds,
interval: options.pollInterval.seconds,
waitingOnAuth,
});
remainingWait = kit_1.Duration.seconds(remainingWait.seconds - options.pollInterval.seconds);
return { completed: false, payload: sandboxProcessObj };
},
frequency: options.pollInterval,
timeout: options.wait,
});
return pollingClient.subscribe();
}
/**
* query SandboxProcess using supplied where clause
*
* @param where clause to query for
* @private
*/
async querySandboxProcess(where) {
const soql = `SELECT ${sandboxProcessFields.join(',')} FROM SandboxProcess WHERE ${where} ORDER BY CreatedDate DESC`;
const result = (await this.connection.tooling.query(soql)).records.filter((item) => !item.Status.startsWith('Del'));
if (result.length === 0) {
throw new sfError_1.SfError(`No record found for ${soql}`, connection_1.SingleRecordQueryErrors.NoRecords);
}
if (result.length > 1) {
const err = new sfError_1.SfError('The query returned more than 1 record', connection_1.SingleRecordQueryErrors.MultipleRecords);
err.data = result;
throw err;
}
return result[0];
}
/**
* determines if the sandbox has successfully been created
*
* @param sandboxProcessObj sandbox signup progress
* @private
*/
async sandboxSignupComplete(sandboxProcessObj) {
this.logger.debug('sandboxSignupComplete called with SandboxProcessObject', sandboxProcessObj);
if (!sandboxProcessObj.EndDate) {
return;
}
try {
// call server side /sandboxAuth API to auth the sandbox org user with the connected app
const authFields = this.connection.getAuthInfoFields();
const callbackUrl = `http://localhost:${await webOAuthServer_1.WebOAuthServer.determineOauthPort()}/OauthRedirect`;
const sandboxReq = {
// the sandbox signup has been completed on production, we have production clientId by this point
clientId: authFields.clientId,
sandboxName: sandboxProcessObj.SandboxName,
callbackUrl,
};
this.logger.debug(sandboxReq, 'Calling sandboxAuth with SandboxUserAuthRequest');
// eslint-disable-next-line no-underscore-dangle
const url = `${this.connection.tooling._baseUrl()}/sandboxAuth`;
const params = {
method: 'POST',
url,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sandboxReq),
};
const result = await this.connection.tooling.request(params);
this.logger.debug(result, 'Result of calling sandboxAuth');
return result;
}
catch (err) {
const error = err instanceof Error ? err : sfError_1.SfError.wrap((0, ts_types_1.isString)(err) ? err : 'unknown');
// There are cases where the endDate is set before the sandbox has actually completed.
// In that case, the sandboxAuth call will throw a specific exception.
if (error?.name === 'INVALID_STATUS') {
this.logger.debug('Error while authenticating the user:', error.message);
const sfError = messages.createError('sandboxAuthIncomplete', [error.message]);
sfError.cause = error;
throw sfError;
}
throw error;
}
}
validateWaitOptions(options) {
const wait = options.wait ?? kit_1.Duration.minutes(30);
const interval = options.interval ?? kit_1.Duration.seconds(30);
let pollInterval = options.async ? kit_1.Duration.seconds(0) : interval;
// pollInterval cannot be > wait.
pollInterval = pollInterval.seconds > wait.seconds ? wait : pollInterval;
return [wait, pollInterval];
}
/**
* removes source tracking files hosted in the project/.sf/orgs/<org id>/
*
* @private
*/
async removeSourceTrackingFiles() {
try {
const rootFolder = await config_1.Config.resolveRootFolder(false);
await fs_1.fs.promises.rm(path.join(rootFolder, global_1.Global.SF_STATE_FOLDER, 'orgs', this.getOrgId()), {
recursive: true,
force: true,
});
}
catch (e) {
const err = sfError_1.SfError.wrap(e);
// consume the error in case something went wrong
this.logger.debug(`error deleting source tracking information for ${this.getOrgId()} error: ${err.message}`);
}
}
}
exports.Org = Org;
(function (Org) {
/**
* Scratch Org status.
*/
let Status;
(function (Status) {
/**
* The scratch org is active.
*/
Status["ACTIVE"] = "ACTIVE";
/**
* The scratch org has expired.
*/
Status["EXPIRED"] = "EXPIRED";
/**
* The org is a scratch Org but no dev hub is indicated.
*/
Status["UNKNOWN"] = "UNKNOWN";
/**
* The dev hub configuration is reporting an active Scratch org but the AuthInfo cannot be found.
*/
Status["MISSING"] = "MISSING";
})(Status = Org.Status || (Org.Status = {}));
/**
* Org Fields.
*/
// A subset of fields from AuthInfoFields and properties that are specific to Org,
// and properties that are defined on Org itself.
let Fields;
(function (Fields) {
/**
* The org alias.
*/
// From AuthInfo
Fields["ALIAS"] = "alias";
Fields["CREATED"] = "created";
// From Organization
Fields["NAME"] = "name";
Fields["NAMESPACE_PREFIX"] = "namespacePrefix";
Fields["INSTANCE_NAME"] = "instanceName";
Fields["TRIAL_EXPIRATION_DATE"] = "trailExpirationDate";
/**
* The org edition. e.g. `Developer Edition`, `Enterprise Edition`.
*/
Fields["ORG_EDITION"] = "orgEdition";
/**
* The Salesforce instance the org was created on. e.g. `cs42`.
*/
Fields["CREATED_ORG_INSTANCE"] = "createdOrgInstance";
/**
* The username of the dev hub org that created this org. Only populated for scratch orgs.
*/
Fields["DEV_HUB_USERNAME"] = "devHubUsername";
/**
* The full url of the instance the org lives on.
*/
Fields["INSTANCE_URL"] = "instanceUrl";
/**
* Is the current org a dev hub org. e.g. They have access to the `ScratchOrgInfo` object.
*/
Fields["IS_DEV_HUB"] = "isDevHub";
/**
* Is the current org a scratch org. e.g. Organization has IsSandbox == true and TrialExpirationDate != null.
*/
Fields["IS_SCRATCH"] = "isScratch";
/**
* Is the current org a sandbox (not a scratch org on a non-prod instance), but an actual Sandbox org). e.g. Organization has IsSandbox == true and TrialExpirationDate == null.
*/
Fields["IS_SANDBOX"] = "isSandbox";
/**
* The login url of the org. e.g. `https://login.salesforce.com` or `https://test.salesforce.com`.
*/
Fields["LOGIN_URL"] = "loginUrl";
/**
* The org ID.
*/
Fields["ORG_ID"] = "orgId";
/**
* The `OrgStatus` of the org.
*/
Fields["STATUS"] = "status";
/**
* The snapshot used to create the scratch org.
*/
Fields["SNAPSHOT"] = "snapshot";
/**
* true: the org supports and wants source tracking
* false: the org opted out of tracking or can't support it
*/
Fields["TRACKS_SOURCE"] = "tracksSource";
// Should it be on org? Leave it off for now, as it might
// be confusing to the consumer what this actually is.
// USERNAMES = 'usernames',
// Keep separation of concerns. I think these should be on a "user" that belongs to the org.
// Org can have a list of user objects that belong to it? Should connection be on user and org.getConnection()
// gets the orgs current user for the process? Maybe we just want to keep with the Org only model for
// the end of time?
// USER_ID = 'userId',
// USERNAME = 'username',
// PASSWORD = 'password',
// USER_PROFILE_NAME = 'userProfileName'
})(Fields = Org.Fields || (Org.Fields = {}));
})(Org || (exports.Org = Org = {}));
//# sourceMappingURL=org.js.map