UNPKG

@salesforce/core

Version:

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

1,082 lines 52.4 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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthInfo = exports.CODE_BUILDER_CONNECTED_APP_INFO = exports.DEFAULT_CONNECTED_APP_INFO = void 0;
const node_crypto_1 = require("node:crypto");
const node_path_1 = require("node:path");
const os = __importStar(require("node:os"));
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const jsforce_node_1 = require("@jsforce/jsforce-node");
const transport_1 = __importDefault(require("@jsforce/jsforce-node/lib/transport"));
const jwt = __importStar(require("jsonwebtoken"));
const fs_1 = require("../fs/fs");
const config_1 = require("../config/config");
const configAggregator_1 = require("../config/configAggregator");
const logger_1 = require("../logger/logger");
const sfError_1 = require("../sfError");
const sfdc_1 = require("../util/sfdc");
const stateAggregator_1 = require("../stateAggregator/stateAggregator");
const filters_1 = require("../logger/filters");
const messages_1 = require("../messages");
const sfdcUrl_1 = require("../util/sfdcUrl");
const findSuggestion_1 = require("../util/findSuggestion");
const connection_1 = require("./connection");
const determineOrg_1 = require("./determineOrg");
const org_1 = require("./org");
const orgConfigProperties_1 = require("./orgConfigProperties");
;
const messages = new messages_1.Messages('@salesforce/core', 'core', new Map([["authInfoCreationError", "Must pass a username and/or OAuth options when creating an AuthInfo instance."], ["authInfoOverwriteError", "Cannot create an AuthInfo instance that will overwrite existing auth data."], ["authInfoOverwriteError.actions", ["Create the AuthInfo instance using existing auth data by just passing the username. E.g., `AuthInfo.create({ username: 'my@user.org' });`."]], ["authCodeExchangeError", "Error authenticating with auth code due to: %s"], ["authCodeUsernameRetrievalError", "Could not retrieve the username after successful auth code exchange.\n\nDue to: %s"], ["jwtAuthError", "Error authenticating with JWT config due to: %s"], ["jwtAuthErrors", "Error authenticating with JWT.\nErrors encountered:\n%s"], ["refreshTokenAuthError", "Error authenticating with the refresh token due to: %s"], ["invalidSfdxAuthUrlError", "Invalid SFDX authorization URL. Must be in the format \"force://<clientId>:<clientSecret>:<refreshToken>@<instanceUrl>\". Note that the \"instanceUrl\" inside the SFDX authorization URL doesn\\'t include the protocol (\"https://\"). Run \"org display --target-org\" on an org to see an example of an SFDX authorization URL."], ["orgDataNotAvailableError", "An attempt to refresh the authentication token failed with a 'Data Not Found Error'. The org identified by username %s does not appear to exist. Likely cause is that the org was deleted by another user or has expired."], ["orgDataNotAvailableError.actions", ["Run `sfdx force:org:list --clean` to remove stale org authentications.", "Use `sfdx force:config:set` to update the defaultusername.", "Use `sfdx force:org:create` to create a new org.", "Use `sfdx auth` to authenticate an existing org."]], ["namedOrgNotFound", "No authorization information found for %s."], ["noAliasesFound", "Nothing to set."], ["invalidFormat", "Setting aliases must be in the format <key>=<value> but found: [%s]."], ["invalidJsonCasing", "All JSON input must have heads down camelcase keys. E.g., `{ sfdcLoginUrl: \"https://login.salesforce.com\" }`\nFound \"%s\" at %s"], ["missingClientId", "Client ID is required for JWT authentication."]]));
// parses the id field returned from jsForce oauth2 methods to get
// user ID and org ID.
function parseIdUrl(idUrl) {
    const idUrls = idUrl.split('/');
    const userId = idUrls.pop();
    const orgId = idUrls.pop();
    return {
        userId,
        orgId,
        url: idUrl,
    };
}
exports.DEFAULT_CONNECTED_APP_INFO = {
    clientId: 'PlatformCLI',
    clientSecret: '',
};
exports.CODE_BUILDER_CONNECTED_APP_INFO = {
    clientId: 'CodeBuilder',
    clientSecret: '',
};
/**
 * Handles persistence and fetching of user authentication information using
 * JWT, OAuth, or refresh tokens. Sets up the refresh flows that jsForce will
 * use to keep tokens active. An AuthInfo can also be created with an access
 * token, but AuthInfos created with access tokens can't be persisted to disk.
 *
 * **See** [Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm)
 *
 * **See** [Salesforce DX Usernames and Orgs](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_usernames_orgs.htm)
 *
 * ```
 * // Creating a new authentication file.
 * const authInfo = await AuthInfo.create({
 *   username: myAdminUsername,
 *   oauth2Options: {
 *     loginUrl, authCode, clientId, clientSecret
 *   }
 * );
 * authInfo.save();
 *
 * // Creating an authorization info with an access token.
 * const authInfo = await AuthInfo.create({
 *   username: accessToken
 * });
 *
 * // Using an existing authentication file.
 * const authInfo = await AuthInfo.create({
 *   username: myAdminUsername
 * });
 *
 * // Using the AuthInfo
 * const connection = await Connection.create({ authInfo });
 * ```
 */
