@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.
- package/lib/config/envVars.d.ts +2 -1
- package/lib/config/envVars.js +6 -1
- package/lib/org/authInfo.d.ts +66 -0
- package/lib/org/authInfo.js +267 -26
- package/lib/stateAggregator/accessors/orgAccessor.d.ts +16 -0
- package/lib/stateAggregator/accessors/orgAccessor.js +29 -1
- package/lib/util/findUppercaseKeys.js +1 -1
- package/messages/core.md +8 -0
- package/messages/envVars.md +4 -0
- package/package.json +1 -1
package/lib/config/envVars.d.ts
CHANGED
|
@@ -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;
|
package/lib/config/envVars.js
CHANGED
|
@@ -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) {
|
package/lib/org/authInfo.d.ts
CHANGED
|
@@ -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;
|
package/lib/org/authInfo.js
CHANGED
|
@@ -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
|
|
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
|
|
201
|
+
isScratchOrg,
|
|
195
202
|
isDevHub: isDevHub ?? false,
|
|
196
|
-
|
|
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:
|
|
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(
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
826
|
-
|
|
827
|
-
//
|
|
828
|
-
|
|
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
|
-
|
|
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.
|
package/messages/envVars.md
CHANGED
|
@@ -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.
|