@salesforce/core 9.1.10 → 9.1.12-qa.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,7 +77,8 @@ export declare enum EnvironmentVariable {
77
77
  'SF_UPDATE_INSTRUCTIONS' = "SF_UPDATE_INSTRUCTIONS",
78
78
  'SF_INSTALLER' = "SF_INSTALLER",
79
79
  'SF_ENV' = "SF_ENV",
80
- 'SF_CAPITALIZE_RECORD_TYPES' = "SF_CAPITALIZE_RECORD_TYPES"
80
+ 'SF_CAPITALIZE_RECORD_TYPES' = "SF_CAPITALIZE_RECORD_TYPES",
81
+ 'SF_SKIP_SCRATCH_ORG_CHECK' = "SF_SKIP_SCRATCH_ORG_CHECK"
81
82
  }
82
83
  type EnvMetaData = {
83
84
  description: string;
@@ -22,7 +22,7 @@ const kit_1 = require("@salesforce/kit");
22
22
  const messages_1 = require("../messages");
23
23
  const lifecycleEvents_1 = require("../lifecycleEvents");
24
24
  ;
25
- const messages = new messages_1.Messages('@salesforce/core', 'envVars', new Map([["forceOpenUrl", "Web page that opens in your browser when you connect to an org. For example, to open Lightning Experience, set to \"lightning\"."], ["forceShowSpinner", "Set to true to show a spinner animation on the command line when running asynchronous CLI commands. Default is false."], ["forceSpinnerDelay", "Speed of the spinner in milliseconds. Default is 60."], ["httpProxy", "HTTP URL and port of the proxy server when using Salesforce CLI behind a corporate firewall or web proxy."], ["httpsProxy", "HTTPS URL and port of the proxy server when using Salesforce CLI behind a corporate firewall or web proxy."], ["nodeExtraCaCerts", "Fully qualified path to your self-signed certificate. Will be installed after you run \"sfdx update\"."], ["nodeTlsRejectUnauthorized", "Set to 0 to allow Node.js to use the self-signed certificate in the certificate chain."], ["sfdxAccessToken", "Specifies an access token when using the auth:accesstoken:store command."], ["sfdxApiVersion", "The API version for a specific project or all projects. Default value is the API version of your Dev Hub. Overrides the apiVersion config value."], ["sfdxAudienceUrl", "URL that overrides the aud (audience) field used for JWT authentication so that it matches the expected value of the authorization server URL for the org you\u2019re logging into."], ["sfdxCodeCoverageRequirement", "Code coverage percentages that are displayed in green when you run force:apex:test:\\* with the --codecoverage parameter."], ["sfdxContentType", "Set to JSON so that all CLI commands output results in JSON format."], ["sfdxDefaultdevhubusername", "Username or alias of your default Dev Hub org. Overrides the defaultdevhubusername configuration value."], ["sfdxDefaultusername", "Username or alias of your default org. Overrides the defaultusername configuration value."], ["sfdxDisableAutoupdate", "Set to true to disable the auto-update feature of Salesforce CLI. Default value is false."], ["sfdxAutoupdateDisable", "Set to true to disable the auto-update feature of Salesforce CLI. Default value is false."], ["sfdxDisableSourceMemberPolling", "Set to true to disable polling of your org\u2019s SourceMember object when you run the force:source:push|pull commands. Default value is false."], ["sfdxSourceTrackingBatchSize", "Number of source-tracked file updates to batch after a deploy or retrieve. The default value is 8,000 (Windows) or 15,000 (Linux/macOS)."], ["sfdxDisableTelemetry", "Set to true to disable Salesforce CLI from collecting usage information, user environment information, and crash reports. Default value is false. Overrides the disableTelemetry configration variable."], ["sfdxDnsTimeout", "Number of seconds that the force:org:\\* commands wait for a response when checking whether an org is connected. Default value is 3."], ["sfdxDomainRetry", "Time, in seconds, that Salesforce CLI waits for the Lightning Experience custom domain to resolve and become available in a scratch org. Default value is 240."], ["sfdxImprovedCodeCoverage", "Set to true to scope Apex test results to the classes entered during a test run when running force:apex:test:\\*."], ["sfdxInstanceUrl", "URL of the Salesforce instance that is hosting your org. Default value is https://login.salesforce.com. Overrides the instanceUrl configuration value."], ["sfdxJsonToStdout", "Set to true to send messages resulting from failed Salesforce CLI commands to stdout instead of stderr."], ["sfdxDisableLogFile", "Set to true to disable log file writing"], ["sfdxLogLevel", "Level of messages that the CLI writes to the log file. Valid values are trace, debug, info, warn, error, fatal. Default value is warn."], ["sfdxLogRotationCount", "The default rotation period for logs. Example '1d' will rotate logs daily (at midnight)."], ["sfdxLogRotationPeriod", "The number of backup rotated log files to keep. Example: '3' will have the base sf.log file, and the past 3 (period) log files."], ["sfdxMaxQueryLimit", "Maximum number of Salesforce records returned by a CLI command. Default value is 10,000. Overrides the maxQueryLimit configuration value."], ["sfdxMdapiTempDir", "Directory that contains files (in metadata format) when running certain Salesforce CLI commands, such as force:source:\\*."], ["sfdxNpmRegistry", "URL to a private npm server, where all packages that you publish are private."], ["sfdxPrecompileEnable", "Set to true to enable Apex pre-compile before the tests are run with the force:apex:test:run command. Default is false."], ["sfdxProjectAutoupdateDisableForPackageCreate", "Set to true to disable automatic updates to sfdx-project.json when running force:package:create."], ["sfdxProjectAutoupdateDisableForPackageVersionCreate", "Set to true to disable automatic updates to sfdx-project.json when running force:package:version:create."], ["sfdxRestDeploy", "Set to true to make Salesforce CLI use the Metadata REST API for deployments. By default, the CLI uses SOAP."], ["sfdxSourceMemberPollingTimeout", "Number of seconds you want the force:source:push command to keep polling the SourceMember object before the command times out."], ["sfdxUseGenericUnixKeychain", "(Linux and macOS only) Set to true if you want to use the generic UNIX keychain instead of the Linux libsecret library or macOS keychain."], ["sfdxUseProgressBar", "Set to false to disable the progress bar when running force:mdapi:deploy, force:source:deploy, or force:source:push."], ["sfdxLazyLoadModules", "Set to true to enable lazy loading of sfdx modules"], ["sfdxS3Host", "URL to S3 host"], ["sfdxUpdateInstructions", "Text that describes how to update sfdx"], ["sfdxInstaller", "Boolean indicating that the installer is running"], ["sfdxEnv", "Describes if sfdx is in \"demo\" mode"], ["sfTargetOrg", "Username or alias of your default org. Overrides the target-org configuration variable."], ["sfTargetDevHub", "Username or alias of your default Dev Hub org. Overrides the target-dev-hub configuration variable."], ["sfAccessToken", "Specifies an access token when using a login command that uses access tokens."], ["sfOrgApiVersion", "API version for a specific project or all projects. Default value is the API version of your Dev Hub. Overrides the apiVersion configuration variable."], ["sfAudienceUrl", "URL that overrides the aud (audience) field used for JWT authentication so that it matches the expected value of the authorization server URL for the org you\u2019re logging into."], ["sfCodeCoverageRequirement", "Code coverage percentages that are displayed in green when you run the Apex test CLIcommands with the --code-coverage flag."], ["sfContentType", "Set to JSON so that all CLI commands output results in JSON format."], ["sfDisableAutoupdate", "Set to true to disable the auto-update feature of Salesforce CLI. Default value is false."], ["sfAutoupdateDisable", "Set to true to disable the auto-update feature of Salesforce CLI. Default value is false."], ["sfDisableSourceMemberPolling", "Set to true to disable polling of your org\u2019s SourceMember object when you run the commands to push and pull source. Default value is false."], ["sfDisableTelemetry", "Set to true to disable Salesforce CLI from collecting usage information, user environment information, and crash reports. Default value is false. Overrides the disableTelemetry configration variable."], ["sfDnsTimeout", "Number of seconds that the env commands wait for a response when checking whether an org is connected. Default value is 3."], ["sfDomainRetry", "Time, in seconds, that Salesforce CLI waits for the Lightning Experience custom domain to resolve and become available in a scratch org. Default value is 240."], ["sfImprovedCodeCoverage", "Set to true to scope Apex test results to the classes entered during a test run when running the Apex test commands."], ["sfOrgInstanceUrl", "URL of the Salesforce instance that is hosting your org. Default value is https://login.salesforce.com. Overrides the instanceUrl configuration variable."], ["sfJsonToStdout", "Set to true to send messages resulting from failed Salesforce CLI commands to stdout instead of stderr."], ["sfDisableLogFile", "Set to true to disable log file writing"], ["sfLogLevel", "Level of messages that the CLI writes to the log file. Valid values are trace, debug, info, warn, error, fatal. Default value is warn."], ["sfLogRotationCount", "The default rotation period for logs. Example '1d' will rotate logs daily (at midnight)."], ["sfLogRotationPeriod", "The number of backup rotated log files to keep. Example: '3' will have the base sf.log file, and the past 3 (period) log files."], ["sfOrgMaxQueryLimit", "Maximum number of Salesforce records returned by a CLI command. Default value is 10,000. Overrides the maxQueryLimit configuration variable."], ["sfMdapiTempDir", "Directory that contains files (in metadata format) when running certain Salesforce CLI commands."], ["sfNpmRegistry", "URL to a private npm server, where all packages that you publish are private."], ["sfPrecompileEnable", "Set to true to enable Apex pre-compile before the tests are run with the Apex test run command. Default is false."], ["sfProjectAutoupdateDisableForPackageCreate", "Set to true to disable automatic updates to sfdx-project.json when running the package create command."], ["sfProjectAutoupdateDisableForPackageVersionCreate", "Set to true to disable automatic updates to sfdx-project.json when running the package version create command."], ["sfSourceMemberPollingTimeout", "Number of seconds you want the source push command to keep polling the SourceMember object before the command times out."], ["sfUseGenericUnixKeychain", "(Linux and macOS only) Set to true if you want to use the generic UNIX keychain instead of the Linux libsecret library or macOS keychain."], ["sfUseProgressBar", "Set to false to disable the progress bar when running the metadata deploy command."], ["sfLazyLoadModules", "Set to true to enable lazy loading of sf modules"], ["sfS3Host", "URL to S3 host"], ["sfUpdateInstructions", "Text that describes how to update sf"], ["sfInstaller", "Boolean indicating that the installer is running"], ["sfEnv", "Describes if sf is in \"demo\" mode"], ["deprecatedEnv", "Deprecated environment variable: %s. Please use %s instead."], ["deprecatedEnvDisagreement", "Deprecated environment variable: %s. Please use %s instead.\n\nYour environment has both variables populated, and with different values. The value from %s will be used."], ["sfCapitalizeRecordTypes", "Whether record types are capitalized on scratch org creation."]]));
25
+ const messages = new messages_1.Messages('@salesforce/core', 'envVars', new Map([["forceOpenUrl", "Web page that opens in your browser when you connect to an org. For example, to open Lightning Experience, set to \"lightning\"."], ["forceShowSpinner", "Set to true to show a spinner animation on the command line when running asynchronous CLI commands. Default is false."], ["forceSpinnerDelay", "Speed of the spinner in milliseconds. Default is 60."], ["httpProxy", "HTTP URL and port of the proxy server when using Salesforce CLI behind a corporate firewall or web proxy."], ["httpsProxy", "HTTPS URL and port of the proxy server when using Salesforce CLI behind a corporate firewall or web proxy."], ["nodeExtraCaCerts", "Fully qualified path to your self-signed certificate. Will be installed after you run \"sfdx update\"."], ["nodeTlsRejectUnauthorized", "Set to 0 to allow Node.js to use the self-signed certificate in the certificate chain."], ["sfdxAccessToken", "Specifies an access token when using the auth:accesstoken:store command."], ["sfdxApiVersion", "The API version for a specific project or all projects. Default value is the API version of your Dev Hub. Overrides the apiVersion config value."], ["sfdxAudienceUrl", "URL that overrides the aud (audience) field used for JWT authentication so that it matches the expected value of the authorization server URL for the org you\u2019re logging into."], ["sfdxCodeCoverageRequirement", "Code coverage percentages that are displayed in green when you run force:apex:test:\\* with the --codecoverage parameter."], ["sfdxContentType", "Set to JSON so that all CLI commands output results in JSON format."], ["sfdxDefaultdevhubusername", "Username or alias of your default Dev Hub org. Overrides the defaultdevhubusername configuration value."], ["sfdxDefaultusername", "Username or alias of your default org. Overrides the defaultusername configuration value."], ["sfdxDisableAutoupdate", "Set to true to disable the auto-update feature of Salesforce CLI. Default value is false."], ["sfdxAutoupdateDisable", "Set to true to disable the auto-update feature of Salesforce CLI. Default value is false."], ["sfdxDisableSourceMemberPolling", "Set to true to disable polling of your org\u2019s SourceMember object when you run the force:source:push|pull commands. Default value is false."], ["sfdxSourceTrackingBatchSize", "Number of source-tracked file updates to batch after a deploy or retrieve. The default value is 8,000 (Windows) or 15,000 (Linux/macOS)."], ["sfdxDisableTelemetry", "Set to true to disable Salesforce CLI from collecting usage information, user environment information, and crash reports. Default value is false. Overrides the disableTelemetry configration variable."], ["sfdxDnsTimeout", "Number of seconds that the force:org:\\* commands wait for a response when checking whether an org is connected. Default value is 3."], ["sfdxDomainRetry", "Time, in seconds, that Salesforce CLI waits for the Lightning Experience custom domain to resolve and become available in a scratch org. Default value is 240."], ["sfdxImprovedCodeCoverage", "Set to true to scope Apex test results to the classes entered during a test run when running force:apex:test:\\*."], ["sfdxInstanceUrl", "URL of the Salesforce instance that is hosting your org. Default value is https://login.salesforce.com. Overrides the instanceUrl configuration value."], ["sfdxJsonToStdout", "Set to true to send messages resulting from failed Salesforce CLI commands to stdout instead of stderr."], ["sfdxDisableLogFile", "Set to true to disable log file writing"], ["sfdxLogLevel", "Level of messages that the CLI writes to the log file. Valid values are trace, debug, info, warn, error, fatal. Default value is warn."], ["sfdxLogRotationCount", "The default rotation period for logs. Example '1d' will rotate logs daily (at midnight)."], ["sfdxLogRotationPeriod", "The number of backup rotated log files to keep. Example: '3' will have the base sf.log file, and the past 3 (period) log files."], ["sfdxMaxQueryLimit", "Maximum number of Salesforce records returned by a CLI command. Default value is 10,000. Overrides the maxQueryLimit configuration value."], ["sfdxMdapiTempDir", "Directory that contains files (in metadata format) when running certain Salesforce CLI commands, such as force:source:\\*."], ["sfdxNpmRegistry", "URL to a private npm server, where all packages that you publish are private."], ["sfdxPrecompileEnable", "Set to true to enable Apex pre-compile before the tests are run with the force:apex:test:run command. Default is false."], ["sfdxProjectAutoupdateDisableForPackageCreate", "Set to true to disable automatic updates to sfdx-project.json when running force:package:create."], ["sfdxProjectAutoupdateDisableForPackageVersionCreate", "Set to true to disable automatic updates to sfdx-project.json when running force:package:version:create."], ["sfdxRestDeploy", "Set to true to make Salesforce CLI use the Metadata REST API for deployments. By default, the CLI uses SOAP."], ["sfdxSourceMemberPollingTimeout", "Number of seconds you want the force:source:push command to keep polling the SourceMember object before the command times out."], ["sfdxUseGenericUnixKeychain", "(Linux and macOS only) Set to true if you want to use the generic UNIX keychain instead of the Linux libsecret library or macOS keychain."], ["sfdxUseProgressBar", "Set to false to disable the progress bar when running force:mdapi:deploy, force:source:deploy, or force:source:push."], ["sfdxLazyLoadModules", "Set to true to enable lazy loading of sfdx modules"], ["sfdxS3Host", "URL to S3 host"], ["sfdxUpdateInstructions", "Text that describes how to update sfdx"], ["sfdxInstaller", "Boolean indicating that the installer is running"], ["sfdxEnv", "Describes if sfdx is in \"demo\" mode"], ["sfTargetOrg", "Username or alias of your default org. Overrides the target-org configuration variable."], ["sfTargetDevHub", "Username or alias of your default Dev Hub org. Overrides the target-dev-hub configuration variable."], ["sfAccessToken", "Specifies an access token when using a login command that uses access tokens."], ["sfOrgApiVersion", "API version for a specific project or all projects. Default value is the API version of your Dev Hub. Overrides the apiVersion configuration variable."], ["sfAudienceUrl", "URL that overrides the aud (audience) field used for JWT authentication so that it matches the expected value of the authorization server URL for the org you\u2019re logging into."], ["sfCodeCoverageRequirement", "Code coverage percentages that are displayed in green when you run the Apex test CLIcommands with the --code-coverage flag."], ["sfContentType", "Set to JSON so that all CLI commands output results in JSON format."], ["sfDisableAutoupdate", "Set to true to disable the auto-update feature of Salesforce CLI. Default value is false."], ["sfAutoupdateDisable", "Set to true to disable the auto-update feature of Salesforce CLI. Default value is false."], ["sfDisableSourceMemberPolling", "Set to true to disable polling of your org\u2019s SourceMember object when you run the commands to push and pull source. Default value is false."], ["sfDisableTelemetry", "Set to true to disable Salesforce CLI from collecting usage information, user environment information, and crash reports. Default value is false. Overrides the disableTelemetry configration variable."], ["sfDnsTimeout", "Number of seconds that the env commands wait for a response when checking whether an org is connected. Default value is 3."], ["sfDomainRetry", "Time, in seconds, that Salesforce CLI waits for the Lightning Experience custom domain to resolve and become available in a scratch org. Default value is 240."], ["sfImprovedCodeCoverage", "Set to true to scope Apex test results to the classes entered during a test run when running the Apex test commands."], ["sfOrgInstanceUrl", "URL of the Salesforce instance that is hosting your org. Default value is https://login.salesforce.com. Overrides the instanceUrl configuration variable."], ["sfJsonToStdout", "Set to true to send messages resulting from failed Salesforce CLI commands to stdout instead of stderr."], ["sfDisableLogFile", "Set to true to disable log file writing"], ["sfLogLevel", "Level of messages that the CLI writes to the log file. Valid values are trace, debug, info, warn, error, fatal. Default value is warn."], ["sfLogRotationCount", "The default rotation period for logs. Example '1d' will rotate logs daily (at midnight)."], ["sfLogRotationPeriod", "The number of backup rotated log files to keep. Example: '3' will have the base sf.log file, and the past 3 (period) log files."], ["sfOrgMaxQueryLimit", "Maximum number of Salesforce records returned by a CLI command. Default value is 10,000. Overrides the maxQueryLimit configuration variable."], ["sfMdapiTempDir", "Directory that contains files (in metadata format) when running certain Salesforce CLI commands."], ["sfNpmRegistry", "URL to a private npm server, where all packages that you publish are private."], ["sfPrecompileEnable", "Set to true to enable Apex pre-compile before the tests are run with the Apex test run command. Default is false."], ["sfProjectAutoupdateDisableForPackageCreate", "Set to true to disable automatic updates to sfdx-project.json when running the package create command."], ["sfProjectAutoupdateDisableForPackageVersionCreate", "Set to true to disable automatic updates to sfdx-project.json when running the package version create command."], ["sfSourceMemberPollingTimeout", "Number of seconds you want the source push command to keep polling the SourceMember object before the command times out."], ["sfUseGenericUnixKeychain", "(Linux and macOS only) Set to true if you want to use the generic UNIX keychain instead of the Linux libsecret library or macOS keychain."], ["sfUseProgressBar", "Set to false to disable the progress bar when running the metadata deploy command."], ["sfLazyLoadModules", "Set to true to enable lazy loading of sf modules"], ["sfS3Host", "URL to S3 host"], ["sfUpdateInstructions", "Text that describes how to update sf"], ["sfInstaller", "Boolean indicating that the installer is running"], ["sfEnv", "Describes if sf is in \"demo\" mode"], ["deprecatedEnv", "Deprecated environment variable: %s. Please use %s instead."], ["deprecatedEnvDisagreement", "Deprecated environment variable: %s. Please use %s instead.\n\nYour environment has both variables populated, and with different values. The value from %s will be used."], ["sfCapitalizeRecordTypes", "Whether record types are capitalized on scratch org creation."], ["sfSkipScratchOrgCheck", "Set to \"true\" to skip the scratch org and sandbox identification check that runs after authenticating an org. When enabled, `sf org login jwt` (and other login commands) will not query existing cached orgs to determine whether the newly authenticated org is a scratch org or sandbox. This can significantly speed up authentication in CI/CD environments with many cached org authorizations."]]));
26
26
  var EnvironmentVariable;
27
27
  (function (EnvironmentVariable) {
28
28
  EnvironmentVariable["FORCE_OPEN_URL"] = "FORCE_OPEN_URL";
@@ -102,6 +102,7 @@ var EnvironmentVariable;
102
102
  EnvironmentVariable["SF_INSTALLER"] = "SF_INSTALLER";
103
103
  EnvironmentVariable["SF_ENV"] = "SF_ENV";
104
104
  EnvironmentVariable["SF_CAPITALIZE_RECORD_TYPES"] = "SF_CAPITALIZE_RECORD_TYPES";
105
+ EnvironmentVariable["SF_SKIP_SCRATCH_ORG_CHECK"] = "SF_SKIP_SCRATCH_ORG_CHECK";
105
106
  })(EnvironmentVariable || (exports.EnvironmentVariable = EnvironmentVariable = {}));
106
107
  const getMessage = (environmentVariable) => messages.getMessage((0, change_case_1.camelCase)(environmentVariable));
107
108
  exports.SUPPORTED_ENV_VARS = {
@@ -416,6 +417,10 @@ exports.SUPPORTED_ENV_VARS = {
416
417
  description: getMessage(EnvironmentVariable.SF_CAPITALIZE_RECORD_TYPES),
417
418
  synonymOf: null,
418
419
  },
420
+ [EnvironmentVariable.SF_SKIP_SCRATCH_ORG_CHECK]: {
421
+ description: getMessage(EnvironmentVariable.SF_SKIP_SCRATCH_ORG_CHECK),
422
+ synonymOf: null,
423
+ },
419
424
  };
420
425
  class EnvVars extends kit_1.Env {
421
426
  constructor(env = process.env) {
@@ -321,6 +321,72 @@ export declare class AuthInfo extends AsyncOptionalCreatable<AuthInfo.Options> {
321
321
  private loadDecryptedAuthFromConfig;
322
322
  private isTokenOptions;
323
323
  private refreshFn;
324
+ /**
325
+ * Single-flight a refresh-token refresh so two connections/processes can't rotate the same token at once.
326
+ *
327
+ * With Refresh Token Rotation (RTR) enabled, each refresh returns a new refresh token and invalidates the
328
+ * previous one; a second refresh sent with the now-stale refresh token fails and invalidates the current token too.
329
+ * We take a cross-process lock (which also serializes same-process contenders), then re-read the latest
330
+ * tokens from disk: if another actor already rotated while we waited, we adopt their fresh credentials
331
+ * instead of refreshing again with our now-invalid token.
332
+ *
333
+ * Each pass is one call to `tryRotateOrAdopt`, which (1) adopts if disk already holds a rotated token, else
334
+ * (2) tries to acquire the lock and, once held, re-checks-then-refreshes. If we can't acquire (a live holder
335
+ * is mid-rotation, ELOCKED), we do NOT fall back to an unlocked refresh, which would race the holder and double-rotate
336
+ * under RTR; we simply loop back to (1) and re-run the pass (re-read disk, then try to acquire the lock again).
337
+ * `proper-lockfile` only grants the lock on genuine release or genuine staleness (a live
338
+ * holder refreshes its lock mtime ~every 5s and so is never stolen from), so looping converges: a slow holder is waited out,
339
+ * a dead holder's lock crosses the ~10s stale line and is stolen on a later attempt.
340
+ * Only a holder that keeps the lock alive for the whole budget below yields the timeout error.
341
+ *
342
+ * The lock uses a dedicated `<authfile>.token-rotation.lock`, kept separate from ConfigFile's
343
+ * own `<authfile>.lock` write lock, because proper-lockfile is not re-entrant: holding the auth-file lock
344
+ * here and then letting save() re-acquire the same path would self-block.
345
+ *
346
+ * KNOWN LIMITATIONS (all inherent, not bugs):
347
+ *
348
+ * (1) Only actors that take THIS lock are serialized. Within a single CLI release this is a non-issue: the
349
+ * CLI deduplicates @salesforce/core to one version, so every in-process refresher runs this same locking
350
+ * code (and the lock path is derived deterministically, so differing versions that BOTH lock still
351
+ * interoperate). The realistic gap is a 2nd/3rd-party plugin: those install under the CLI's plugin data dir
352
+ * with their OWN non-deduped node_modules and may bundle a core version predating this lock. Such a plugin
353
+ * (or any external tool that refreshes the token directly) won't honor `<authfile>.token-rotation.lock` and
354
+ * can still double-rotate the same auth while we hold it. There is no server-side coordination to prevent this.
355
+ *
356
+ * (2) On the web runtime (Global.isWeb), lockInit is a no-op that takes no lock (matching existing
357
+ * ConfigFile behavior), so nothing is serialized there.
358
+ *
359
+ * (3) A genuinely-stuck holder makes this block for up to the budget below plus one in-flight lock cycle
360
+ * (~40s) before throwing. Because that happens inside a live jsforce session-refresh, an upstream client
361
+ * with a shorter timeout may give up first with a less specific error. Only the pathological stuck case
362
+ * pays this; the common paths return in one lock cycle or via the lock-free adopt.
363
+ *
364
+ * @param heldRefreshToken the refresh token this connection currently holds (read from the auth file); the
365
+ * baseline we compare against disk to detect a rotation, and the token we would send if we do rotate.
366
+ */
367
+ private refreshWithTokenRotationLock;
368
+ /**
369
+ * One rotation attempt: adopt an already-rotated token if disk has one, else take the lock and
370
+ * double-check-then-rotate under it.
371
+ *
372
+ * @returns `true` if we adopted or refreshed (caller is done); `false` if the lock was contended
373
+ * (`ELOCKED`) and the caller should re-attempt. Throws on any non-`ELOCKED` failure.
374
+ */
375
+ private tryRotateOrAdopt;
376
+ /**
377
+ * Re-read the on-disk auth (without disturbing this instance's in-memory fields) and, if another
378
+ * connection/process has already rotated the refresh token, adopt those fresh credentials. Disk is
379
+ * already current in that case, so no save is needed.
380
+ *
381
+ * NOTE: this gates on the refresh token only, not access-token freshness (we don't persist access-token
382
+ * expiry). If the adopted access token has since expired, jsforce gets a 401 and re-enters refreshFn,
383
+ * which then rotates under the lock (our refresh token now matches disk, so the double-check falls through
384
+ * to a real refresh). That is one extra round trip in a narrow case, and still strictly better than the
385
+ * pre-adopt behavior, where refreshing with our rotated-out token would have failed outright.
386
+ *
387
+ * @returns true if a rotated token was adopted; false if the on-disk token still matches ours.
388
+ */
389
+ private adoptIfAlreadyRotated;
324
390
  private readJwtKey;
325
391
  private authJwt;
326
392
  private tryJwtAuth;
@@ -72,12 +72,13 @@ const filters_1 = require("../logger/filters");
72
72
  const messages_1 = require("../messages");
73
73
  const sfdcUrl_1 = require("../util/sfdcUrl");
74
74
  const findSuggestion_1 = require("../util/findSuggestion");
75
+ const fileLocking_1 = require("../util/fileLocking");
75
76
  const connection_1 = require("./connection");
76
77
  const determineOrg_1 = require("./determineOrg");
77
78
  const org_1 = require("./org");
78
79
  const orgConfigProperties_1 = require("./orgConfigProperties");
79
80
  ;
80
- 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."]]));
81
+ 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"], ["refreshTokenAuthError.actions", ["The stored refresh token is no longer valid (it may have expired, been revoked, or been rotated). Re-authenticate to the org, then retry. For example, run \"sf org login web --alias <your-alias>\""]], ["refreshTokenRotationTimeoutError", "Timed out waiting for another process to finish rotating the refresh token for %s. Another process held the rotation lock for the entire wait without completing. Retry the command. If it persists, another \"sf\" process is likely stuck mid-refresh: stop any other running \"sf\" processes, then retry."], ["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."]]));
81
82
  // parses the id field returned from jsForce oauth2 methods to get
