UNPKG

@salesforce/core

Version:

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

992 lines 43.5 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Org = exports.SandboxEvents = exports.OrgTypes = void 0; const path_1 = require("path"); const kit_1 = require("@salesforce/kit"); const ts_types_1 = require("@salesforce/ts-types"); const scratchOrgCreate_1 = require("./scratchOrgCreate"); const authInfo_1 = require("./authInfo"); const aliases_1 = require("./config/aliases"); const authInfoConfig_1 = require("./config/authInfoConfig"); const config_1 = require("./config/config"); const configAggregator_1 = require("./config/configAggregator"); const orgUsersConfig_1 = require("./config/orgUsersConfig"); const sandboxOrgConfig_1 = require("./config/sandboxOrgConfig"); const connection_1 = require("./connection"); const global_1 = require("./global"); const lifecycleEvents_1 = require("./lifecycleEvents"); const logger_1 = require("./logger"); const sfdxError_1 = require("./sfdxError"); const fs_1 = require("./util/fs"); const sfdc_1 = require("./util/sfdc"); const webOAuthServer_1 = require("./webOAuthServer"); var OrgTypes; (function (OrgTypes) { OrgTypes["Scratch"] = "scratch"; OrgTypes["Sandbox"] = "sandbox"; })(OrgTypes = exports.OrgTypes || (exports.OrgTypes = {})); var SandboxEvents; (function (SandboxEvents) { SandboxEvents["EVENT_STATUS"] = "status"; SandboxEvents["EVENT_ASYNC_RESULT"] = "asyncResult"; SandboxEvents["EVENT_RESULT"] = "result"; SandboxEvents["EVENT_AUTH"] = "auth"; })(SandboxEvents = exports.SandboxEvents || (exports.SandboxEvents = {})); /** * 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 defaultusername 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.AsyncCreatable { /** * @ignore */ constructor(options) { super(options); this.status = Org.Status.UNKNOWN; this.options = options; } /** * create a sandbox from a production org * 'this' needs to be a production org with sandbox licenses available * * @param sandboxReq SandboxRequest options to create the sandbox with * @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling */ async createSandbox(sandboxReq, options) { var _a; this.logger.debug('CreateSandbox called with SandboxRequest: %s ', sandboxReq); const createResult = await this.connection.tooling.create('SandboxInfo', sandboxReq); this.logger.debug('Return from calling tooling.create: %s ', createResult); if (Array.isArray(createResult) || !createResult.success) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'SandboxInfoCreateFailed', [JSON.stringify(createResult)]); } const sandboxCreationProgress = await this.querySandboxProcess(createResult.id); this.logger.debug('Return from calling singleRecordQuery with tooling: %s', sandboxCreationProgress); const retries = options.wait ? options.wait.seconds / kit_1.Duration.seconds(30).seconds : 0; this.logger.debug('pollStatusAndAuth sandboxProcessObj %s, maxPollingRetries %i', sandboxCreationProgress, retries); const pollInterval = (_a = options.interval) !== null && _a !== void 0 ? _a : kit_1.Duration.seconds(30); return this.pollStatusAndAuth({ sandboxProcessObj: sandboxCreationProgress, retries, shouldPoll: retries > 0, pollInterval, }); } /** * Creates a scratchOrg * 'this' needs to be a valid dev-hub * * @param {options} ScratchOrgCreateOptions * @returns {ScratchOrgCreateResult} */ async scratchOrgCreate(options) { return 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 { const rootFolder = await config_1.Config.resolveRootFolder(false); dataPath = path_1.join(rootFolder, global_1.Global.STATE_FOLDER, orgDataPath ? orgDataPath : 'orgs'); this.logger.debug(`cleaning data for path: ${dataPath}`); } catch (err) { if (err instanceof Error && err.name === 'InvalidProjectWorkspace') { // 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 () => await fs_1.fs.remove(dataPath), dataPath, throwWhenRemoveFails); } /** * @ignore */ async retrieveOrgUsersConfig() { return await orgUsersConfig_1.OrgUsersConfig.create(orgUsersConfig_1.OrgUsersConfig.getOptions(this.getOrgId())); } /** * Removes the scratch org config file at $HOME/.sfdx/[name].json, any project level org * files, all user auth files for the org, matching default config settings, and any * matching aliases. * * @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(throwWhenRemoveFails); 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(); } /** * Check if org is a sandbox org by checking its SandboxOrgConfig. * */ async isSandbox() { return !!(await this.getSandboxOrgConfigField(sandboxOrgConfig_1.SandboxOrgConfig.Fields.PROD_ORG_USERNAME)); } /** * Check that this org is a scratch org by asking the dev hub if it knows about it. * * **Throws** *{@link SfdxError}{ name: 'NotADevHub' }* Not a Dev Hub. * * **Throws** *{@link SfdxError}{ name: 'NoResults' }* No results. * * @param devHubUsernameOrAlias The username or alias of the dev hub org. */ async checkScratchOrg(devHubUsernameOrAlias) { let aliasOrUsername = devHubUsernameOrAlias; if (!aliasOrUsername) { aliasOrUsername = ts_types_1.asString(this.configAggregator.getPropertyValue(config_1.Config.DEFAULT_DEV_HUB_USERNAME)); } const devHubConnection = (await Org.create({ aliasOrUsername })).getConnection(); const thisOrgAuthConfig = this.getConnection().getAuthInfoFields(); const trimmedId = sfdc_1.sfdc.trimTo15(thisOrgAuthConfig.orgId); const DEV_HUB_SOQL = `SELECT CreatedDate,Edition,ExpirationDate FROM ActiveScratchOrg WHERE ScratchOrg='${trimmedId}'`; let results; try { results = await devHubConnection.query(DEV_HUB_SOQL); } catch (err) { if (err instanceof Error && err.name === 'INVALID_TYPE') { throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'NotADevHub', [devHubConnection.getUsername()]); } throw err; } if (ts_types_1.getNumber(results, 'records.length') !== 1) { throw new sfdxError_1.SfdxError('No results', 'NoResults'); } 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 = 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 (ts_types_1.isBoolean(isDevHub)) { return isDevHub; } else { return false; } } /** * Will delete 'this' instance remotely and any files locally * * @param controllingOrg username or Org that 'this.devhub' or 'this.production' refers to. AKA a DevHub for a scratch org, or a Production Org for a sandbox */ async deleteFrom(controllingOrg) { if (typeof controllingOrg === 'string') { controllingOrg = await Org.create({ aggregator: this.configAggregator, aliasOrUsername: controllingOrg, }); } if (await this.isSandbox()) { await this.deleteSandbox(controllingOrg); } else { await this.deleteScratchOrg(controllingOrg); } } /** * Will delete 'this' instance remotely and any files locally */ async delete() { 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 && 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 = ts_types_1.ensure(this.getUsername()); const auth = await authInfo_1.AuthInfo.create({ username }); await auth.save({ isDevHub }); authInfo_1.AuthInfo.clearCache(username); // Reset the connection with the updated auth file this.connection = await connection_1.Connection.create({ authInfo: await authInfo_1.AuthInfo.create({ username }), }); return isDevHub; } /** * Refreshes the auth for this org's instance by calling HTTP GET on the baseUrl of the connection object. */ async refreshAuth() { const conn = this.getConnection(); await conn.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 = ts_types_1.ensure(this.getUsername()); const usernames = ts_types_1.ensureJsonArray(contents.usernames || [thisUsername]); return Promise.all(usernames.map((username) => { if (username === thisUsername) { return authInfo_1.AuthInfo.create({ username: this.getConnection().getUsername(), }); } else { return authInfo_1.AuthInfo.create({ username: 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 sfdxError_1.SfdxError('Missing auth info', 'MissingAuthInfo'); } const authInfo = ts_types_1.isString(auth) ? await authInfo_1.AuthInfo.create({ username: auth }) : auth; this.logger.debug(`adding username ${authInfo.getFields().username}`); const orgConfig = await this.retrieveOrgUsersConfig(); const contents = await orgConfig.read(); // TODO: This is kind of screwy because contents values can be `AnyJson | object`... // needs config refactoring to improve const usernames = contents.usernames || []; if (!ts_types_1.isArray(usernames)) { throw new sfdxError_1.SfdxError('Usernames is not an array', 'UnexpectedDataFormat'); } let shouldUpdate = false; const thisUsername = 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 SfdxError}{ name: 'MissingAuthInfo' }* Auth info is missing. * * @param {AuthInfo | string} auth The AuthInfo containing the username to remove. */ async removeUsername(auth) { if (!auth) { throw new sfdxError_1.SfdxError('Missing auth info', 'MissingAuthInfo'); } const authInfo = ts_types_1.isString(auth) ? await authInfo_1.AuthInfo.create({ username: auth }) : auth; this.logger.debug(`removing username ${authInfo.getFields().username}`); const orgConfig = await this.retrieveOrgUsersConfig(); const contents = await orgConfig.read(); const targetUser = authInfo.getFields().username; const usernames = (contents.usernames || []); contents.usernames = usernames.filter((username) => username !== targetUser); await orgConfig.write(); return this; } /** * Sets the key/value pair in the sandbox config for this org. For convenience `this` object is returned. * * * @param {key} The key for this value * @param {value} The value to save */ async setSandboxOrgConfigField(field, value) { const sandboxOrgConfig = await this.retrieveSandboxOrgConfig(); sandboxOrgConfig.set(field, value); await sandboxOrgConfig.write(); return this; } /** * Returns an org config field. Returns undefined if the field is not set or invalid. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async getSandboxOrgConfigField(field) { const sandboxOrgConfig = await this.retrieveSandboxOrgConfig(); return sandboxOrgConfig.get(field); } /** * 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 await 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.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) { // @ts-ignore TODO: Need to refactor storage of these values on both Org and AuthFields return this[key] || this.getConnection().getAuthInfoFields()[key]; } /** * Returns a map of requested fields. */ getFields(keys) { const json = {}; return keys.reduce((map, key) => { map[key] = this.getField(key); return map; }, json); } /** * Returns the JSForce connection for the org. */ getConnection() { return this.connection; } /** * Initialize async components. */ async init() { this.logger = await logger_1.Logger.child('Org'); this.configAggregator = this.options.aggregator ? this.options.aggregator : await configAggregator_1.ConfigAggregator.create(); if (!this.options.connection) { if (this.options.aliasOrUsername == null) { this.configAggregator = this.getConfigAggregator(); const aliasOrUsername = this.options.isDevHub ? ts_types_1.getString(this.configAggregator.getInfo(config_1.Config.DEFAULT_DEV_HUB_USERNAME), 'value') : ts_types_1.getString(this.configAggregator.getInfo(config_1.Config.DEFAULT_USERNAME), 'value'); this.options.aliasOrUsername = aliasOrUsername || undefined; } const username = this.options.aliasOrUsername; this.connection = await connection_1.Connection.create({ // If no username is provided or resolvable from an alias, AuthInfo will throw an SfdxError. authInfo: await authInfo_1.AuthInfo.create({ username: (username != null && (await aliases_1.Aliases.fetch(username))) || username, isDevHub: this.options.isDevHub, }), }); } else { this.connection = this.options.connection; } } /** * **Throws** *{@link SfdxError} Throws and unsupported error. */ getDefaultOptions() { throw new sfdxError_1.SfdxError('Not Supported'); } /** * 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) { var _a; const { tooling } = this.getConnection(); this.logger.debug('QueryLatestSandboxProcessBySandboxName called with SandboxName: %s ', sandboxNameIn); const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE SandboxName='${sandboxNameIn}' AND Status != 'D' ORDER BY CreatedDate DESC LIMIT 1`; const queryResult = await tooling.query(queryStr); this.logger.debug('Return from calling queryToolingApi: %s ', queryResult); if (((_a = queryResult === null || queryResult === void 0 ? void 0 : queryResult.records) === null || _a === void 0 ? void 0 : _a.length) === 1) { return queryResult.records[0]; } else if (queryResult.records && queryResult.records.length > 1) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'MultiSandboxProcessNotFoundBySandboxName', [sandboxNameIn]); } else { throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'SandboxProcessNotFoundBySandboxName', [sandboxNameIn]); } } /** * 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) { var _a; const retries = options.wait ? options.wait.seconds / kit_1.Duration.seconds(30).seconds : 0; const pollInterval = (_a = options.interval) !== null && _a !== void 0 ? _a : kit_1.Duration.seconds(30); this.logger.debug('AuthWithRetries sandboxProcessObj %s, retries %i', sandboxProcessObj, retries); return this.pollStatusAndAuth({ sandboxProcessObj, retries, shouldPoll: retries > 0, pollInterval, }); } async queryProduction(org, field, value) { return org.connection.singleRecordQuery(`SELECT SandboxInfoId FROM SandboxProcess WHERE ${field} ='${value}' AND Status NOT IN ('D', 'E')`, { tooling: true }); } /** * 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) { prodOrg !== null && prodOrg !== void 0 ? prodOrg : (prodOrg = await Org.create({ aggregator: this.configAggregator, aliasOrUsername: await this.getSandboxOrgConfigField(sandboxOrgConfig_1.SandboxOrgConfig.Fields.PROD_ORG_USERNAME), })); let result; // attempt to locate sandbox id by username try { // try to calculate sandbox name from the production org // production org: admin@integrationtesthub.org // this.getUsername: admin@integrationtesthub.org.dev1 // sandboxName in Production: dev1 const sandboxName = (this.getUsername() || '').split(`${prodOrg.getUsername()}.`)[1]; if (!sandboxName) { this.logger.debug('Could not construct a sandbox name'); throw new Error(); } this.logger.debug(`attempting to locate sandbox with username ${sandboxName}`); result = await this.queryProduction(prodOrg, 'SandboxName', sandboxName); if (!result) { this.logger.debug(`Failed to find sandbox with username: ${sandboxName}`); throw new Error(); } } catch { // if an error is thrown, don't panic yet. we'll try querying by orgId const trimmedId = sfdc_1.sfdc.trimTo15(this.getOrgId()); this.logger.debug(`defaulting to trimming id from ${this.getOrgId()} to ${trimmedId}`); try { result = await this.queryProduction(prodOrg, 'SandboxOrganization', trimmedId); } catch { throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'SandboxNotFound', [trimmedId]); } } const deleteResult = await prodOrg.connection.tooling.delete('SandboxInfo', result.SandboxInfoId); this.logger.debug('Return from calling tooling.delete: %o ', deleteResult); await this.remove(); if (Array.isArray(deleteResult) || !deleteResult.success) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'SandboxDeleteFailed', [JSON.stringify(deleteResult)]); } } /** * If this Org is a scratch org, calling this method will delete the scratch org from the DevHub and clean up any local files * * @param devHub - optional DevHub Org of the to-be-deleted scratch org * @private */ async deleteScratchOrg(devHub) { // if we didn't get a devHub, we'll get it from the this org devHub !== null && devHub !== void 0 ? devHub : (devHub = await this.getDevHubOrg()); if (!devHub) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'NoDevHubFound'); } if (devHub.getOrgId() === this.getOrgId()) { // we're attempting to delete a DevHub throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'DeleteOrgHubError'); } try { const devHubConn = devHub.getConnection(); const username = this.getUsername(); const activeScratchOrgRecordId = (await devHubConn.singleRecordQuery(`SELECT Id FROM ActiveScratchOrg WHERE SignupUsername='${username}'`)).Id; this.logger.trace(`found matching ActiveScratchOrg with SignupUsername: ${username}. Deleting...`); await devHubConn.delete('ActiveScratchOrg', 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 sfdxError_1.SfdxError.create('@salesforce/core', 'org', '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 sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'ScratchOrgNotFound'); } throw err; } } /** * Returns a promise to 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 username = ts_types_1.ensure(this.getUsername()); this.logger.debug(`Removing auth for user: ${username}`); const config = await authInfoConfig_1.AuthInfoConfig.create({ ...authInfoConfig_1.AuthInfoConfig.getOptions(username), throwOnNotFound: false, }); this.logger.debug(`Clearing auth cache for user: ${username}`); authInfo_1.AuthInfo.clearCache(username); if (await config.exists()) { await config.unlink(); } } /** * 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(); } } /** * @ignore */ async retrieveSandboxOrgConfig() { return await sandboxOrgConfig_1.SandboxOrgConfig.create(sandboxOrgConfig_1.SandboxOrgConfig.getOptions(this.getOrgId())); } 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) { this.logger.debug(`Removing users associate with org: ${this.getOrgId()}`); const [config, authInfos, aliases] = await Promise.all([ this.retrieveOrgUsersConfig(), this.readUserAuthFiles(), aliases_1.Aliases.create(aliases_1.Aliases.getDefaultOptions()), ]); this.logger.debug(`using path for org users: ${config.getPath()}`); this.logger.info(`Cleaning up usernames in org: ${this.getOrgId()}`); await Promise.all(authInfos .map((auth) => auth.getFields().username) .map(async (username) => { const aliasKeys = (username && aliases.getKeysByValue(username)) || []; aliases.unsetAll(aliasKeys); 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() ? config_1.Config.DEFAULT_DEV_HUB_USERNAME : config_1.Config.DEFAULT_USERNAME; const configInfo = orgForUser.configAggregator.getInfo(orgType); const needsConfigUpdate = (configInfo.isGlobal() || configInfo.isLocal()) && (configInfo.value === username || aliasKeys.includes(configInfo.value)); return [ orgForUser.removeAuth(), needsConfigUpdate ? config_1.Config.update(configInfo.isGlobal(), orgType, undefined) : undefined, ].filter(Boolean); })); await aliases.write(); } /** * Remove an associate sandbox config. * * @param throwWhenRemoveFails true if manageDelete should throw or not if the deleted fails. */ async removeSandboxConfig(throwWhenRemoveFails) { const sandboxOrgConfig = await this.retrieveSandboxOrgConfig(); if (await sandboxOrgConfig.exists()) { await this.manageDelete(async () => await sandboxOrgConfig.unlink(), sandboxOrgConfig.getPath(), throwWhenRemoveFails); } } async writeSandboxAuthFile(sandboxProcessObj, sandboxRes) { this.logger.debug('writeSandboxAuthFile sandboxProcessObj: %s, sandboxRes: %s', sandboxProcessObj, sandboxRes); if (sandboxRes.authUserName) { const productionAuthFields = this.connection.getAuthInfoFields(); this.logger.debug('Result from getAuthInfoFields: AuthFields %s', productionAuthFields); // 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; } const authInfo = await authInfo_1.AuthInfo.create({ username: sandboxRes.authUserName, oauth2Options, parentUsername: productionAuthFields.username, }); await authInfo.save(); const sandboxOrg = await Org.create({ aliasOrUsername: authInfo.getUsername() }); await sandboxOrg.setSandboxOrgConfigField(sandboxOrgConfig_1.SandboxOrgConfig.Fields.PROD_ORG_USERNAME, // we couldn't get this far into the process without a production org so username will be there productionAuthFields.username); await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_RESULT, { sandboxProcessObj, sandboxRes, }); } else { // no authed sandbox user, error throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'MissingAuthUsername', [sandboxProcessObj.SandboxName]); } } /** * Polls for the new sandbox to be created - and will write the associated auth files * * @private * @param options * sandboxProcessObj: The in-progress sandbox signup request * retries: the number of retries to poll for every 30s * shouldPoll: wait for polling, or just return * pollInterval: Duration to sleep between poll events, default 30 seconds */ async pollStatusAndAuth(options) { const { sandboxProcessObj, retries, shouldPoll, pollInterval } = options; this.logger.debug('PollStatusAndAuth called with SandboxProcessObject%s, retries %s', sandboxProcessObj, retries); const lifecycle = lifecycleEvents_1.Lifecycle.getInstance(); let pollFinished = false; let waitingOnAuth = false; const sandboxInfo = await this.sandboxSignupComplete(sandboxProcessObj); if (sandboxInfo) { await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_AUTH, sandboxInfo); try { this.logger.debug('sandbox signup complete with %s', sandboxInfo); await this.writeSandboxAuthFile(sandboxProcessObj, sandboxInfo); pollFinished = true; } catch (err) { this.logger.debug('Exception while calling writeSandboxAuthFile %s', err); if ((err === null || err === void 0 ? void 0 : err.name) === 'JWTAuthError' && (err === null || err === void 0 ? void 0 : err.stack.includes("user hasn't approved"))) { waitingOnAuth = true; } else { throw sfdxError_1.SfdxError.wrap(err); } } } if (!pollFinished) { if (retries > 0) { // emit the signup progress of the sandbox and query the production org again after waiting the interval await Promise.all([ await lifecycle.emit(SandboxEvents.EVENT_STATUS, { sandboxProcessObj, interval: pollInterval.seconds, retries, waitingOnAuth, }), await kit_1.sleep(pollInterval), ]); const polledSandboxProcessObj = await this.querySandboxProcess(sandboxProcessObj.SandboxInfoId); return this.pollStatusAndAuth({ sandboxProcessObj: polledSandboxProcessObj, retries: retries - 1, shouldPoll, pollInterval, }); } else { if (shouldPoll) { // timed out on retries throw sfdxError_1.SfdxError.create('@salesforce/core', 'org', 'OrgPollingTimeout', [sandboxProcessObj.Status]); } else { // The user didn't want us to poll, so simply return the status // simply report status and exit await lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxProcessObj); } } } return sandboxProcessObj; } /** * query SandboxProcess via SandboxInfoId * * @param id SandboxInfoId to query for * @private */ async querySandboxProcess(id) { const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE SandboxInfoId='${id}' AND Status != 'D'`; return await this.connection.singleRecordQuery(queryStr, { tooling: true, }); } /** * determines if the sandbox has successfully been created * * @param sandboxProcessObj sandbox signup progeress * @private */ async sandboxSignupComplete(sandboxProcessObj) { this.logger.debug('sandboxSignupComplete called with SandboxProcessObject %s', 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('Calling sandboxAuth with SandboxUserAuthRequest %s', sandboxReq); 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 of calling sandboxAuth %s', result); return result; } catch (err) { // 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 ((err === null || err === void 0 ? void 0 : err.name) === 'INVALID_STATUS') { this.logger.debug('Error while authenticating the user %s', err === null || err === void 0 ? void 0 : err.toString()); } else { // If it fails for any unexpected reason, just pass that through throw sfdxError_1.SfdxError.wrap(err); } } } } 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"; /** * 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"; /** * 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"; // 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 || (exports.Org = {})); //# sourceMappingURL=org.js.map