UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

1,634 lines 75.8 kB
"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