82
83
  // user ID and org ID.
83
84
  function parseIdUrl(idUrl) {
@@ -185,22 +186,25 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
185
186
  // prevent ConfigFile collision bug
186
187
  // eslint-disable-next-line no-await-in-loop
187
188
  const authInfo = await AuthInfo.create({ username });
188
- const { orgId, instanceUrl, devHubUsername, expirationDate, isDevHub } = authInfo.getFields();
189
+ const authFields = authInfo.getFields();
190
+ const { orgId, instanceUrl, devHubUsername, expirationDate, isDevHub } = authFields;
191
+ const isScratchOrg = Boolean(devHubUsername) || Boolean(authFields[org_1.Org.Fields.IS_SCRATCH]);
192
+ const expDate = expirationDate ?? authFields[org_1.Org.Fields.TRIAL_EXPIRATION_DATE];
193
+ // eslint-disable-next-line no-await-in-loop
194
+ const hasSandboxFile = await stateAggregator.sandboxes.hasFile(orgId);
195
+ const isSandbox = Boolean(authFields[org_1.Org.Fields.IS_SANDBOX]) || hasSandboxFile;
189
196
  final.push({
190
197
  aliases,
191
198
  configs,
192
199
  username,
193
200
  instanceUrl,
194
- isScratchOrg: Boolean(devHubUsername),
201
+ isScratchOrg,
195
202
  isDevHub: isDevHub ?? false,
196
- // eslint-disable-next-line no-await-in-loop
197
- isSandbox: await stateAggregator.sandboxes.hasFile(orgId),
203
+ isSandbox,
198
204
  orgId: orgId,
199
205
  accessToken: authInfo.getConnectionOptions().accessToken,
200
206
  oauthMethod: authInfo.isJwt() ? 'jwt' : authInfo.isOauth() ? 'web' : 'token',
201
- isExpired: Boolean(devHubUsername) && expirationDate
202
- ? new Date((0, ts_types_1.ensureString)(expirationDate)).getTime() < new Date().getTime()
203
- : 'unknown',
207
+ isExpired: isScratchOrg && expDate ? new Date((0, ts_types_1.ensureString)(expDate)).getTime() < new Date().getTime() : 'unknown',
204
208
  });
205
209
  }