class AuthInfo extends kit_1.AsyncOptionalCreatable {
    // Possibly overridden in create
    usingAccessToken = false;
    // Initialized in init
    logger;
    stateAggregator;
    username;
    options;
    /**
     * Constructor
     * **Do not directly construct instances of this class -- use {@link AuthInfo.create} instead.**
     *
     * @param options The options for the class instance
     */
    constructor(options) {
        super(options);
        this.options = options ?? {};
    }
    /**
     * Returns the default instance url
     *
     * @returns {string}
     */
    static getDefaultInstanceUrl() {
        const configuredInstanceUrl = configAggregator_1.ConfigAggregator.getValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL)?.value;
        return configuredInstanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
    }
    /**
     * Get a list of all authorizations based on auth files stored in the global directory.
     * One can supply a filter (see @param orgAuthFilter) and calling this function without
     * a filter will return all authorizations.
     *
     * @param orgAuthFilter A predicate function that returns true for those org authorizations that are to be retained.
     *
     * @returns {Promise<OrgAuthorization[]>}
     */
    static async listAllAuthorizations(orgAuthFilter = (orgAuth) => !!orgAuth) {
        const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
        const config = (await configAggregator_1.ConfigAggregator.create()).getConfigInfo();
        const orgs = await stateAggregator.orgs.readAll();
        const final = [];
        for (const org of orgs) {
            const username = (0, ts_types_1.ensureString)(org.username);
            const aliases = stateAggregator.aliases.getAll(username) ?? undefined;
            // Get a list of configuration values that are set to either the username or one
            // of the aliases
            const configs = config
                .filter((c) => aliases.includes(c.value) || c.value === username)
                .map((c) => c.key);
            try {
                // prevent ConfigFile collision bug
                // eslint-disable-next-line no-await-in-loop
                const authInfo = await AuthInfo.create({ username });
                const { orgId, instanceUrl, devHubUsername, expirationDate, isDevHub } = authInfo.getFields();
                final.push({
                    aliases,
                    configs,
                    username,
                    instanceUrl,
                    isScratchOrg: Boolean(devHubUsername),
                    isDevHub: isDevHub ?? false,
                    // eslint-disable-next-line no-await-in-loop
                    isSandbox: await stateAggregator.sandboxes.hasFile(orgId),
                    orgId: orgId,
                    accessToken: authInfo.getConnectionOptions().accessToken,
                    oauthMethod: authInfo.isJwt() ? 'jwt' : authInfo.isOauth() ? 'web' : 'token',
                    isExpired: Boolean(devHubUsername) && expirationDate
                        ? new Date((0, ts_types_1.ensureString)(expirationDate)).getTime() < new Date().getTime()
                        : 'unknown',
                });
            }
            catch (err) {
                final.push({
                    aliases,
                    configs,
                    username,
                    orgId: org.orgId,
                    instanceUrl: org.instanceUrl,
                    accessToken: undefined,
                    oauthMethod: 'unknown',
                    error: err.message,
                    isExpired: 'unknown',
                });
            }
        }
        return final.filter(orgAuthFilter);
    }
    /**
     * Returns true if one or more authentications are persisted.
     */
    static async hasAuthentications() {
        try {
            const auths = await (await stateAggregator_1.StateAggregator.getInstance()).orgs.list();
            return !(0, kit_1.isEmpty)(auths);
        }
        catch (err) {
            const error = err;
            if (error.name === 'OrgDataNotAvailableError' || error.code === 'ENOENT') {
                return false;
            }
            throw error;
        }
    }
    /**
     * Get the authorization URL.
     *
     * @param options The options to generate the URL.
     */
    static getAuthorizationUrl(options, oauth2) {
        // Unless explicitly turned off, use a code verifier for enhanced security
        const oauth2Verifier = oauth2 ?? new jsforce_node_1.OAuth2({ useVerifier: true, ...options });
        // The state parameter allows the redirectUri callback listener to ignore request
        // that don't contain the state value.
        const params = {
            state: options.state ?? (0, node_crypto_1.randomBytes)(Math.ceil(6)).toString('hex'),
            prompt: 'login',
            // Default connected app is 'refresh_token api web'
            scope: options.scope ?? kit_1.env.getString('SFDX_AUTH_SCOPES', 'refresh_token api web'),
        };
        return oauth2Verifier.getAuthorizationUrl(params);
    }
    /**
     * Parse a sfdx auth url, usually obtained by `authInfo.getSfdxAuthUrl`.
     *
     * @example
     * ```
     * await AuthInfo.create(AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl));
     * ```
     * @param sfdxAuthUrl
     */
    static parseSfdxAuthUrl(sfdxAuthUrl) {
        const match = sfdxAuthUrl.match(/^force:\/\/([a-zA-Z0-9._-]+={0,2}):([a-zA-Z0-9._-]*={0,2}):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9:._-]+)/);
        if (!match) {
            throw new sfError_1.SfError(messages.getMessage('invalidSfdxAuthUrlError'), 'INVALID_SFDX_AUTH_URL');
        }
        const [, clientId, clientSecret, refreshToken, loginUrl] = match;
        return {
            clientId,
            clientSecret,
            refreshToken,
            loginUrl: `https://${loginUrl}`,
        };
    }
    /**
     * Given a set of decrypted fields and an authInfo, determine if the org belongs to an available
     * dev hub, or if the org is a sandbox of another CLI authed production org.
     *
     * @param fields
     * @param orgAuthInfo
     */
    static async identifyPossibleScratchOrgs(fields, orgAuthInfo) {
        // fields property is passed in because the consumers of this method have performed the decrypt.
        // This is so we don't have to call authInfo.getFields(true) and decrypt again OR accidentally save an
        // authInfo before it is necessary.
        const logger = await logger_1.Logger.child('Common', { tag: 'identifyPossibleScratchOrgs' });
        await (0, determineOrg_1.determineOrg)(orgAuthInfo);
        // return if we already know the hub org, we know it is a devhub or prod-like, or no orgId present
        if (Boolean(fields.isDevHub) || Boolean(fields.devHubUsername) || !fields.orgId)
            return;
        logger.debug('getting devHubs and prod orgs to identify scratch orgs and sandboxes');
        // TODO: return if url is not sandbox-like to avoid constantly asking about production orgs
        // TODO: someday we make this easier by asking the org if it is a scratch org
        const hubAuthInfos = await AuthInfo.getDevHubAuthInfos();
        // Get a list of org auths that are known not to be scratch orgs or sandboxes.
        const possibleProdOrgs = await AuthInfo.listAllAuthorizations((orgAuth) => orgAuth && !orgAuth.isScratchOrg && !orgAuth.isSandbox);
        logger.debug(`found ${hubAuthInfos.length} DevHubs`);
        logger.debug(`found ${possibleProdOrgs.length} possible prod orgs`);
        if (hubAuthInfos.length === 0 && possibleProdOrgs.length === 0) {
            return;
        }
        // ask all those orgs if they know this orgId
        await Promise.all([
            ...hubAuthInfos.map(async (hubAuthInfo) => {
                try {
                    const soi = await AuthInfo.queryScratchOrg(hubAuthInfo.username, fields.orgId);
                    // if any return a result
                    logger.debug(`found orgId ${fields.orgId ?? '<undefined>'} in devhub ${hubAuthInfo.username}`);
                    try {
                        await orgAuthInfo.save({
                            ...fields,
                            devHubUsername: hubAuthInfo.username,
                            expirationDate: soi.ExpirationDate,
                            isScratch: true,
                        });
                        logger.debug(`set ${hubAuthInfo.username} as devhub and expirationDate ${soi.ExpirationDate} for scratch org ${orgAuthInfo.getUsername()}`);
                    }
                    catch (error) {
                        logger.debug(`error updating auth file for ${orgAuthInfo.getUsername()}`, error);
                    }
                }
                catch (error) {
                    if (error instanceof Error && error.name === 'NoActiveScratchOrgFound') {
                        logger.error(`devhub ${hubAuthInfo.username} has no scratch orgs`, error);
                    }
                    else {
                        logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error);
                    }
                }
            }),
            ...possibleProdOrgs.map(async (pOrgAuthInfo) => {
                await AuthInfo.identifyPossibleSandbox(pOrgAuthInfo, fields, orgAuthInfo, logger);
            }),
        ]);
    }
    /**
     * Find all dev hubs available in the local environment.
     */
    static async getDevHubAuthInfos() {
        return AuthInfo.listAllAuthorizations((possibleHub) => possibleHub?.isDevHub ?? false);
    }
    static async identifyPossibleSandbox(possibleProdOrg, fields, orgAuthInfo, logger) {
        if (!fields.orgId) {
            return;
        }
        try {
            const prodOrg = await org_1.Org.create({ aliasOrUsername: possibleProdOrg.username });
            const sbxProcess = await prodOrg.querySandboxProcessByOrgId(fields.orgId);
            if (!sbxProcess?.SandboxInfoId) {
                return;
            }
            logger.debug(`${fields.orgId} is a sandbox of ${possibleProdOrg.username}`);
            try {
                await orgAuthInfo.save({
                    ...fields,
                    isScratch: false,
                    isSandbox: true,
                });
            }
            catch (err) {
                logger.debug(`error updating auth file for: ${orgAuthInfo.getUsername()}`, err);
                throw err; // rethrow; don't want a sandbox config file with an invalid auth file
            }
            try {
                // set the sandbox config value
                const sfSandbox = {
                    sandboxUsername: fields.username,
                    sandboxOrgId: fields.orgId,
                    prodOrgUsername: possibleProdOrg.username,
                    sandboxName: sbxProcess.SandboxName,
                    sandboxProcessId: sbxProcess.Id,
                    sandboxInfoId: sbxProcess.SandboxInfoId,
                    timestamp: new Date().toISOString(),
                };
                const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
                stateAggregator.sandboxes.set(fields.orgId, sfSandbox);
                logger.debug(`writing sandbox auth file for: ${orgAuthInfo.getUsername()} with ID: ${fields.orgId}`);
                await stateAggregator.sandboxes.write(fields.orgId);
            }
            catch (e) {
                logger.debug(`error writing sandbox auth file for: ${orgAuthInfo.getUsername()}`, e);
            }
        }
        catch (err) {
            logger.debug(`${fields.orgId} is not a sandbox of ${possibleProdOrg.username}`);
        }
    }
    /**
     * Checks active scratch orgs to match by the ScratchOrg field (the 15-char org id)
     * if you pass an 18-char scratchOrgId, it will be trimmed to 15-char for query purposes
     * Throws is no matching scratch org is found
     */
    static async queryScratchOrg(devHubUsername, scratchOrgId) {
        const devHubOrg = await org_1.Org.create({ aliasOrUsername: devHubUsername });
        const trimmedId = (0, sfdc_1.trimTo15)(scratchOrgId);
        const conn = devHubOrg.getConnection();
        const data = await conn.query(`select Id, ExpirationDate, ScratchOrg from ScratchOrgInfo where ScratchOrg = '${trimmedId}' and Status = 'Active'`);
        // where ScratchOrg='00DDE00000485Lg' will return a record for both 00DDE00000485Lg and 00DDE00000485LG.
        // this is our way of enforcing case sensitivity on a 15-char Id (which is unfortunately how ScratchOrgInfo stores it)
        const result = data.records.filter((r) => r.ScratchOrg === trimmedId)[0];
        if (result)
            return result;
        throw new sfError_1.SfError(`DevHub ${devHubUsername ?? '<undefined>'} has no active scratch orgs that match ${trimmedId}`, 'NoActiveScratchOrgFound');
    }
    /**
     * Get the username.
     */
    getUsername() {
        return this.username;
    }
    /**
     * Returns true if `this` is using the JWT flow.
     */
    isJwt() {
        const { refreshToken, privateKey } = this.getFields();
        return !refreshToken && !!privateKey;
    }
    /**
     * Returns true if `this` is using an access token flow.
     */
    isAccessTokenFlow() {
        const { refreshToken, privateKey } = this.getFields();
        return !refreshToken && !privateKey;
    }
    /**
     * Returns true if `this` is using the oauth flow.
     */
    isOauth() {
        return !this.isAccessTokenFlow() && !this.isJwt();
    }
    /**
     * Returns true if `this` is using the refresh token flow.
     */
    isRefreshTokenFlow() {
        const { refreshToken, authCode } = this.getFields();
        return !authCode && !!refreshToken;
    }
    /**
     * Updates the cache and persists the authentication fields (encrypted).
     *
     * @param authData New data to save.
     */
    async save(authData) {
        this.update(authData);
        const username = (0, ts_types_1.ensure)(this.getUsername());
        if ((0, sfdc_1.matchesOpaqueAccessToken)(username)) {
            this.logger.debug('Username is an accesstoken. Skip saving authinfo to disk.');
            return this;
        }
        await this.stateAggregator.orgs.write(username);
        this.logger.info(`Saved auth info for username: ${username}`);
        return this;
    }
    /**
     * Update the authorization fields, encrypting sensitive fields, but do not persist.
     * For convenience `this` object is returned.
     *
     * @param authData Authorization fields to update.
     */
    update(authData) {
        if (authData && (0, ts_types_1.isPlainObject)(authData)) {
            if (authData.instanceApiVersion && !(0, sfdc_1.validateApiVersion)(authData.instanceApiVersion)) {
                this.logger.warn(`Ignoring invalid instanceApiVersion "${authData.instanceApiVersion}" (expected format: "XX.0")`);
                authData.instanceApiVersion = undefined;
            }
            this.username = authData.username ?? this.username;
            this.stateAggregator.orgs.update(this.username, authData);
            this.logger.info(`Updated auth info for username: ${this.username}`);
        }
        return this;
    }
    /**
     * Get the auth fields (decrypted) needed to make a connection.
     *
     * @param clientApp Name of the CA/ECA associated with the user.
     */
    getConnectionOptions(clientApp) {
        const decryptedCopy = this.getFields(true);
        const { accessToken, instanceUrl, loginUrl } = decryptedCopy;
        // return main app auth fields
        if (!clientApp) {
            if (this.isAccessTokenFlow()) {
                this.logger.info('Returning fields for a connection using access token.');
                // Just auth with the accessToken
                return { accessToken, instanceUrl, loginUrl };
            }
            if (this.isJwt()) {
                this.logger.info('Returning fields for a connection using JWT config.');
                return {
                    accessToken,
                    instanceUrl,
                    refreshFn: this.refreshFn.bind(this),
                };
            }
            // @TODO: figure out loginUrl and redirectUri (probably get from config class)
            //
            // redirectUri: org.config.getOauthCallbackUrl()
            // loginUrl: this.fields.instanceUrl || this.config.getAppConfig().sfdcLoginUrl
            this.logger.info('Returning fields for a connection using OAuth config.');
            // Decrypt a user provided client secret or use the default.
            return {
                oauth2: {
                    loginUrl: instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION,
                    clientId: this.getClientId(),
                    redirectUri: this.getRedirectUri(),
                },
                accessToken,
                instanceUrl,
                refreshFn: this.refreshFn.bind(this),
            };
        }
        if (!decryptedCopy.clientApps) {
            throw new sfError_1.SfError(`${this.username} does not have any client app linked.`);
        }
        if (!(clientApp in decryptedCopy.clientApps)) {
            throw new sfError_1.SfError(`${this.username} does not have a "${clientApp}" client app linked.`);
        }
        const decryptedApp = decryptedCopy.clientApps[clientApp];
        return {
            oauth2: {
                loginUrl: instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION,
                clientId: decryptedApp.clientId,
                redirectUri: this.getRedirectUri(),
            },
            accessToken: decryptedApp.accessToken,
            instanceUrl,
            // Specific refreshFn for AuthInfo's clientApps.
            //
            // Each client app stores the oauth flow used for its initial auth, here we ensure each refresh returns
            // a token, update the auth file with it and send it back to jsforce's through the callback.
            refreshFn: async (_conn, callback) => {
                // This only handles refresh for web flow.
                // When more flows are supported for client apps, check the `app.oauthFlow` field to set the appropiate refresh helper.
                const authFields = await this.buildRefreshTokenConfig({
                    clientId: decryptedApp.clientId,
                    clientSecret: decryptedApp.clientSecret,
                    refreshToken: decryptedApp.refreshToken,
                    loginUrl: instanceUrl,
                });
                await this.save({
                    clientApps: {
                        ...decryptedCopy.clientApps,
                        [clientApp]: {
                            accessToken: (0, ts_types_1.ensureString)(authFields.accessToken),
                            clientId: decryptedApp.clientId,
                            clientSecret: decryptedApp.clientSecret,
                            refreshToken: decryptedApp.refreshToken,
                            oauthFlow: 'web',
                        },
                    },
                });
                await callback(null, authFields.accessToken);
            },
        };
    }
    getClientId() {
        return this.getFields()?.clientId ?? exports.DEFAULT_CONNECTED_APP_INFO.clientId;
    }
    getRedirectUri() {
        return 'http://localhost:1717/OauthRedirect';
    }
    /**
     * Get the authorization fields.
     *
     * @param decrypt Decrypt the fields.
     *
     * Returns a ReadOnly object of the fields.  If you need to modify the fields, use AuthInfo.update()
     */
    getFields(decrypt) {
        return this.stateAggregator.orgs.get(this.username, decrypt) ?? {};
    }
    /**
     * Get the org front door (used for web based oauth flows)
     *
     * @deprecated Will be removed in the next major version. Use the `Org.getFrontDoorUrl()` method instead.
     */
    getOrgFrontDoorUrl() {
        const authFields = this.getFields(true);
        const base = (0, ts_types_1.ensureString)(authFields.instanceUrl).replace(/\/+$/, '');
        const accessToken = (0, ts_types_1.ensureString)(authFields.accessToken);
        return `${base}/secur/frontdoor.jsp?sid=${accessToken}`;
    }
    /**
     * Returns true if this org is using access token auth.
     */
    isUsingAccessToken() {
        return this.usingAccessToken;
    }
    /**
     * Get the SFDX Auth URL.
     *
     * **See** [SFDX Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_auth.htm#cli_reference_force_auth)
     */
    getSfdxAuthUrl() {
        const { clientId, clientSecret, refreshToken, instanceUrl } = this.getFields(true);
        // host includes an optional port on the instanceUrl
        const url = new URL((0, ts_types_1.ensure)(instanceUrl, 'undefined instanceUrl')).host;
        const clientIdAndSecret = clientId ? `${clientId}:${clientSecret ?? ''}` : '';
        const token = (0, ts_types_1.ensure)(refreshToken, 'undefined refreshToken');
        return `force://${clientIdAndSecret}:${token}@${url}`;
    }
    /**
     * Convenience function to handle typical side effects encountered when dealing with an AuthInfo.
     * Given the values supplied in parameter sideEffects, this function will set auth alias, default auth
     * and default dev hub.
     *
     * @param sideEffects - instance of AuthSideEffects
     */
    async handleAliasAndDefaultSettings(sideEffects) {
        if (Boolean(sideEffects.alias) ||
            sideEffects.setDefault ||
            sideEffects.setDefaultDevHub ||
            typeof sideEffects.setTracksSource === 'boolean') {
            if (sideEffects.alias)
                await this.setAlias(sideEffects.alias);
            if (sideEffects.setDefault)
                await this.setAsDefault({ org: true });
            if (sideEffects.setDefaultDevHub)
                await this.setAsDefault({ devHub: true });
            if (typeof sideEffects.setTracksSource === 'boolean') {
                await this.save({ tracksSource: sideEffects.setTracksSource });
            }
            else {
                await this.save();
            }
        }
    }
    /**
     * Set the target-env (default) or the target-dev-hub to the alias if
     * it exists otherwise to the username. Method will try to set the local
     * config first but will default to global config if that fails.
     *
     * @param options
     */
    async setAsDefault(options = { org: true }) {
        let config;
        // if we fail to create the local config, default to the global config
        try {
            config = await config_1.Config.create({ isGlobal: false });
        }
        catch {
            config = await config_1.Config.create({ isGlobal: true });
        }
        const username = (0, ts_types_1.ensureString)(this.getUsername());
        const alias = this.stateAggregator.aliases.get(username);
        const value = alias ?? username;
        if (options.org) {
            config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG, value);
        }
        if (options.devHub) {
            config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB, value);
        }
        await config.write();
    }
    /**
     * Sets the provided alias to the username
     *
     * @param alias alias to set
     */
    async setAlias(alias) {
        return this.stateAggregator.aliases.setAndSave(alias, this.getUsername());
    }
    /**
     * Initializes an instance of the AuthInfo class.
     */
    async init() {
        this.stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
        const username = this.options.username;
        const authOptions = this.options.oauth2Options ?? this.options.accessTokenOptions;
        // Must specify either username and/or options
        if (!username && !authOptions) {
            throw messages.createError('authInfoCreationError');
        }
        // If a username AND oauth options, ensure an authorization for the username doesn't
        // already exist. Throw if it does so we don't overwrite the authorization.
        if (username && authOptions) {
            if (await this.stateAggregator.orgs.hasFile(username)) {
                throw messages.createError('authInfoOverwriteError');
            }
        }
        const oauthUsername = username ?? authOptions?.username;
        if (oauthUsername) {
            this.username = oauthUsername;
            await this.stateAggregator.orgs.read(oauthUsername, false, false);
        } // Else it will be set in initAuthOptions below.
        // If the username is an access token, use that for auth and don't persist
        if ((0, ts_types_1.isString)(oauthUsername) && (0, sfdc_1.matchesOpaqueAccessToken)(oauthUsername)) {
            // Need to initAuthOptions the logger and authInfoCrypto since we don't call init()
            this.logger = await logger_1.Logger.child('AuthInfo');
            const aggregator = await configAggregator_1.ConfigAggregator.create();
            const instanceUrl = this.getInstanceUrl(aggregator, authOptions);
            this.update({
                accessToken: oauthUsername,
                instanceUrl,
                orgId: oauthUsername.split('!')[0],
                loginUrl: instanceUrl,
            });
            this.usingAccessToken = true;
        }
        // If a username with NO oauth options, ensure authorization already exist.
        else if (username && !authOptions && !(await this.stateAggregator.orgs.exists(username))) {
            const likeName = (0, findSuggestion_1.findSuggestion)(username, [
                ...(await this.stateAggregator.orgs.list()).map((f) => f.split('.json')[0]),
                ...Object.keys(this.stateAggregator.aliases.getAll()),
            ]);
            throw sfError_1.SfError.create({
                name: 'NamedOrgNotFoundError',
                message: messages.getMessage('namedOrgNotFound', [username]),
                actions: likeName === ''
                    ? undefined
                    : [`It looks like you mistyped the username or alias. Did you mean "${likeName}"?`],
            });
        }
        else {
            await this.initAuthOptions(authOptions);
        }
    }
    getInstanceUrl(aggregator, options) {
        const instanceUrl = options?.instanceUrl ?? aggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL);
        return instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
    }
    /**
     * Initialize this AuthInfo instance with the specified options. If options are not provided, initialize it from cache
     * or by reading from the persistence store. For convenience `this` object is returned.
     *
     * @param options Options to be used for creating an OAuth2 instance.
     *
     * **Throws** *{@link SfError}{ name: 'NamedOrgNotFoundError' }* Org information does not exist.
     * @returns {Promise<AuthInfo>}
     */
    async initAuthOptions(options) {
        this.logger = await logger_1.Logger.child('AuthInfo');
        // If options were passed, use those before checking cache and reading an auth file.
        let authConfig;
        if (options) {
            options = structuredClone(options);
            if (this.isTokenOptions(options)) {
                authConfig = options;
                const userInfo = await this.retrieveUserInfo((0, ts_types_1.ensureString)(options.instanceUrl), (0, ts_types_1.ensureString)(options.accessToken));
                this.update({ username: userInfo?.username, orgId: userInfo?.organizationId });
            }
            else {
                if (this.options.parentUsername) {
                    if (process.env.SF_SCRATCH_SIGNUP_CONNECTED_APP) {
                        // When the signup app is overridden, use it as the clientId for the auth code exchange.
                        // Skip loading parent fields entirely — we don't need the parent's privateKey/clientSecret
                        // and the parent org may not be in the StateAggregator cache.
                        options.clientId = process.env.SF_SCRATCH_SIGNUP_CONNECTED_APP;
                        if (process.env.SFDX_CLIENT_SECRET) {
                            options.clientSecret = process.env.SFDX_CLIENT_SECRET;
                        }
                    }
                    else {
                        const parentFields = await this.loadDecryptedAuthFromConfig(this.options.parentUsername);
                        options.clientId = parentFields.clientId;
                        if (process.env.SFDX_CLIENT_SECRET) {
                            options.clientSecret = process.env.SFDX_CLIENT_SECRET;
                        }
                        else {
                            // Grab whatever flow is defined
                            Object.assign(options, {
                                clientSecret: parentFields.clientSecret,
                                privateKey: parentFields.privateKey ? (0, node_path_1.resolve)(parentFields.privateKey) : parentFields.privateKey,
                            });
                        }
                    }
                }
                // jwt flow
                // Support both sfdx and jsforce private key values
                if (!options.privateKey && options.privateKeyFile) {
                    options.privateKey = (0, node_path_1.resolve)(options.privateKeyFile);
                }
                if (options.privateKey) {
                    authConfig = await this.authJwt(options);
                }
                else if (!options.authCode && options.refreshToken) {
                    // refresh token flow (from sfdxUrl or OAuth refreshFn)
                    authConfig = await this.buildRefreshTokenConfig(options);
                }
                else if (this.options.oauth2 instanceof jsforce_node_1.OAuth2) {
                    // authcode exchange / web auth flow
                    authConfig = await this.exchangeToken(options, this.options.oauth2);
                }
                else {
                    authConfig = await this.exchangeToken(options);
                }
            }
            authConfig.isDevHub = await this.determineIfDevHub((0, ts_types_1.ensureString)(authConfig.instanceUrl), (0, ts_types_1.ensureString)(authConfig.accessToken));
            if (authConfig.username)
                await this.stateAggregator.orgs.read(authConfig.username, false, false);
            // Update the auth fields WITH encryption
            this.update(authConfig);
            // Populate Organization metadata (orgEdition, isScratch, isSandbox, etc.) in a single query.
            await (0, determineOrg_1.determineOrg)(this);
        }
        return this;
    }
    // eslint-disable-next-line @typescript-eslint/require-await
    async loadDecryptedAuthFromConfig(username) {
        // Fetch from the persisted auth file
        const authInfo = this.stateAggregator.orgs.get(username, true);
        if (!authInfo) {
            throw messages.createError('namedOrgNotFound', [username]);
        }
        return authInfo;
    }
    isTokenOptions(options) {
        // Although OAuth2Config does not contain refreshToken, privateKey, or privateKeyFile, a JS consumer could still pass those in
        // which WILL have an access token as well, but it should be considered an OAuth2Config at that point.
        return ('accessToken' in options &&
            !('refreshToken' in options) &&
            !('privateKey' in options) &&
            !('privateKeyFile' in options) &&
            !('authCode' in options));
    }
    // A callback function for a connection to refresh an access token.  This is used
    // both for a JWT connection and an OAuth connection.
    async refreshFn(_conn, callback) {
        this.logger.info('Access token has expired. Updating...');
        try {
            const fields = this.getFields(true);
            // This method will request the new access token and save to the current AuthInfo instance (but don't persist them!).
            await this.initAuthOptions(fields);
            // Persist fields with refreshed access token to auth file.
            await this.save();
            // Pass new access token to the jsforce's session-refresh callback for proper propagation:
            // https://jsforce.github.io/jsforce/types/session_refresh_delegate.SessionRefreshFunc.html
            const { accessToken } = this.getFields(true);
            return await callback(null, accessToken);
        }
        catch (err) {
            const error = err;
            if (error?.message?.includes('Data Not Available')) {
                // Set cause to keep original stacktrace
                return callback(messages.createError('orgDataNotAvailableError', [this.getUsername()], [], error));
            }
            return callback(error);
        }
    }
    async readJwtKey(keyFile) {
        return fs_1.fs.promises.readFile(keyFile, 'utf8');
    }
    // Build OAuth config for a JWT auth flow
    async authJwt(options) {
        if (!options.clientId) {
            throw messages.createError('missingClientId');
        }
        const privateKeyContents = await this.readJwtKey((0, ts_types_1.ensureString)(options.privateKey));
        const { loginUrl = sfdcUrl_1.SfdcUrl.PRODUCTION } = options;
        const url = new sfdcUrl_1.SfdcUrl(loginUrl);
        const createdOrgInstance = (this.getFields().createdOrgInstance ?? '').trim().toLowerCase();
        const audienceUrl = await url.getJwtAudienceUrl(createdOrgInstance);
        let authFieldsBuilder;
        const authErrors = [];
        // given that we can no longer depend on instance names or URls to determine audience, let's try them all
        const loginAndAudienceUrls = (0, sfdcUrl_1.getLoginAudienceCombos)(audienceUrl, loginUrl);
        for (const [login, audience] of loginAndAudienceUrls) {
            try {
                // sequentially, in probabilistic order
                // eslint-disable-next-line no-await-in-loop
                authFieldsBuilder = await this.tryJwtAuth(options.clientId, login, audience, privateKeyContents);
                break;
            }
            catch (err) {
                const error = err;
                const message = error.message.includes('audience')
                    ? `${error.message}  [audience=${audience} login=${login}]`
                    : error.message;
                authErrors.push(message);
            }
        }
        if (!authFieldsBuilder) {
            // messages.createError expects names to end in `error` and this one says Errors so do it manually.
            throw new sfError_1.SfError(messages.getMessage('jwtAuthErrors', [authErrors.join('\n')]), 'JwtAuthError');
        }
        const authFields = {
            accessToken: (0, ts_types_1.asString)(authFieldsBuilder.access_token),
            orgId: parseIdUrl((0, ts_types_1.ensureString)(authFieldsBuilder.id)).orgId,
            loginUrl: options.loginUrl,
            privateKey: options.privateKey,
            clientId: options.clientId,
        };
        const instanceUrl = (0, ts_types_1.ensureString)(authFieldsBuilder.instance_url);
        const sfdcUrl = new sfdcUrl_1.SfdcUrl(instanceUrl);
        try {
            // Check if the url is resolvable. This can fail when my-domains have not been replicated.
            await sfdcUrl.lookup();
            authFields.instanceUrl = instanceUrl;
        }
        catch (err) {
            this.logger.debug(
            // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
            `Instance URL [${authFieldsBuilder.instance_url}] is not available.  DNS lookup failed. Using loginUrl [${options.loginUrl}] instead. This may result in a "Destination URL not reset" error.`);
            authFields.instanceUrl = options.loginUrl;
        }
        return authFields;
    }
    async tryJwtAuth(clientId, loginUrl, audienceUrl, privateKeyContents) {
        const jwtToken = jwt.sign({
            iss: clientId,
            sub: this.getUsername(),
            aud: audienceUrl,
            exp: Date.now() + 300,
        }, privateKeyContents, {
            algorithm: 'RS256',
        });
        const oauth2 = new jsforce_node_1.OAuth2({ loginUrl });
        return (0, ts_types_1.ensureJsonMap)(await oauth2.requestToken({
            // eslint-disable-next-line camelcase
            grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            assertion: jwtToken,
        }));
    }
    // Build OAuth config for a refresh token auth flow
    async buildRefreshTokenConfig(options) {
        const fullOptions = {
            ...options,
            redirectUri: options.redirectUri ?? this.getRedirectUri(),
            // Ideally, this would be removed at some point in the distant future when all auth files
            // now have the clientId stored in it.
            ...(options.clientId
                ? {}
                : { clientId: exports.DEFAULT_CONNECTED_APP_INFO.clientId, clientSecret: exports.DEFAULT_CONNECTED_APP_INFO.clientSecret }),
        };
        const oauth2 = new jsforce_node_1.OAuth2(fullOptions);
        let authFieldsBuilder;
        try {
            authFieldsBuilder = await oauth2.refreshToken((0, ts_types_1.ensure)(fullOptions.refreshToken));
        }
        catch (err) {
            const cause = err instanceof Error ? err : sfError_1.SfError.wrap(err);
            throw messages.createError('refreshTokenAuthError', [cause.message], undefined, cause);
        }
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        const { orgId } = parseIdUrl(authFieldsBuilder.id);
        let username = this.getUsername();
        if (!username) {
            const userInfo = await this.retrieveUserInfo(authFieldsBuilder.instance_url, authFieldsBuilder.access_token);
            username = (0, ts_types_1.ensureString)(userInfo?.username);
        }
        return {
            orgId,
            username,
            accessToken: authFieldsBuilder.access_token,
            instanceUrl: authFieldsBuilder.instance_url,
            loginUrl: fullOptions.loginUrl ?? authFieldsBuilder.instance_url,
            refreshToken: fullOptions.refreshToken,
            clientId: fullOptions.clientId,
            clientSecret: fullOptions.clientSecret,
        };
    }
    /**
     * Performs an authCode exchange but the Oauth2 feature of jsforce is extended to include a code_challenge
     *
     * @param options The oauth options
     * @param oauth2 The oauth2 extension that includes a code_challenge
     */
    async exchangeToken(options, oauth2 = new jsforce_node_1.OAuth2(options)) {
        if (!oauth2.redirectUri) {
            oauth2.redirectUri = this.getRedirectUri();
        }
        if (!oauth2.clientId) {
            oauth2.clientId = this.getClientId();
        }
        // Exchange the auth code for an access token and refresh token.
        let authFields;
        try {
            this.logger.debug(`Exchanging auth code for access token using loginUrl: ${options.loginUrl ?? '<undefined>'}`);
            authFields = await oauth2.requestToken((0, ts_types_1.ensure)(options.authCode));
            this.logger.debug(`Successfully requested an access token with scopes: "${authFields.scope}".`);
        }
        catch (err) {
            const msg = err instanceof Error ? `${err.name}::${err.message}` : typeof err === 'string' ? err : 'UNKNOWN';
            const redacted = (0, filters_1.filterSecrets)(options);
            throw sfError_1.SfError.create({
                message: messages.getMessage('authCodeExchangeError', [msg]),
                name: 'AuthCodeExchangeError',
                ...(err instanceof Error ? { cause: err } : {}),
                data: ((0, ts_types_1.isArray)(redacted) ? redacted[0] : redacted),
            });
        }
        const { orgId } = parseIdUrl(authFields.id);
        let username = this.getUsername();
        // Only need to query for the username if it isn't known. For example, a new auth code exchange
        // rather than refreshing a token on an existing connection.
        if (!username) {
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            const userInfo = await this.retrieveUserInfo(authFields.instance_url, authFields.access_token);
            username = userInfo?.username;
        }
        return {
            accessToken: authFields.access_token,
            instanceUrl: authFields.instance_url,
            orgId,
            username,
            loginUrl: options.loginUrl ?? authFields.instance_url,
            refreshToken: authFields.refresh_token,
            clientId: options.clientId,
            clientSecret: options.clientSecret,
        };
    }
    async retrieveUserInfo(instanceUrl, accessToken) {
        // Make a REST call for the username directly.  Normally this is done via a connection
        // but we don't want to create circular dependencies or lots of snowflakes
        // within this file to support it.
        const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
        const instance = (0, ts_types_1.ensure)(instanceUrl);
        const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
        const userInfoUrl = `${baseUrl.toString()}services/oauth2/userinfo`;
        const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
        try {
            this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${userInfoUrl}`);
            let response = await new transport_1.default().httpRequest({ url: userInfoUrl, method: 'GET', headers });
            if (response.statusCode >= 400) {
                this.throwUserGetException(response);
            }
            else {
                const userInfoJson = (0, kit_1.parseJsonMap)(response.body);
                const url = `${baseUrl.toString()}services/data/${apiVersion}/sobjects/User/${userInfoJson.user_id}`;
                this.logger.info(`Sending request for User SObject after successful auth code exchange to URL: ${url}`);
                response = await new transport_1.default().httpRequest({ url, method: 'GET', headers });
                if (response.statusCode >= 400) {
                    this.throwUserGetException(response);
                }
                else {
                    // eslint-disable-next-line camelcase
                    userInfoJson.preferred_username = (0, kit_1.parseJsonMap)(response.body).Username;
                }
                return { username: userInfoJson.preferred_username, organizationId: userInfoJson.organization_id };
            }
        }
        catch (err) {
            throw messages.createError('authCodeUsernameRetrievalError', [err.message]);
        }
    }
    /**
     * Given an error while getting the User object, handle different possibilities of response.body.
     *
     * @param response
     * @private
     */
    throwUserGetException(response) {
        let errorMsg = '';
        const bodyAsString = response.body ?? JSON.stringify({ message: 'UNKNOWN', errorCode: 'UNKNOWN' });
        try {
            const body = (0, kit_1.parseJson)(bodyAsString);
            if ((0, ts_types_1.isArray)(body)) {
                errorMsg = body.map((line) => line.message ?? line.errorCode ?? 'UNKNOWN').join(os.EOL);
            }
            else {
                errorMsg = body.message ?? body.errorCode ?? 'UNKNOWN';
            }
        }
        catch (err) {
            errorMsg = `${bodyAsString}`;
        }
        throw new sfError_1.SfError(errorMsg);
    }
    /**
     * Returns `true` if the org is a Dev Hub.
     *
     * Check access to the ScratchOrgInfo object to determine if the org is a dev hub.
     */
    async determineIfDevHub(instanceUrl, accessToken) {
        // Make a REST call for the ScratchOrgInfo obj directly.  Normally this is done via a connection
        // but we don't want to create circular dependencies or lots of snowflakes
        // within this file to support it.
        const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
        const instance = (0, ts_types_1.ensure)(instanceUrl);
        const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
        const scratchOrgInfoUrl = `${baseUrl.toString()}services/data/${apiVersion}/query?q=SELECT%20Id%20FROM%20ScratchOrgInfo%20limit%201`;
        const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
        try {
            const res = await new transport_1.default().httpRequest({ url: scratchOrgInfoUrl, method: 'GET', headers });
            if (res.statusCode >= 400) {
                return false;
            }
            return true;
        }
        catch (err) {
            /* Not a dev hub */
            return false;
        }
    }
}
exports.AuthInfo = AuthInfo;
//# sourceMappingURL=authInfo.js.map