206
210
  catch (err) {
@@ -283,31 +287,83 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
283
287
  * @param orgAuthInfo
284
288
  */
285
289
  static async identifyPossibleScratchOrgs(fields, orgAuthInfo) {
290
+ if (kit_1.env.getBoolean('SF_SKIP_SCRATCH_ORG_CHECK')) {
291
+ (await logger_1.Logger.child('Common', { tag: 'identifyPossibleScratchOrgs' })).debug('Skipping scratch org identification due to SF_SKIP_SCRATCH_ORG_CHECK');
292
+ return;
293
+ }
286
294
  // fields property is passed in because the consumers of this method have performed the decrypt.
287
295
  // This is so we don't have to call authInfo.getFields(true) and decrypt again OR accidentally save an
288
296
  // authInfo before it is necessary.
289
297
  const logger = await logger_1.Logger.child('Common', { tag: 'identifyPossibleScratchOrgs' });
290
298
  await (0, determineOrg_1.determineOrg)(orgAuthInfo);
299
+ // re-read fields after determineOrg may have updated them
300
+ const updatedFields = orgAuthInfo.getFields();
291
301
  // return if we already know the hub org, we know it is a devhub or prod-like, or no orgId present
292
- if (Boolean(fields.isDevHub) || Boolean(fields.devHubUsername) || !fields.orgId)
302
+ if (Boolean(updatedFields.isDevHub) || Boolean(updatedFields.devHubUsername) || !updatedFields.orgId)
303
+ return;
304
+ if (updatedFields.isSandbox) {
305
+ logger.debug('determineOrg already identified org as sandbox, skipping expensive org scan');
293
306
  return;
307
+ }
308
+ // for scratch orgs, skip the full listAllAuthorizations + sandbox identification and
309
+ // instead do a lightweight DevHub lookup using raw configs (no AuthInfo creation/decryption)
310
+ if (updatedFields.isScratch) {
311
+ logger.debug('determineOrg identified org as scratch, doing lightweight DevHub lookup');
312
+ const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
313
+ const allConfigs = await stateAggregator.orgs.readAll();
314
+ const devHubUsernames = allConfigs.filter((c) => c.isDevHub).map((c) => (0, ts_types_1.ensureString)(c.username));
315
+ for (const hubUsername of devHubUsernames) {
316
+ try {
317
+ // eslint-disable-next-line no-await-in-loop
318
+ const soi = await AuthInfo.queryScratchOrg(hubUsername, updatedFields.orgId);
319
+ // eslint-disable-next-line no-await-in-loop
320
+ await orgAuthInfo.save({
321
+ ...fields,
322
+ devHubUsername: hubUsername,
323
+ expirationDate: soi.ExpirationDate,
324
+ isScratch: true,
325
+ });
326
+ logger.debug(`set ${hubUsername} as devhub for scratch org ${orgAuthInfo.getUsername()}`);
327
+ return;
328
+ }
329
+ catch (error) {
330
+ if (error instanceof Error && error.name === 'NoActiveScratchOrgFound') {
331
+ logger.debug(`devhub ${hubUsername} does not own this scratch org`);
332
+ }
333
+ else {
334
+ logger.debug(`Error connecting to devhub ${hubUsername}`, error);
335
+ }
336
+ }
337
+ }
338
+ return;
339
+ }
294
340
  logger.debug('getting devHubs and prod orgs to identify scratch orgs and sandboxes');
295
- // TODO: return if url is not sandbox-like to avoid constantly asking about production orgs
296
341
  // TODO: someday we make this easier by asking the org if it is a scratch org
297
- const hubAuthInfos = await AuthInfo.getDevHubAuthInfos();
298
- // Get a list of org auths that are known not to be scratch orgs or sandboxes.
299
- const possibleProdOrgs = await AuthInfo.listAllAuthorizations((orgAuth) => orgAuth && !orgAuth.isScratchOrg && !orgAuth.isSandbox);
342
+ const allOrgs = await AuthInfo.listAllAuthorizations();
343
+ const hubAuthInfos = allOrgs.filter((org) => org.isDevHub);
344
+ // skip sandbox identification if the instance URL is not sandbox-like
345
+ // enhanced domains: *.sandbox.my.salesforce.com; pre-enhanced My Domain: company--sbxname.my.salesforce.com
346
+ const isSandboxLikeUrl = fields.instanceUrl != null && (fields.instanceUrl.includes('.sandbox.') || fields.instanceUrl.includes('--'));
347
+ const possibleProdOrgs = isSandboxLikeUrl ? allOrgs.filter((org) => !org.isScratchOrg && !org.isSandbox) : [];
348
+ if (!isSandboxLikeUrl) {
349
+ logger.debug('instance URL is not sandbox-like, skipping sandbox identification');
350
+ }
300
351
  logger.debug(`found ${hubAuthInfos.length} DevHubs`);
301
352
  logger.debug(`found ${possibleProdOrgs.length} possible prod orgs`);
302
353
  if (hubAuthInfos.length === 0 && possibleProdOrgs.length === 0) {
303
354
  return;
304
355
  }
305
- // ask all those orgs if they know this orgId
356
+ let identified = false;
357
+ // ask all those orgs if they know this orgId, but stop once one claims it
306
358
  await Promise.all([
307
359
  ...hubAuthInfos.map(async (hubAuthInfo) => {
360
+ if (identified)
361
+ return;
308
362
  try {
309
363
  const soi = await AuthInfo.queryScratchOrg(hubAuthInfo.username, fields.orgId);
310
- // if any return a result
364
+ if (identified)
365
+ return;
366
+ identified = true;
311
367
  logger.debug(`found orgId ${fields.orgId ?? '<undefined>'} in devhub ${hubAuthInfo.username}`);
312
368
  try {
313
369
  await orgAuthInfo.save({
@@ -332,7 +388,11 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
332
388
  }
333
389
  }),
334
390
  ...possibleProdOrgs.map(async (pOrgAuthInfo) => {
335
- await AuthInfo.identifyPossibleSandbox(pOrgAuthInfo, fields, orgAuthInfo, logger);
391
+ if (identified)
392
+ return;
393
+ const found = await AuthInfo.identifyPossibleSandbox(pOrgAuthInfo, fields, orgAuthInfo, logger);
394
+ if (found)
395
+ identified = true;
336
396
  }),
337
397
  ]);
338
398
  }
@@ -344,13 +404,13 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
344
404
  }
345
405
  static async identifyPossibleSandbox(possibleProdOrg, fields, orgAuthInfo, logger) {
346
406
  if (!fields.orgId) {
347
- return;
407
+ return false;
348
408
  }
349
409
  try {
350
410
  const prodOrg = await org_1.Org.create({ aliasOrUsername: possibleProdOrg.username });
351
411
  const sbxProcess = await prodOrg.querySandboxProcessByOrgId(fields.orgId);
352
412
  if (!sbxProcess?.SandboxInfoId) {
353
- return;
413
+ return false;
354
414
  }
355
415
  logger.debug(`${fields.orgId} is a sandbox of ${possibleProdOrg.username}`);
356
416
  try {
@@ -383,9 +443,11 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
383
443
  catch (e) {
384
444
  logger.debug(`error writing sandbox auth file for: ${orgAuthInfo.getUsername()}`, e);
385
445
  }
446
+ return true;
386
447
  }
387
448
  catch (err) {
388
449
  logger.debug(`${fields.orgId} is not a sandbox of ${possibleProdOrg.username}`);
450
+ return false;
389
451
  }
390
452
  }
391
453
  /**
@@ -547,7 +609,10 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
547
609
  accessToken: (0, ts_types_1.ensureString)(authFields.accessToken),
548
610
  clientId: decryptedApp.clientId,
549
611
  clientSecret: decryptedApp.clientSecret,
550
- refreshToken: decryptedApp.refreshToken,
612
+ // Persist the server-returned refresh token (rotated under RTR), falling back to the one we
613
+ // sent when the response omits it (RTR off). Persisting the old token here would send an
614
+ // invalidated credential on the next refresh once RTR is enabled on this connected app.
615
+ refreshToken: (0, ts_types_1.ensureString)(authFields.refreshToken ?? decryptedApp.refreshToken),
551
616
  oauthFlow: 'web',
552
617
  },
553
618
  },
@@ -773,11 +838,23 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
773
838
  if (!options.privateKey && options.privateKeyFile) {
774
839
  options.privateKey = (0, node_path_1.resolve)(options.privateKeyFile);
775
840
  }
776
- if (options.privateKey) {
841
+ // Route to JWT only when there's a privateKey AND no refreshToken. A legitimate JWT auth never
842
+ // carries a refreshToken (authJwt writes none, and `login jwt` deletes-then-recreates the auth file).
843
+ // The edge case that contains BOTH a privateKey AND a refreshToken is when auth was originally JWT
844
+ // and later switched to Web auth. Web auth does not delete the existing auth file, so `AuthInfo.update`
845
+ // merges over the old file. In that case the refreshToken is the intended credential, so fall
846
+ // through to the refresh-token flow instead of misrouting into JWT, which also matches isJwt().
847
+ if (options.privateKey && !options.refreshToken) {
777
848
  authConfig = await this.authJwt(options);
778
849
  }
779
850
  else if (!options.authCode && options.refreshToken) {
780
- // refresh token flow (from sfdxUrl or OAuth refreshFn)
851
+ // refresh token flow (from sfdxUrl or OAuth refreshFn).
852
+ // RTR CAUTION: this POST rotates the token server-side and invalidates the old one immediately.
853
+ // Everything from here until the caller's save() (determineIfDevHub, orgs.read, update/encrypt,
854
+ // determineOrg) runs on borrowed time -- a throw in that window strands the rotated token on the
855
+ // wire while the old one is already dead server-side, permanently breaking the auth until re-login.
856
+ // determineIfDevHub and determineOrg swallow their own errors, so the practical window is small,
857
+ // but it is non-zero. A save-early seam for this branch would close it (tracked as a follow-up).
781
858
  authConfig = await this.buildRefreshTokenConfig(options);
782
859
  }
783
860
  else if (this.options.oauth2 instanceof jsforce_node_1.OAuth2) {
@@ -793,6 +870,15 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
793
870
  await this.stateAggregator.orgs.read(authConfig.username, false, false);
794
871
  // Update the auth fields WITH encryption
795
872
  this.update(authConfig);
873
+ // A web/auth-code or refresh-token authorization is never a JWT one, so it must not carry a
874
+ // privateKey. When this flow overwrites an existing auth file (e.g. the user was JWT-authed for
875
+ // this org, then re-authed via web), the save path merges (Object.assign) over the existing
876
+ // file and would otherwise retain the stale privateKey, which later misroutes refreshFn into
877
+ // the JWT flow. Only clear it when a stale value actually lingers so we don't add an empty key
878
+ // to a fresh authorization.
879
+ if (!authConfig.privateKey && this.getFields().privateKey) {
880
+ this.stateAggregator.orgs.update(this.username, { privateKey: undefined });
881
+ }
796
882
  // Populate Organization metadata (orgEdition, isScratch, isSandbox, etc.) in a single query.
797
883
  await (0, determineOrg_1.determineOrg)(this);
798
884
  }
@@ -822,10 +908,27 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
822
908
  this.logger.info('Access token has expired. Updating...');
823
909
  try {
824
910
  const fields = this.getFields(true);
825
- // This method will request the new access token and save to the current AuthInfo instance (but don't persist them!).
826
- await this.initAuthOptions(fields);
827
- // Persist fields with refreshed access token to auth file.
828
- await this.save();
911
+ // JWT auth mints a fresh access token from a locally-signed assertion and consumes no stored credential,
912
+ // so concurrent JWT refreshes are independent and safe. The refresh-token flow is different: every caller
913
+ // refreshes by re-sending the *same persisted refresh token* read from the auth file, and with Refresh
914
+ // Token Rotation enabled the server returns a new refresh token and invalidates the old one *immediately*.
915
+ // So two connections/processes refreshing at once would both send that on-disk token and double-rotate the
916
+ // auth. Serialize just that flow.
917
+ //
918
+ // Gate on `refreshToken` alone (not `refreshToken && !privateKey`): a legitimate JWT auth never carries
919
+ // a refresh token, so this still excludes JWT, and it also covers the fixed edge case where both-fields
920
+ // exist from an original JWT login and switching to web (stale privateKey + real refreshToken), which
921
+ // initAuthOptions routes through the refresh-token flow and which therefore must take the lock too.
922
+ // See the router in initAuthOptions and isJwt().
923
+ if (fields.refreshToken) {
924
+ await this.refreshWithTokenRotationLock(fields.refreshToken);
925
+ }
926
+ else {
927
+ // This method will request the new access token and save to the current AuthInfo instance (but don't persist them!).
928
+ await this.initAuthOptions(fields);
929
+ // Persist fields with refreshed access token to auth file.
930
+ await this.save();
931
+ }
829
932
  // Pass new access token to the jsforce's session-refresh callback for proper propagation:
830
933
  // https://jsforce.github.io/jsforce/types/session_refresh_delegate.SessionRefreshFunc.html
831
934
  const { accessToken } = this.getFields(true);
@@ -840,6 +943,140 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
840
943
  return callback(error);
841
944
  }
842
945
  }
946
+ /**
947
+ * Single-flight a refresh-token refresh so two connections/processes can't rotate the same token at once.
948
+ *
949
+ * With Refresh Token Rotation (RTR) enabled, each refresh returns a new refresh token and invalidates the
950
+ * previous one; a second refresh sent with the now-stale refresh token fails and invalidates the current token too.
951
+ * We take a cross-process lock (which also serializes same-process contenders), then re-read the latest
952
+ * tokens from disk: if another actor already rotated while we waited, we adopt their fresh credentials
953
+ * instead of refreshing again with our now-invalid token.
954
+ *
955
+ * Each pass is one call to `tryRotateOrAdopt`, which (1) adopts if disk already holds a rotated token, else
956
+ * (2) tries to acquire the lock and, once held, re-checks-then-refreshes. If we can't acquire (a live holder
957
+ * is mid-rotation, ELOCKED), we do NOT fall back to an unlocked refresh, which would race the holder and double-rotate
958
+ * under RTR; we simply loop back to (1) and re-run the pass (re-read disk, then try to acquire the lock again).
959
+ * `proper-lockfile` only grants the lock on genuine release or genuine staleness (a live
960
+ * holder refreshes its lock mtime ~every 5s and so is never stolen from), so looping converges: a slow holder is waited out,
961
+ * a dead holder's lock crosses the ~10s stale line and is stolen on a later attempt.
962
+ * Only a holder that keeps the lock alive for the whole budget below yields the timeout error.
963
+ *
964
+ * The lock uses a dedicated `<authfile>.token-rotation.lock`, kept separate from ConfigFile's
965
+ * own `<authfile>.lock` write lock, because proper-lockfile is not re-entrant: holding the auth-file lock
966
+ * here and then letting save() re-acquire the same path would self-block.
967
+ *
968
+ * KNOWN LIMITATIONS (all inherent, not bugs):
969
+ *
970
+ * (1) Only actors that take THIS lock are serialized. Within a single CLI release this is a non-issue: the
971
+ * CLI deduplicates @salesforce/core to one version, so every in-process refresher runs this same locking
972
+ * code (and the lock path is derived deterministically, so differing versions that BOTH lock still
973
+ * interoperate). The realistic gap is a 2nd/3rd-party plugin: those install under the CLI's plugin data dir
974
+ * with their OWN non-deduped node_modules and may bundle a core version predating this lock. Such a plugin
975
+ * (or any external tool that refreshes the token directly) won't honor `<authfile>.token-rotation.lock` and
976
+ * can still double-rotate the same auth while we hold it. There is no server-side coordination to prevent this.
977
+ *
978
+ * (2) On the web runtime (Global.isWeb), lockInit is a no-op that takes no lock (matching existing
979
+ * ConfigFile behavior), so nothing is serialized there.
980
+ *
981
+ * (3) A genuinely-stuck holder makes this block for up to the budget below plus one in-flight lock cycle
982
+ * (~40s) before throwing. Because that happens inside a live jsforce session-refresh, an upstream client
983
+ * with a shorter timeout may give up first with a less specific error. Only the pathological stuck case
984
+ * pays this; the common paths return in one lock cycle or via the lock-free adopt.
985
+ *
986
+ * @param heldRefreshToken the refresh token this connection currently holds (read from the auth file); the
987
+ * baseline we compare against disk to detect a rotation, and the token we would send if we do rotate.
988
+ */
989
+ async refreshWithTokenRotationLock(heldRefreshToken) {
990
+ const username = (0, ts_types_1.ensure)(this.getUsername());
991
+ // Cutoff for STARTING another attempt -- not a wall-clock cap on the whole method. Each attempt either
992
+ // finishes (adopt or refresh) or reports contention (ELOCKED); we re-attempt ONLY while contended. One
993
+ // lockInit acquisition cycle is ~10s (its lockRetryOptions retry budget before it throws ELOCKED) and the
994
+ // cutoff is consulted only between attempts, so a genuinely stuck holder pushes the actual time-to-throw
995
+ // to roughly this cutoff plus one in-flight cycle. Sizing it to ~35s (~3.5x a cycle) starts a 3rd attempt
996
+ // at ~20s with room to spare and leaves headroom for a 4th: enough for a dead holder's lock to cross the
997
+ // 10s stale line and be stolen on a later attempt, and for a slow-but-live holder to release.
998
+ const attemptDeadline = Date.now() + kit_1.Duration.seconds(35).milliseconds;
999
+ while (Date.now() < attemptDeadline) {
1000
+ // eslint-disable-next-line no-await-in-loop
1001
+ if (await this.tryRotateOrAdopt(username, heldRefreshToken)) {
1002
+ return;
1003
+ }
1004
+ // Otherwise the lock was contended (a live holder is mid-rotation): loop and re-attempt. We never
1005
+ // refresh unlocked, that would race the holder and double-rotate under RTR. No sleep needed: lockInit
1006
+ // already backed off ~10s, and the next attempt re-reads disk before doing anything.
1007
+ }
1008
+ // Past the cutoff with every attempt still contended: a process kept the rotation lock's mtime fresh for
1009
+ // the entire budget without completing. Genuinely stuck, not merely slow or crashed (a crash lets the
1010
+ // lock go stale and be stolen by an attempt above).
1011
+ throw messages.createError('refreshTokenRotationTimeoutError', [username]);
1012
+ }
1013
+ /**
1014
+ * One rotation attempt: adopt an already-rotated token if disk has one, else take the lock and
1015
+ * double-check-then-rotate under it.
1016
+ *
1017
+ * @returns `true` if we adopted or refreshed (caller is done); `false` if the lock was contended
1018
+ * (`ELOCKED`) and the caller should re-attempt. Throws on any non-`ELOCKED` failure.
1019
+ */
1020
+ async tryRotateOrAdopt(username, heldRefreshToken) {
1021
+ // Fast path / adopt: if another connection or process already rotated the token, adopt those fresh
1022
+ // credentials without taking the lock. On the first pass this thins the herd (late arrivals never
1023
+ // contend); on later passes it catches a holder that rotated and released while we were waiting.
1024
+ if (await this.adoptIfAlreadyRotated(username, heldRefreshToken)) {
1025
+ return true;
1026
+ }
1027
+ // proper-lockfile appends `.lock`, so this locks `<authfile>.token-rotation.lock`.
1028
+ const lockPath = `${this.stateAggregator.orgs.getPath(username)}.token-rotation`;
1029
+ let unlock;
1030
+ try {
1031
+ ({ unlock } = await (0, fileLocking_1.lockInit)(lockPath));
1032
+ }
1033
+ catch (err) {
1034
+ // Contended: a live holder is mid-rotation. Report it so the caller re-attempts (never refreshing
1035
+ // unlocked, which would double-rotate under RTR). Any non-ELOCKED error is a real failure.
1036
+ if (err?.code === 'ELOCKED') {
1037
+ return false;
1038
+ }
1039
+ throw err;
1040
+ }
1041
+ // We hold the lock. Run to completion regardless of the caller's cutoff -- we never abandon a rotation we
1042
+ // hold the lock for; the cutoff only gates whether a NEW attempt starts.
1043
+ try {
1044
+ // Double-check under the lock: the holder we queued behind may have rotated while we waited.
1045
+ if (await this.adoptIfAlreadyRotated(username, heldRefreshToken)) {
1046
+ return true;
1047
+ }
1048
+ // No one rotated: perform the refresh (updates this instance in memory) and persist the new token
1049
+ // before we release the lock, so everyone waiting behind us adopts it instead of re-rotating.
1050
+ await this.initAuthOptions(this.getFields(true));
1051
+ await this.save();
1052
+ return true;
1053
+ }
1054
+ finally {
1055
+ await unlock();
1056
+ }
1057
+ }
1058
+ /**
1059
+ * Re-read the on-disk auth (without disturbing this instance's in-memory fields) and, if another
1060
+ * connection/process has already rotated the refresh token, adopt those fresh credentials. Disk is
1061
+ * already current in that case, so no save is needed.
1062
+ *
1063
+ * NOTE: this gates on the refresh token only, not access-token freshness (we don't persist access-token
1064
+ * expiry). If the adopted access token has since expired, jsforce gets a 401 and re-enters refreshFn,
1065
+ * which then rotates under the lock (our refresh token now matches disk, so the double-check falls through
1066
+ * to a real refresh). That is one extra round trip in a narrow case, and still strictly better than the
1067
+ * pre-adopt behavior, where refreshing with our rotated-out token would have failed outright.
1068
+ *
1069
+ * @returns true if a rotated token was adopted; false if the on-disk token still matches ours.
1070
+ */
1071
+ async adoptIfAlreadyRotated(username, heldRefreshToken) {
1072
+ const onDisk = await this.stateAggregator.orgs.peek(username, true);
1073
+ if (onDisk?.refreshToken && onDisk.refreshToken !== heldRefreshToken) {
1074
+ this.logger.info('Refresh token was already rotated by another process; adopting refreshed credentials.');
1075
+ this.update(onDisk);
1076
+ return true;
1077
+ }
1078
+ return false;
1079
+ }
843
1080
  async readJwtKey(keyFile) {
844
1081
  return fs_1.fs.promises.readFile(keyFile, 'utf8');
845
1082
  }
@@ -948,7 +1185,11 @@ class AuthInfo extends kit_1.AsyncOptionalCreatable {
948
1185
  accessToken: authFieldsBuilder.access_token,
949
1186
  instanceUrl: authFieldsBuilder.instance_url,
950
1187
  loginUrl: fullOptions.loginUrl ?? authFieldsBuilder.instance_url,
951
- refreshToken: fullOptions.refreshToken,
1188
+ // Refresh Token Rotation (RTR): when the app has RTR enabled, the token endpoint returns a
1189
+ // NEW refresh_token that we must persist, replacing the one we sent. When RTR is off, the
1190
+ // response omits refresh_token, so we keep the existing one.
1191
+ // https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_refresh_token_flow.htm&type=5
1192
+ refreshToken: authFieldsBuilder.refresh_token ?? fullOptions.refreshToken,
952
1193
  clientId: fullOptions.clientId,
953
1194
  clientSecret: fullOptions.clientSecret,
954
1195
  };
@@ -18,6 +18,15 @@ export declare abstract class BaseOrgAccessor<T extends ConfigFile, P extends Co
18
18
  * @param throwOnNotFound throw if file is not found for username
19
19
  */
20
20
  read(username: string, decrypt?: boolean, throwOnNotFound?: boolean): Promise<Nullable<P>>;
21
+ /**
22
+ * Read the current on-disk contents of a username's auth file WITHOUT mutating the cache (unlike `read`,
23
+ * which replaces the cached config/contents). Use this to inspect what another process has persisted
24
+ * without disturbing this instance's in-memory state. Returns null if the file can't be read.
25
+ *
26
+ * @param username username to read
27
+ * @param decrypt if true, decrypt encrypted values
28
+ */
29
+ peek(username: string, decrypt?: boolean): Promise<Nullable<P>>;
21
30
  /**
22
31
  * Read all the auth files under the global state directory
23
32
  *
@@ -50,6 +59,13 @@ export declare abstract class BaseOrgAccessor<T extends ConfigFile, P extends Co
50
59
  * @param username
51
60
  */
52
61
  stat(username: string): Promise<Nullable<Awaited<ReturnType<typeof fs.promises.stat>>>>;
62
+ /**
63
+ * Return the absolute path to the auth file for a given username. Does not require the file to have been
64
+ * read (resolves purely from the username and the global state directory).
65
+ *
66
+ * @param username
67
+ */
68
+ getPath(username: string): string;
53
69
  /**
54
70
  * Returns true if there is an auth file for the given username
55
71
  *
@@ -92,6 +92,25 @@ class BaseOrgAccessor extends kit_1.AsyncOptionalCreatable {
92
92
  return null;
93
93
  }
94
94
  }
95
+ /**
96
+ * Read the current on-disk contents of a username's auth file WITHOUT mutating the cache (unlike `read`,
97
+ * which replaces the cached config/contents). Use this to inspect what another process has persisted
98
+ * without disturbing this instance's in-memory state. Returns null if the file can't be read.
99
+ *
100
+ * @param username username to read
101
+ * @param decrypt if true, decrypt encrypted values
102
+ */
103
+ async peek(username, decrypt = false) {
104
+ try {
105
+ const config = await this.initAuthFile(username, false);
106
+ return config.getContents(decrypt);
107
+ }
108
+ catch (err) {
109
+ const error = sfError_1.SfError.wrap(err);
110
+ this.logger.debug(`Error when peeking auth file for user: ${username} due to: ${error.name}:${error.message}`);
111
+ return null;
112
+ }
113
+ }
95
114
  /**
96
115
  * Read all the auth files under the global state directory
97
116
  *
@@ -134,7 +153,7 @@ class BaseOrgAccessor extends kit_1.AsyncOptionalCreatable {
134
153
  const config = this.configs.get(username);
135
154
  if (throwOnNotFound && config?.keys().length === 0) {
136
155
  ;
137
- 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."]]));
156
+ 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"], ["refreshTokenAuthError.actions", ["The stored refresh token is no longer valid (it may have expired, been revoked, or been rotated). Re-authenticate to the org, then retry. For example, run \"sf org login web --alias <your-alias>\""]], ["refreshTokenRotationTimeoutError", "Timed out waiting for another process to finish rotating the refresh token for %s. Another process held the rotation lock for the entire wait without completing. Retry the command. If it persists, another \"sf\" process is likely stuck mid-refresh: stop any other running \"sf\" processes, then retry."], ["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."]]));
138
157
  throw messages.createError('namedOrgNotFound', [username]);
139
158
  }
140
159
  if (config) {
@@ -177,6 +196,15 @@ class BaseOrgAccessor extends kit_1.AsyncOptionalCreatable {
177
196
  const config = this.configs.get(username);
178
197
  return config ? config.stat() : null;
179
198
  }
199
+ /**
200
+ * Return the absolute path to the auth file for a given username. Does not require the file to have been
201
+ * read (resolves purely from the username and the global state directory).
202
+ *
203
+ * @param username
204
+ */
205
+ getPath(username) {
206
+ return this.parseFilename(username);
207
+ }
180
208
  /**
181
209
  * Returns true if there is an auth file for the given username
182
210
  *
@@ -20,7 +20,7 @@ const node_assert_1 = require("node:assert");
20
20
  const ts_types_1 = require("@salesforce/ts-types");
21
21
  const messages_1 = require("../messages");
22
22
  ;
23
- const coreMessages = 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."]]));
23
+ const coreMessages = 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"], ["refreshTokenAuthError.actions", ["The stored refresh token is no longer valid (it may have expired, been revoked, or been rotated). Re-authenticate to the org, then retry. For example, run \"sf org login web --alias <your-alias>\""]], ["refreshTokenRotationTimeoutError", "Timed out waiting for another process to finish rotating the refresh token for %s. Another process held the rotation lock for the entire wait without completing. Retry the command. If it persists, another \"sf\" process is likely stuck mid-refresh: stop any other running \"sf\" processes, then retry."], ["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."]]));
24
24
  /** will throw on any upperCase unless they are present in the allowList. Recursively searches the object, returning valid keys */
25
25
  const ensureNoUppercaseKeys = (path) => (allowList = []) => (data) => {
26
26
  const keys = getKeys(data, allowList);
package/messages/core.md CHANGED
@@ -34,6 +34,14 @@ Errors encountered:
34
34
 
35
35
  Error authenticating with the refresh token due to: %s
36
36
 
37
+ # refreshTokenAuthError.actions
38
+
39
+ - The stored refresh token is no longer valid (it may have expired, been revoked, or been rotated). Re-authenticate to the org, then retry. For example, run "sf org login web --alias <your-alias>"
40
+
41
+ # refreshTokenRotationTimeoutError
42
+
43
+ Timed out waiting for another process to finish rotating the refresh token for %s. Another process held the rotation lock for the entire wait without completing. Retry the command. If it persists, another "sf" process is likely stuck mid-refresh: stop any other running "sf" processes, then retry.
44
+
37
45
  # invalidSfdxAuthUrlError
38
46
 
39
47
  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.
@@ -319,3 +319,7 @@ Your environment has both variables populated, and with different values. The va
319
319
  # sfCapitalizeRecordTypes
320
320
 
321
321
  Whether record types are capitalized on scratch org creation.
322
+
323
+ # sfSkipScratchOrgCheck
324
+
325
+ Set to "true" to skip the scratch org and sandbox identification check that runs after authenticating an org. When enabled, `sf org login jwt` (and other login commands) will not query existing cached orgs to determine whether the newly authenticated org is a scratch org or sandbox. This can significantly speed up authentication in CI/CD environments with many cached org authorizations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/core",
3
- "version": "9.1.10",
3
+ "version": "9.1.12-qa.0",
4
4
  "description": "Core libraries to interact with SFDX projects, orgs, and APIs.",
5
5
  "main": "lib/index",
6
6
  "types": "lib/index.d.ts",