@salesforce/core 3.31.4 → 3.31.7

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.
Files changed (153) hide show
  1. package/LICENSE.txt +11 -11
  2. package/README.md +222 -222
  3. package/lib/config/aliasesConfig.d.ts +12 -12
  4. package/lib/config/aliasesConfig.js +27 -27
  5. package/lib/config/authInfoConfig.d.ts +19 -19
  6. package/lib/config/authInfoConfig.js +34 -34
  7. package/lib/config/config.d.ts +311 -311
  8. package/lib/config/config.js +574 -574
  9. package/lib/config/configAggregator.d.ts +232 -232
  10. package/lib/config/configAggregator.js +379 -379
  11. package/lib/config/configFile.d.ts +199 -199
  12. package/lib/config/configFile.js +340 -340
  13. package/lib/config/configGroup.d.ts +141 -141
  14. package/lib/config/configGroup.js +224 -224
  15. package/lib/config/configStore.d.ts +241 -241
  16. package/lib/config/configStore.js +352 -352
  17. package/lib/config/envVars.d.ts +101 -101
  18. package/lib/config/envVars.js +456 -456
  19. package/lib/config/orgUsersConfig.d.ts +31 -31
  20. package/lib/config/orgUsersConfig.js +41 -41
  21. package/lib/config/sandboxOrgConfig.d.ts +37 -37
  22. package/lib/config/sandboxOrgConfig.js +50 -50
  23. package/lib/config/sandboxProcessCache.d.ts +16 -16
  24. package/lib/config/sandboxProcessCache.js +37 -37
  25. package/lib/config/tokensConfig.d.ts +10 -10
  26. package/lib/config/tokensConfig.js +28 -28
  27. package/lib/config/ttlConfig.d.ts +34 -34
  28. package/lib/config/ttlConfig.js +54 -54
  29. package/lib/crypto/crypto.d.ts +54 -54
  30. package/lib/crypto/crypto.js +220 -220
  31. package/lib/crypto/keyChain.d.ts +8 -8
  32. package/lib/crypto/keyChain.js +61 -61
  33. package/lib/crypto/keyChainImpl.d.ts +116 -116
  34. package/lib/crypto/keyChainImpl.js +486 -486
  35. package/lib/crypto/secureBuffer.d.ts +46 -46
  36. package/lib/crypto/secureBuffer.js +82 -82
  37. package/lib/deviceOauthService.d.ts +71 -71
  38. package/lib/deviceOauthService.js +191 -191
  39. package/lib/exported.d.ts +38 -38
  40. package/lib/exported.js +118 -118
  41. package/lib/global.d.ts +70 -70
  42. package/lib/global.js +109 -109
  43. package/lib/lifecycleEvents.d.ts +93 -93
  44. package/lib/lifecycleEvents.js +188 -188
  45. package/lib/logger.d.ts +381 -381
  46. package/lib/logger.js +734 -734
  47. package/lib/messages.d.ts +291 -291
  48. package/lib/messages.js +543 -543
  49. package/lib/org/authInfo.d.ts +344 -344
  50. package/lib/org/authInfo.js +892 -892
  51. package/lib/org/authRemover.d.ts +88 -88
  52. package/lib/org/authRemover.js +182 -182
  53. package/lib/org/connection.d.ts +197 -197
  54. package/lib/org/connection.js +395 -395
  55. package/lib/org/index.d.ts +6 -6
  56. package/lib/org/index.js +28 -28
  57. package/lib/org/org.d.ts +558 -558
  58. package/lib/org/org.js +1267 -1267
  59. package/lib/org/orgConfigProperties.d.ts +69 -69
  60. package/lib/org/orgConfigProperties.js +136 -136
  61. package/lib/org/permissionSetAssignment.d.ts +35 -35
  62. package/lib/org/permissionSetAssignment.js +125 -125
  63. package/lib/org/scratchOrgCache.d.ts +20 -20
  64. package/lib/org/scratchOrgCache.js +32 -32
  65. package/lib/org/scratchOrgCreate.d.ts +54 -54
  66. package/lib/org/scratchOrgCreate.js +216 -216
  67. package/lib/org/scratchOrgErrorCodes.d.ts +10 -10
  68. package/lib/org/scratchOrgErrorCodes.js +88 -88
  69. package/lib/org/scratchOrgFeatureDeprecation.d.ts +26 -26
  70. package/lib/org/scratchOrgFeatureDeprecation.js +109 -109
  71. package/lib/org/scratchOrgInfoApi.d.ts +68 -68
  72. package/lib/org/scratchOrgInfoApi.js +413 -413
  73. package/lib/org/scratchOrgInfoGenerator.d.ts +64 -64
  74. package/lib/org/scratchOrgInfoGenerator.js +241 -241
  75. package/lib/org/scratchOrgLifecycleEvents.d.ts +10 -10
  76. package/lib/org/scratchOrgLifecycleEvents.js +40 -40
  77. package/lib/org/scratchOrgSettingsGenerator.d.ts +78 -78
  78. package/lib/org/scratchOrgSettingsGenerator.js +276 -276
  79. package/lib/org/scratchOrgTypes.d.ts +43 -43
  80. package/lib/org/scratchOrgTypes.js +8 -8
  81. package/lib/org/user.d.ts +187 -187
  82. package/lib/org/user.js +448 -448
  83. package/lib/schema/printer.d.ts +79 -79
  84. package/lib/schema/printer.js +260 -260
  85. package/lib/schema/validator.d.ts +70 -70
  86. package/lib/schema/validator.js +169 -169
  87. package/lib/sfError.d.ts +73 -73
  88. package/lib/sfError.js +136 -136
  89. package/lib/sfProject.d.ts +357 -357
  90. package/lib/sfProject.js +671 -671
  91. package/lib/stateAggregator/accessors/aliasAccessor.d.ts +98 -98
  92. package/lib/stateAggregator/accessors/aliasAccessor.js +145 -145
  93. package/lib/stateAggregator/accessors/orgAccessor.d.ts +101 -101
  94. package/lib/stateAggregator/accessors/orgAccessor.js +240 -240
  95. package/lib/stateAggregator/accessors/sandboxAccessor.d.ts +8 -8
  96. package/lib/stateAggregator/accessors/sandboxAccessor.js +27 -27
  97. package/lib/stateAggregator/accessors/tokenAccessor.d.ts +63 -63
  98. package/lib/stateAggregator/accessors/tokenAccessor.js +79 -79
  99. package/lib/stateAggregator/index.d.ts +4 -4
  100. package/lib/stateAggregator/index.js +26 -26
  101. package/lib/stateAggregator/stateAggregator.d.ts +25 -25
  102. package/lib/stateAggregator/stateAggregator.js +45 -45
  103. package/lib/status/myDomainResolver.d.ts +66 -66
  104. package/lib/status/myDomainResolver.js +124 -124
  105. package/lib/status/pollingClient.d.ts +85 -85
  106. package/lib/status/pollingClient.js +115 -115
  107. package/lib/status/streamingClient.d.ts +244 -244
  108. package/lib/status/streamingClient.js +436 -436
  109. package/lib/status/types.d.ts +89 -89
  110. package/lib/status/types.js +17 -17
  111. package/lib/testSetup.d.ts +553 -553
  112. package/lib/testSetup.js +871 -871
  113. package/lib/util/cache.d.ts +11 -11
  114. package/lib/util/cache.js +69 -69
  115. package/lib/util/checkLightningDomain.d.ts +1 -1
  116. package/lib/util/checkLightningDomain.js +28 -28
  117. package/lib/util/directoryWriter.d.ts +12 -12
  118. package/lib/util/directoryWriter.js +53 -53
  119. package/lib/util/getJwtAudienceUrl.d.ts +4 -4
  120. package/lib/util/getJwtAudienceUrl.js +18 -18
  121. package/lib/util/internal.d.ts +58 -58
  122. package/lib/util/internal.js +118 -118
  123. package/lib/util/jsonXmlTools.d.ts +14 -14
  124. package/lib/util/jsonXmlTools.js +38 -38
  125. package/lib/util/mapKeys.d.ts +14 -14
  126. package/lib/util/mapKeys.js +51 -51
  127. package/lib/util/sfdc.d.ts +52 -52
  128. package/lib/util/sfdc.js +85 -85
  129. package/lib/util/sfdcUrl.d.ts +72 -72
  130. package/lib/util/sfdcUrl.js +215 -215
  131. package/lib/util/structuredWriter.d.ts +9 -9
  132. package/lib/util/structuredWriter.js +2 -2
  133. package/lib/util/zipWriter.d.ts +16 -16
  134. package/lib/util/zipWriter.js +67 -67
  135. package/lib/webOAuthServer.d.ts +156 -156
  136. package/lib/webOAuthServer.js +388 -388
  137. package/messages/auth.md +37 -37
  138. package/messages/config.md +156 -156
  139. package/messages/connection.md +30 -30
  140. package/messages/core.json +20 -20
  141. package/messages/core.md +67 -67
  142. package/messages/encryption.md +85 -85
  143. package/messages/envVars.md +303 -303
  144. package/messages/org.md +63 -63
  145. package/messages/permissionSetAssignment.md +31 -31
  146. package/messages/scratchOrgCreate.md +23 -23
  147. package/messages/scratchOrgErrorCodes.md +115 -115
  148. package/messages/scratchOrgFeatureDeprecation.md +11 -11
  149. package/messages/scratchOrgInfoApi.md +15 -15
  150. package/messages/scratchOrgInfoGenerator.md +23 -23
  151. package/messages/streaming.md +23 -23
  152. package/messages/user.md +35 -35
  153. package/package.json +97 -97
@@ -1,893 +1,893 @@
1
- "use strict";
2
- /*
3
- * Copyright (c) 2020, salesforce.com, inc.
4
- * All rights reserved.
5
- * Licensed under the BSD 3-Clause license.
6
- * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7
- */
8
- /* eslint-disable class-methods-use-this */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.AuthInfo = exports.DEFAULT_CONNECTED_APP_INFO = void 0;
11
- const crypto_1 = require("crypto");
12
- const path_1 = require("path");
13
- const os = require("os");
14
- const fs = require("fs");
15
- const kit_1 = require("@salesforce/kit");
16
- const ts_types_1 = require("@salesforce/ts-types");
17
- const jsforce_1 = require("jsforce");
18
- const transport_1 = require("jsforce/lib/transport");
19
- const jwt = require("jsonwebtoken");
20
- const config_1 = require("../config/config");
21
- const configAggregator_1 = require("../config/configAggregator");
22
- const logger_1 = require("../logger");
23
- const sfError_1 = require("../sfError");
24
- const sfdc_1 = require("../util/sfdc");
25
- const stateAggregator_1 = require("../stateAggregator");
26
- const messages_1 = require("../messages");
27
- const sfdcUrl_1 = require("../util/sfdcUrl");
28
- const connection_1 = require("./connection");
29
- const orgConfigProperties_1 = require("./orgConfigProperties");
30
- const org_1 = require("./org");
31
- messages_1.Messages.importMessagesDirectory(__dirname);
32
- const messages = messages_1.Messages.load('@salesforce/core', 'core', [
33
- 'authInfoCreationError',
34
- 'authInfoOverwriteError',
35
- 'namedOrgNotFound',
36
- 'orgDataNotAvailableError',
37
- 'orgDataNotAvailableError.actions',
38
- 'refreshTokenAuthError',
39
- 'jwtAuthErrors',
40
- 'authCodeUsernameRetrievalError',
41
- 'authCodeExchangeError',
42
- 'missingClientId',
43
- ]);
44
- // parses the id field returned from jsForce oauth2 methods to get
45
- // user ID and org ID.
46
- function parseIdUrl(idUrl) {
47
- const idUrls = idUrl.split('/');
48
- const userId = idUrls.pop();
49
- const orgId = idUrls.pop();
50
- return {
51
- userId,
52
- orgId,
53
- url: idUrl,
54
- };
55
- }
56
- exports.DEFAULT_CONNECTED_APP_INFO = {
57
- clientId: 'PlatformCLI',
58
- // Legacy. The connected app info is owned by the thing that
59
- // creates new AuthInfos. Currently that is the auth:* commands which
60
- // aren't owned by this core library. These values need to be here
61
- // for any old auth files where the id and secret aren't stored.
62
- //
63
- // Ideally, this would be removed at some point in the distant future
64
- // when all auth files now have the clientId stored in it.
65
- legacyClientId: 'SalesforceDevelopmentExperience',
66
- legacyClientSecret: '1384510088588713504',
67
- };
68
- /**
69
- * Handles persistence and fetching of user authentication information using
70
- * JWT, OAuth, or refresh tokens. Sets up the refresh flows that jsForce will
71
- * use to keep tokens active. An AuthInfo can also be created with an access
72
- * token, but AuthInfos created with access tokens can't be persisted to disk.
73
- *
74
- * **See** [Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm)
75
- *
76
- * **See** [Salesforce DX Usernames and Orgs](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_usernames_orgs.htm)
77
- *
78
- * ```
79
- * // Creating a new authentication file.
80
- * const authInfo = await AuthInfo.create({
81
- * username: myAdminUsername,
82
- * oauth2Options: {
83
- * loginUrl, authCode, clientId, clientSecret
84
- * }
85
- * );
86
- * authInfo.save();
87
- *
88
- * // Creating an authorization info with an access token.
89
- * const authInfo = await AuthInfo.create({
90
- * username: accessToken
91
- * });
92
- *
93
- * // Using an existing authentication file.
94
- * const authInfo = await AuthInfo.create({
95
- * username: myAdminUsername
96
- * });
97
- *
98
- * // Using the AuthInfo
99
- * const connection = await Connection.create({ authInfo });
100
- * ```
101
- */
102
- class AuthInfo extends kit_1.AsyncOptionalCreatable {
103
- /**
104
- * Constructor
105
- * **Do not directly construct instances of this class -- use {@link AuthInfo.create} instead.**
106
- *
107
- * @param options The options for the class instance
108
- */
109
- constructor(options) {
110
- super(options);
111
- // Possibly overridden in create
112
- this.usingAccessToken = false;
113
- this.options = options ?? {};
114
- }
115
- /**
116
- * Returns the default instance url
117
- *
118
- * @returns {string}
119
- */
120
- static getDefaultInstanceUrl() {
121
- const configuredInstanceUrl = configAggregator_1.ConfigAggregator.getValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL).value;
122
- return configuredInstanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
123
- }
124
- /**
125
- * Get a list of all authorizations based on auth files stored in the global directory.
126
- * One can supply a filter (see @param orgAuthFilter) and calling this function without
127
- * a filter will return all authorizations.
128
- *
129
- * @param orgAuthFilter A predicate function that returns true for those org authorizations that are to be retained.
130
- *
131
- * @returns {Promise<OrgAuthorization[]>}
132
- */
133
- static async listAllAuthorizations(orgAuthFilter = (orgAuth) => !!orgAuth) {
134
- const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
135
- const config = (await configAggregator_1.ConfigAggregator.create()).getConfigInfo();
136
- const orgs = await stateAggregator.orgs.readAll();
137
- const final = [];
138
- for (const org of orgs) {
139
- const username = (0, ts_types_1.ensureString)(org.username);
140
- const aliases = stateAggregator.aliases.getAll(username) ?? undefined;
141
- // Get a list of configuration values that are set to either the username or one
142
- // of the aliases
143
- const configs = config
144
- .filter((c) => aliases.includes(c.value) || c.value === username)
145
- .map((c) => c.key);
146
- try {
147
- // prevent ConfigFile collision bug
148
- // eslint-disable-next-line no-await-in-loop
149
- const authInfo = await AuthInfo.create({ username });
150
- const { orgId, instanceUrl, devHubUsername, expirationDate, isDevHub } = authInfo.getFields();
151
- final.push({
152
- aliases,
153
- configs,
154
- username,
155
- instanceUrl,
156
- isScratchOrg: Boolean(devHubUsername),
157
- isDevHub: isDevHub ?? false,
158
- // eslint-disable-next-line no-await-in-loop
159
- isSandbox: await stateAggregator.sandboxes.hasFile(orgId),
160
- orgId: orgId,
161
- accessToken: authInfo.getConnectionOptions().accessToken,
162
- oauthMethod: authInfo.isJwt() ? 'jwt' : authInfo.isOauth() ? 'web' : 'token',
163
- isExpired: Boolean(devHubUsername) && expirationDate
164
- ? new Date((0, ts_types_1.ensureString)(expirationDate)).getTime() < new Date().getTime()
165
- : 'unknown',
166
- });
167
- }
168
- catch (err) {
169
- final.push({
170
- aliases,
171
- configs,
172
- username,
173
- orgId: org.orgId,
174
- instanceUrl: org.instanceUrl,
175
- accessToken: undefined,
176
- oauthMethod: 'unknown',
177
- error: err.message,
178
- isExpired: 'unknown',
179
- });
180
- }
181
- }
182
- return final.filter(orgAuthFilter);
183
- }
184
- /**
185
- * Returns true if one or more authentications are persisted.
186
- */
187
- static async hasAuthentications() {
188
- try {
189
- const auths = await (await stateAggregator_1.StateAggregator.getInstance()).orgs.list();
190
- return !(0, kit_1.isEmpty)(auths);
191
- }
192
- catch (err) {
193
- const error = err;
194
- if (error.name === 'OrgDataNotAvailableError' || error.code === 'ENOENT') {
195
- return false;
196
- }
197
- throw error;
198
- }
199
- }
200
- /**
201
- * Get the authorization URL.
202
- *
203
- * @param options The options to generate the URL.
204
- */
205
- static getAuthorizationUrl(options, oauth2) {
206
- // Always use a verifier for enhanced security
207
- options.useVerifier = true;
208
- const oauth2Verifier = oauth2 ?? new jsforce_1.OAuth2(options);
209
- // The state parameter allows the redirectUri callback listener to ignore request
210
- // that don't contain the state value.
211
- const params = {
212
- state: (0, crypto_1.randomBytes)(Math.ceil(6)).toString('hex'),
213
- prompt: 'login',
214
- // Default connected app is 'refresh_token api web'
215
- scope: options.scope ?? kit_1.env.getString('SFDX_AUTH_SCOPES', 'refresh_token api web'),
216
- };
217
- return oauth2Verifier.getAuthorizationUrl(params);
218
- }
219
- /**
220
- * Parse a sfdx auth url, usually obtained by `authInfo.getSfdxAuthUrl`.
221
- *
222
- * @example
223
- * ```
224
- * await AuthInfo.create(AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl));
225
- * ```
226
- * @param sfdxAuthUrl
227
- */
228
- static parseSfdxAuthUrl(sfdxAuthUrl) {
229
- const match = sfdxAuthUrl.match(/^force:\/\/([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)/);
230
- if (!match) {
231
- throw new sfError_1.SfError('Invalid SFDX auth URL. Must be in the format "force://<clientId>:<clientSecret>:<refreshToken>@<instanceUrl>". Note that the SFDX auth URL uses the "force" protocol, and not "http" or "https". Also note that the "instanceUrl" inside the SFDX auth URL doesn\'t include the protocol ("https://").', 'INVALID_SFDX_AUTH_URL');
232
- }
233
- const [, clientId, clientSecret, refreshToken, loginUrl] = match;
234
- return {
235
- clientId,
236
- clientSecret,
237
- refreshToken,
238
- loginUrl: `https://${loginUrl}`,
239
- };
240
- }
241
- /**
242
- * Given a set of decrypted fields and an authInfo, determine if the org belongs to an available
243
- * dev hub.
244
- *
245
- * @param fields
246
- * @param orgAuthInfo
247
- */
248
- static async identifyPossibleScratchOrgs(fields, orgAuthInfo) {
249
- // fields property is passed in because the consumers of this method have performed the decrypt.
250
- // This is so we don't have to call authInfo.getFields(true) and decrypt again OR accidentally save an
251
- // authInfo before it is necessary.
252
- const logger = await logger_1.Logger.child('Common', { tag: 'identifyPossibleScratchOrgs' });
253
- // return if we already know the hub org we know it is a devhub or prod-like or no orgId present
254
- if (fields.isDevHub || fields.devHubUsername || !fields.orgId)
255
- return;
256
- logger.debug('getting devHubs');
257
- // TODO: return if url is not sandbox-like to avoid constantly asking about production orgs
258
- // TODO: someday we make this easier by asking the org if it is a scratch org
259
- const hubAuthInfos = await AuthInfo.getDevHubAuthInfos();
260
- logger.debug(`found ${hubAuthInfos.length} DevHubs`);
261
- if (hubAuthInfos.length === 0)
262
- return;
263
- // ask all those orgs if they know this orgId
264
- await Promise.all(hubAuthInfos.map(async (hubAuthInfo) => {
265
- try {
266
- const data = await AuthInfo.queryScratchOrg(hubAuthInfo.username, fields.orgId);
267
- if (data.totalSize > 0) {
268
- // if any return a result
269
- logger.debug(`found orgId ${fields.orgId} in devhub ${hubAuthInfo.username}`);
270
- try {
271
- await orgAuthInfo.save({ ...fields, devHubUsername: hubAuthInfo.username });
272
- logger.debug(`set ${hubAuthInfo.username} as devhub for scratch org ${orgAuthInfo.getUsername()}`);
273
- }
274
- catch (error) {
275
- logger.debug(`error updating auth file for ${orgAuthInfo.getUsername()}`, error);
276
- }
277
- }
278
- }
279
- catch (error) {
280
- logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error);
281
- }
282
- }));
283
- }
284
- /**
285
- * Find all dev hubs available in the local environment.
286
- */
287
- static async getDevHubAuthInfos() {
288
- return AuthInfo.listAllAuthorizations((possibleHub) => possibleHub?.isDevHub ?? false);
289
- }
290
- static async queryScratchOrg(devHubUsername, scratchOrgId) {
291
- const devHubOrg = await org_1.Org.create({ aliasOrUsername: devHubUsername });
292
- const conn = devHubOrg.getConnection();
293
- const data = await conn.query(`select Id from ScratchOrgInfo where ScratchOrg = '${sfdc_1.sfdc.trimTo15(scratchOrgId)}'`);
294
- return data;
295
- }
296
- /**
297
- * Get the username.
298
- */
299
- getUsername() {
300
- return this.username;
301
- }
302
- /**
303
- * Returns true if `this` is using the JWT flow.
304
- */
305
- isJwt() {
306
- const { refreshToken, privateKey } = this.getFields();
307
- return !refreshToken && !!privateKey;
308
- }
309
- /**
310
- * Returns true if `this` is using an access token flow.
311
- */
312
- isAccessTokenFlow() {
313
- const { refreshToken, privateKey } = this.getFields();
314
- return !refreshToken && !privateKey;
315
- }
316
- /**
317
- * Returns true if `this` is using the oauth flow.
318
- */
319
- isOauth() {
320
- return !this.isAccessTokenFlow() && !this.isJwt();
321
- }
322
- /**
323
- * Returns true if `this` is using the refresh token flow.
324
- */
325
- isRefreshTokenFlow() {
326
- const { refreshToken, authCode } = this.getFields();
327
- return !authCode && !!refreshToken;
328
- }
329
- /**
330
- * Updates the cache and persists the authentication fields (encrypted).
331
- *
332
- * @param authData New data to save.
333
- */
334
- async save(authData) {
335
- this.update(authData);
336
- const username = (0, ts_types_1.ensure)(this.getUsername());
337
- if (sfdc_1.sfdc.matchesAccessToken(username)) {
338
- this.logger.debug('Username is an accesstoken. Skip saving authinfo to disk.');
339
- return this;
340
- }
341
- await this.stateAggregator.orgs.write(username);
342
- this.logger.info(`Saved auth info for username: ${username}`);
343
- return this;
344
- }
345
- /**
346
- * Update the authorization fields, encrypting sensitive fields, but do not persist.
347
- * For convenience `this` object is returned.
348
- *
349
- * @param authData Authorization fields to update.
350
- */
351
- update(authData) {
352
- if (authData && (0, ts_types_1.isPlainObject)(authData)) {
353
- this.username = authData.username ?? this.username;
354
- this.stateAggregator.orgs.update(this.username, authData);
355
- this.logger.info(`Updated auth info for username: ${this.username}`);
356
- }
357
- return this;
358
- }
359
- /**
360
- * Get the auth fields (decrypted) needed to make a connection.
361
- */
362
- getConnectionOptions() {
363
- let opts;
364
- const decryptedCopy = this.getFields(true);
365
- const { accessToken, instanceUrl, loginUrl } = decryptedCopy;
366
- if (this.isAccessTokenFlow()) {
367
- this.logger.info('Returning fields for a connection using access token.');
368
- // Just auth with the accessToken
369
- opts = { accessToken, instanceUrl, loginUrl };
370
- }
371
- else if (this.isJwt()) {
372
- this.logger.info('Returning fields for a connection using JWT config.');
373
- opts = {
374
- accessToken,
375
- instanceUrl,
376
- refreshFn: this.refreshFn.bind(this),
377
- };
378
- }
379
- else {
380
- // @TODO: figure out loginUrl and redirectUri (probably get from config class)
381
- //
382
- // redirectUri: org.config.getOauthCallbackUrl()
383
- // loginUrl: this.fields.instanceUrl || this.config.getAppConfig().sfdcLoginUrl
384
- this.logger.info('Returning fields for a connection using OAuth config.');
385
- // Decrypt a user provided client secret or use the default.
386
- opts = {
387
- oauth2: {
388
- loginUrl: instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION,
389
- clientId: this.getClientId(),
390
- redirectUri: this.getRedirectUri(),
391
- },
392
- accessToken,
393
- instanceUrl,
394
- refreshFn: this.refreshFn.bind(this),
395
- };
396
- }
397
- // decrypt the fields
398
- return opts;
399
- }
400
- getClientId() {
401
- return this.getFields()?.clientId ?? exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId;
402
- }
403
- getRedirectUri() {
404
- return 'http://localhost:1717/OauthRedirect';
405
- }
406
- /**
407
- * Get the authorization fields.
408
- *
409
- * @param decrypt Decrypt the fields.
410
- */
411
- getFields(decrypt) {
412
- return this.stateAggregator.orgs.get(this.username, decrypt) ?? {};
413
- }
414
- /**
415
- * Get the org front door (used for web based oauth flows)
416
- */
417
- getOrgFrontDoorUrl() {
418
- const authFields = this.getFields(true);
419
- const base = (0, ts_types_1.ensureString)(authFields.instanceUrl).replace(/\/+$/, '');
420
- const accessToken = (0, ts_types_1.ensureString)(authFields.accessToken);
421
- return `${base}/secur/frontdoor.jsp?sid=${accessToken}`;
422
- }
423
- /**
424
- * Returns true if this org is using access token auth.
425
- */
426
- isUsingAccessToken() {
427
- return this.usingAccessToken;
428
- }
429
- /**
430
- * Get the SFDX Auth URL.
431
- *
432
- * **See** [SFDX Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_auth.htm#cli_reference_force_auth)
433
- */
434
- getSfdxAuthUrl() {
435
- const decryptedFields = this.getFields(true);
436
- const instanceUrl = (0, ts_types_1.ensure)(decryptedFields.instanceUrl, 'undefined instanceUrl').replace(/^https?:\/\//, '');
437
- let sfdxAuthUrl = 'force://';
438
- if (decryptedFields.clientId) {
439
- sfdxAuthUrl += `${decryptedFields.clientId}:${decryptedFields.clientSecret ?? ''}:`;
440
- }
441
- sfdxAuthUrl += `${(0, ts_types_1.ensure)(decryptedFields.refreshToken, 'undefined refreshToken')}@${instanceUrl}`;
442
- return sfdxAuthUrl;
443
- }
444
- /**
445
- * Convenience function to handle typical side effects encountered when dealing with an AuthInfo.
446
- * Given the values supplied in parameter sideEffects, this function will set auth alias, default auth
447
- * and default dev hub.
448
- *
449
- * @param sideEffects - instance of AuthSideEffects
450
- */
451
- async handleAliasAndDefaultSettings(sideEffects) {
452
- if (sideEffects.alias ||
453
- sideEffects.setDefault ||
454
- sideEffects.setDefaultDevHub ||
455
- typeof sideEffects.setTracksSource === 'boolean') {
456
- if (sideEffects.alias)
457
- await this.setAlias(sideEffects.alias);
458
- if (sideEffects.setDefault)
459
- await this.setAsDefault({ org: true });
460
- if (sideEffects.setDefaultDevHub)
461
- await this.setAsDefault({ devHub: true });
462
- if (typeof sideEffects.setTracksSource === 'boolean') {
463
- await this.save({ tracksSource: sideEffects.setTracksSource });
464
- }
465
- else {
466
- await this.save();
467
- }
468
- }
469
- }
470
- /**
471
- * Set the target-env (default) or the target-dev-hub to the alias if
472
- * it exists otherwise to the username. Method will try to set the local
473
- * config first but will default to global config if that fails.
474
- *
475
- * @param options
476
- */
477
- async setAsDefault(options = { org: true }) {
478
- let config;
479
- // if we fail to create the local config, default to the global config
480
- try {
481
- config = await config_1.Config.create({ isGlobal: false });
482
- }
483
- catch {
484
- config = await config_1.Config.create({ isGlobal: true });
485
- }
486
- const username = (0, ts_types_1.ensureString)(this.getUsername());
487
- const alias = this.stateAggregator.aliases.get(username);
488
- const value = alias ?? username;
489
- if (options.org) {
490
- config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG, value);
491
- }
492
- if (options.devHub) {
493
- config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB, value);
494
- }
495
- await config.write();
496
- }
497
- /**
498
- * Sets the provided alias to the username
499
- *
500
- * @param alias alias to set
501
- */
502
- async setAlias(alias) {
503
- this.stateAggregator.aliases.set(alias, this.getUsername());
504
- await this.stateAggregator.aliases.write();
505
- }
506
- /**
507
- * Initializes an instance of the AuthInfo class.
508
- */
509
- async init() {
510
- this.stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
511
- const username = this.options.username;
512
- const authOptions = this.options.oauth2Options ?? this.options.accessTokenOptions;
513
- // Must specify either username and/or options
514
- if (!username && !authOptions) {
515
- throw messages.createError('authInfoCreationError');
516
- }
517
- // If a username AND oauth options, ensure an authorization for the username doesn't
518
- // already exist. Throw if it does so we don't overwrite the authorization.
519
- if (username && authOptions) {
520
- if (await this.stateAggregator.orgs.hasFile(username)) {
521
- throw messages.createError('authInfoOverwriteError');
522
- }
523
- }
524
- const oauthUsername = username ?? authOptions?.username;
525
- if (oauthUsername) {
526
- this.username = oauthUsername;
527
- await this.stateAggregator.orgs.read(oauthUsername, false, false);
528
- } // Else it will be set in initAuthOptions below.
529
- // If the username is an access token, use that for auth and don't persist
530
- if ((0, ts_types_1.isString)(oauthUsername) && sfdc_1.sfdc.matchesAccessToken(oauthUsername)) {
531
- // Need to initAuthOptions the logger and authInfoCrypto since we don't call init()
532
- this.logger = await logger_1.Logger.child('AuthInfo');
533
- const aggregator = await configAggregator_1.ConfigAggregator.create();
534
- const instanceUrl = this.getInstanceUrl(aggregator, authOptions);
535
- this.update({
536
- accessToken: oauthUsername,
537
- instanceUrl,
538
- orgId: oauthUsername.split('!')[0],
539
- loginUrl: instanceUrl,
540
- });
541
- this.usingAccessToken = true;
542
- }
543
- // If a username with NO oauth options, ensure authorization already exist.
544
- else if (username && !authOptions && !(await this.stateAggregator.orgs.exists(username))) {
545
- throw messages.createError('namedOrgNotFound', [username]);
546
- }
547
- else {
548
- await this.initAuthOptions(authOptions);
549
- }
550
- }
551
- getInstanceUrl(aggregator, options) {
552
- const instanceUrl = options?.instanceUrl ?? aggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL);
553
- return instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
554
- }
555
- /**
556
- * Initialize this AuthInfo instance with the specified options. If options are not provided, initialize it from cache
557
- * or by reading from the persistence store. For convenience `this` object is returned.
558
- *
559
- * @param options Options to be used for creating an OAuth2 instance.
560
- *
561
- * **Throws** *{@link SfError}{ name: 'NamedOrgNotFoundError' }* Org information does not exist.
562
- * @returns {Promise<AuthInfo>}
563
- */
564
- async initAuthOptions(options) {
565
- this.logger = await logger_1.Logger.child('AuthInfo');
566
- // If options were passed, use those before checking cache and reading an auth file.
567
- let authConfig;
568
- if (options) {
569
- options = (0, kit_1.cloneJson)(options);
570
- if (this.isTokenOptions(options)) {
571
- authConfig = options;
572
- const userInfo = await this.retrieveUserInfo((0, ts_types_1.ensureString)(options.instanceUrl), (0, ts_types_1.ensureString)(options.accessToken));
573
- this.update({ username: userInfo?.username, orgId: userInfo?.organizationId });
574
- }
575
- else {
576
- if (this.options.parentUsername) {
577
- const parentFields = await this.loadDecryptedAuthFromConfig(this.options.parentUsername);
578
- options.clientId = parentFields.clientId;
579
- if (process.env.SFDX_CLIENT_SECRET) {
580
- options.clientSecret = process.env.SFDX_CLIENT_SECRET;
581
- }
582
- else {
583
- // Grab whatever flow is defined
584
- Object.assign(options, {
585
- clientSecret: parentFields.clientSecret,
586
- privateKey: parentFields.privateKey ? (0, path_1.resolve)(parentFields.privateKey) : parentFields.privateKey,
587
- });
588
- }
589
- }
590
- // jwt flow
591
- // Support both sfdx and jsforce private key values
592
- if (!options.privateKey && options.privateKeyFile) {
593
- options.privateKey = (0, path_1.resolve)(options.privateKeyFile);
594
- }
595
- if (options.privateKey) {
596
- authConfig = await this.authJwt(options);
597
- }
598
- else if (!options.authCode && options.refreshToken) {
599
- // refresh token flow (from sfdxUrl or OAuth refreshFn)
600
- authConfig = await this.buildRefreshTokenConfig(options);
601
- }
602
- else if (this.options.oauth2 instanceof jsforce_1.OAuth2) {
603
- // authcode exchange / web auth flow
604
- authConfig = await this.exchangeToken(options, this.options.oauth2);
605
- }
606
- else {
607
- authConfig = await this.exchangeToken(options);
608
- }
609
- }
610
- authConfig.isDevHub = await this.determineIfDevHub((0, ts_types_1.ensureString)(authConfig.instanceUrl), (0, ts_types_1.ensureString)(authConfig.accessToken));
611
- if (authConfig.username)
612
- await this.stateAggregator.orgs.read(authConfig.username, false, false);
613
- // Update the auth fields WITH encryption
614
- this.update(authConfig);
615
- }
616
- return this;
617
- }
618
- // eslint-disable-next-line @typescript-eslint/require-await
619
- async loadDecryptedAuthFromConfig(username) {
620
- // Fetch from the persisted auth file
621
- const authInfo = this.stateAggregator.orgs.get(username, true);
622
- if (!authInfo) {
623
- throw messages.createError('namedOrgNotFound', [username]);
624
- }
625
- return authInfo;
626
- }
627
- isTokenOptions(options) {
628
- // Although OAuth2Config does not contain refreshToken, privateKey, or privateKeyFile, a JS consumer could still pass those in
629
- // which WILL have an access token as well, but it should be considered an OAuth2Config at that point.
630
- return ('accessToken' in options &&
631
- !('refreshToken' in options) &&
632
- !('privateKey' in options) &&
633
- !('privateKeyFile' in options) &&
634
- !('authCode' in options));
635
- }
636
- // A callback function for a connection to refresh an access token. This is used
637
- // both for a JWT connection and an OAuth connection.
638
- async refreshFn(conn, callback) {
639
- this.logger.info('Access token has expired. Updating...');
640
- try {
641
- const fields = this.getFields(true);
642
- await this.initAuthOptions(fields);
643
- await this.save();
644
- return await callback(null, fields.accessToken);
645
- }
646
- catch (err) {
647
- const error = err;
648
- if (error?.message?.includes('Data Not Available')) {
649
- // Set cause to keep original stacktrace
650
- return await callback(messages.createError('orgDataNotAvailableError', [this.getUsername()], [], error));
651
- }
652
- return await callback(error);
653
- }
654
- }
655
- async readJwtKey(keyFile) {
656
- return fs.promises.readFile(keyFile, 'utf8');
657
- }
658
- // Build OAuth config for a JWT auth flow
659
- async authJwt(options) {
660
- if (!options.clientId) {
661
- throw messages.createError('missingClientId');
662
- }
663
- const privateKeyContents = await this.readJwtKey((0, ts_types_1.ensureString)(options.privateKey));
664
- const { loginUrl = sfdcUrl_1.SfdcUrl.PRODUCTION } = options;
665
- const url = new sfdcUrl_1.SfdcUrl(loginUrl);
666
- const createdOrgInstance = (this.getFields().createdOrgInstance ?? '').trim().toLowerCase();
667
- const audienceUrl = await url.getJwtAudienceUrl(createdOrgInstance);
668
- let authFieldsBuilder;
669
- const authErrors = [];
670
- // given that we can no longer depend on instance names or URls to determine audience, let's try them all
671
- const loginAndAudienceUrls = (0, sfdcUrl_1.getLoginAudienceCombos)(audienceUrl, loginUrl);
672
- for (const [login, audience] of loginAndAudienceUrls) {
673
- try {
674
- // sequentially, in probabilistic order
675
- // eslint-disable-next-line no-await-in-loop
676
- authFieldsBuilder = await this.tryJwtAuth(options.clientId, login, audience, privateKeyContents);
677
- break;
678
- }
679
- catch (err) {
680
- const error = err;
681
- const message = error.message.includes('audience')
682
- ? `${error.message} [audience=${audience} login=${login}]`
683
- : error.message;
684
- authErrors.push(message);
685
- }
686
- }
687
- if (!authFieldsBuilder) {
688
- // messages.createError expects names to end in `error` and this one says Errors so do it manually.
689
- throw new sfError_1.SfError(messages.getMessage('jwtAuthErrors', [authErrors.join('\n')]), 'JwtAuthError');
690
- }
691
- const authFields = {
692
- accessToken: (0, ts_types_1.asString)(authFieldsBuilder.access_token),
693
- orgId: parseIdUrl((0, ts_types_1.ensureString)(authFieldsBuilder.id)).orgId,
694
- loginUrl: options.loginUrl,
695
- privateKey: options.privateKey,
696
- clientId: options.clientId,
697
- };
698
- const instanceUrl = (0, ts_types_1.ensureString)(authFieldsBuilder.instance_url);
699
- const sfdcUrl = new sfdcUrl_1.SfdcUrl(instanceUrl);
700
- try {
701
- // Check if the url is resolvable. This can fail when my-domains have not been replicated.
702
- await sfdcUrl.lookup();
703
- authFields.instanceUrl = instanceUrl;
704
- }
705
- catch (err) {
706
- this.logger.debug(
707
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
708
- `Instance URL [${authFieldsBuilder.instance_url}] is not available. DNS lookup failed. Using loginUrl [${options.loginUrl}] instead. This may result in a "Destination URL not reset" error.`);
709
- authFields.instanceUrl = options.loginUrl;
710
- }
711
- return authFields;
712
- }
713
- async tryJwtAuth(clientId, loginUrl, audienceUrl, privateKeyContents) {
714
- const jwtToken = jwt.sign({
715
- iss: clientId,
716
- sub: this.getUsername(),
717
- aud: audienceUrl,
718
- exp: Date.now() + 300,
719
- }, privateKeyContents, {
720
- algorithm: 'RS256',
721
- });
722
- const oauth2 = new jsforce_1.JwtOAuth2({ loginUrl });
723
- // jsforce has it types as any
724
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
725
- return (0, ts_types_1.ensureJsonMap)(await oauth2.jwtAuthorize(jwtToken));
726
- }
727
- // Build OAuth config for a refresh token auth flow
728
- async buildRefreshTokenConfig(options) {
729
- // Ideally, this would be removed at some point in the distant future when all auth files
730
- // now have the clientId stored in it.
731
- if (!options.clientId) {
732
- options.clientId = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId;
733
- options.clientSecret = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientSecret;
734
- }
735
- if (!options.redirectUri) {
736
- options.redirectUri = this.getRedirectUri();
737
- }
738
- const oauth2 = new jsforce_1.OAuth2(options);
739
- let authFieldsBuilder;
740
- try {
741
- authFieldsBuilder = await oauth2.refreshToken((0, ts_types_1.ensure)(options.refreshToken));
742
- }
743
- catch (err) {
744
- throw messages.createError('refreshTokenAuthError', [err.message]);
745
- }
746
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
747
- // @ts-ignore
748
- const { orgId } = parseIdUrl(authFieldsBuilder.id);
749
- let username = this.getUsername();
750
- if (!username) {
751
- const userInfo = await this.retrieveUserInfo(authFieldsBuilder.instance_url, authFieldsBuilder.access_token);
752
- username = (0, ts_types_1.ensureString)(userInfo?.username);
753
- }
754
- return {
755
- orgId,
756
- username,
757
- accessToken: authFieldsBuilder.access_token,
758
- instanceUrl: authFieldsBuilder.instance_url,
759
- loginUrl: options.loginUrl ?? authFieldsBuilder.instance_url,
760
- refreshToken: options.refreshToken,
761
- clientId: options.clientId,
762
- clientSecret: options.clientSecret,
763
- };
764
- }
765
- /**
766
- * Performs an authCode exchange but the Oauth2 feature of jsforce is extended to include a code_challenge
767
- *
768
- * @param options The oauth options
769
- * @param oauth2 The oauth2 extension that includes a code_challenge
770
- */
771
- async exchangeToken(options, oauth2 = new jsforce_1.OAuth2(options)) {
772
- if (!oauth2.redirectUri) {
773
- oauth2.redirectUri = this.getRedirectUri();
774
- }
775
- if (!oauth2.clientId) {
776
- oauth2.clientId = this.getClientId();
777
- }
778
- // Exchange the auth code for an access token and refresh token.
779
- let authFields;
780
- try {
781
- this.logger.info(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`);
782
- authFields = await oauth2.requestToken((0, ts_types_1.ensure)(options.authCode));
783
- }
784
- catch (err) {
785
- throw messages.createError('authCodeExchangeError', [err.message]);
786
- }
787
- const { orgId } = parseIdUrl(authFields.id);
788
- let username = this.getUsername();
789
- // Only need to query for the username if it isn't known. For example, a new auth code exchange
790
- // rather than refreshing a token on an existing connection.
791
- if (!username) {
792
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
793
- // @ts-ignore
794
- const userInfo = await this.retrieveUserInfo(authFields.instance_url, authFields.access_token);
795
- username = userInfo?.username;
796
- }
797
- return {
798
- accessToken: authFields.access_token,
799
- instanceUrl: authFields.instance_url,
800
- orgId,
801
- username,
802
- loginUrl: options.loginUrl ?? authFields.instance_url,
803
- refreshToken: authFields.refresh_token,
804
- clientId: options.clientId,
805
- clientSecret: options.clientSecret,
806
- };
807
- }
808
- async retrieveUserInfo(instanceUrl, accessToken) {
809
- // Make a REST call for the username directly. Normally this is done via a connection
810
- // but we don't want to create circular dependencies or lots of snowflakes
811
- // within this file to support it.
812
- const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
813
- const instance = (0, ts_types_1.ensure)(instanceUrl);
814
- const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
815
- const userInfoUrl = `${baseUrl.toString()}services/oauth2/userinfo`;
816
- const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
817
- try {
818
- this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${userInfoUrl}`);
819
- let response = await new transport_1.default().httpRequest({ url: userInfoUrl, method: 'GET', headers });
820
- if (response.statusCode >= 400) {
821
- this.throwUserGetException(response);
822
- }
823
- else {
824
- const userInfoJson = (0, kit_1.parseJsonMap)(response.body);
825
- const url = `${baseUrl.toString()}/services/data/${apiVersion}/sobjects/User/${userInfoJson.user_id}`;
826
- this.logger.info(`Sending request for User SObject after successful auth code exchange to URL: ${url}`);
827
- response = await new transport_1.default().httpRequest({ url, method: 'GET', headers });
828
- if (response.statusCode >= 400) {
829
- this.throwUserGetException(response);
830
- }
831
- else {
832
- // eslint-disable-next-line camelcase
833
- userInfoJson.preferred_username = (0, kit_1.parseJsonMap)(response.body).Username;
834
- }
835
- return { username: userInfoJson.preferred_username, organizationId: userInfoJson.organization_id };
836
- }
837
- }
838
- catch (err) {
839
- throw messages.createError('authCodeUsernameRetrievalError', [err.message]);
840
- }
841
- }
842
- /**
843
- * Given an error while getting the User object, handle different possibilities of response.body.
844
- *
845
- * @param response
846
- * @private
847
- */
848
- throwUserGetException(response) {
849
- let errorMsg = '';
850
- const bodyAsString = response.body ?? JSON.stringify({ message: 'UNKNOWN', errorCode: 'UNKNOWN' });
851
- try {
852
- const body = (0, kit_1.parseJson)(bodyAsString);
853
- if ((0, ts_types_1.isArray)(body)) {
854
- errorMsg = body.map((line) => line.message ?? line.errorCode ?? 'UNKNOWN').join(os.EOL);
855
- }
856
- else {
857
- errorMsg = body.message ?? body.errorCode ?? 'UNKNOWN';
858
- }
859
- }
860
- catch (err) {
861
- errorMsg = `${bodyAsString}`;
862
- }
863
- throw new sfError_1.SfError(errorMsg);
864
- }
865
- /**
866
- * Returns `true` if the org is a Dev Hub.
867
- *
868
- * Check access to the ScratchOrgInfo object to determine if the org is a dev hub.
869
- */
870
- async determineIfDevHub(instanceUrl, accessToken) {
871
- // Make a REST call for the ScratchOrgInfo obj directly. Normally this is done via a connection
872
- // but we don't want to create circular dependencies or lots of snowflakes
873
- // within this file to support it.
874
- const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
875
- const instance = (0, ts_types_1.ensure)(instanceUrl);
876
- const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
877
- const scratchOrgInfoUrl = `${baseUrl.toString()}/services/data/${apiVersion}/query?q=SELECT%20Id%20FROM%20ScratchOrgInfo%20limit%201`;
878
- const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
879
- try {
880
- const res = await new transport_1.default().httpRequest({ url: scratchOrgInfoUrl, method: 'GET', headers });
881
- if (res.statusCode >= 400) {
882
- return false;
883
- }
884
- return true;
885
- }
886
- catch (err) {
887
- /* Not a dev hub */
888
- return false;
889
- }
890
- }
891
- }
892
- exports.AuthInfo = AuthInfo;
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2020, salesforce.com, inc.
4
+ * All rights reserved.
5
+ * Licensed under the BSD 3-Clause license.
6
+ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7
+ */
8
+ /* eslint-disable class-methods-use-this */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.AuthInfo = exports.DEFAULT_CONNECTED_APP_INFO = void 0;
11
+ const crypto_1 = require("crypto");
12
+ const path_1 = require("path");
13
+ const os = require("os");
14
+ const fs = require("fs");
15
+ const kit_1 = require("@salesforce/kit");
16
+ const ts_types_1 = require("@salesforce/ts-types");
17
+ const jsforce_1 = require("jsforce");
18
+ const transport_1 = require("jsforce/lib/transport");
19
+ const jwt = require("jsonwebtoken");
20
+ const config_1 = require("../config/config");
21
+ const configAggregator_1 = require("../config/configAggregator");
22
+ const logger_1 = require("../logger");
23
+ const sfError_1 = require("../sfError");
24
+ const sfdc_1 = require("../util/sfdc");
25
+ const stateAggregator_1 = require("../stateAggregator");
26
+ const messages_1 = require("../messages");
27
+ const sfdcUrl_1 = require("../util/sfdcUrl");
28
+ const connection_1 = require("./connection");
29
+ const orgConfigProperties_1 = require("./orgConfigProperties");
30
+ const org_1 = require("./org");
31
+ messages_1.Messages.importMessagesDirectory(__dirname);
32
+ const messages = messages_1.Messages.load('@salesforce/core', 'core', [
33
+ 'authInfoCreationError',
34
+ 'authInfoOverwriteError',
35
+ 'namedOrgNotFound',
36
+ 'orgDataNotAvailableError',
37
+ 'orgDataNotAvailableError.actions',
38
+ 'refreshTokenAuthError',
39
+ 'jwtAuthErrors',
40
+ 'authCodeUsernameRetrievalError',
41
+ 'authCodeExchangeError',
42
+ 'missingClientId',
43
+ ]);
44
+ // parses the id field returned from jsForce oauth2 methods to get
45
+ // user ID and org ID.
46
+ function parseIdUrl(idUrl) {
47
+ const idUrls = idUrl.split('/');
48
+ const userId = idUrls.pop();
49
+ const orgId = idUrls.pop();
50
+ return {
51
+ userId,
52
+ orgId,
53
+ url: idUrl,
54
+ };
55
+ }
56
+ exports.DEFAULT_CONNECTED_APP_INFO = {
57
+ clientId: 'PlatformCLI',
58
+ // Legacy. The connected app info is owned by the thing that
59
+ // creates new AuthInfos. Currently that is the auth:* commands which
60
+ // aren't owned by this core library. These values need to be here
61
+ // for any old auth files where the id and secret aren't stored.
62
+ //
63
+ // Ideally, this would be removed at some point in the distant future
64
+ // when all auth files now have the clientId stored in it.
65
+ legacyClientId: 'SalesforceDevelopmentExperience',
66
+ legacyClientSecret: '1384510088588713504',
67
+ };
68
+ /**
69
+ * Handles persistence and fetching of user authentication information using
70
+ * JWT, OAuth, or refresh tokens. Sets up the refresh flows that jsForce will
71
+ * use to keep tokens active. An AuthInfo can also be created with an access
72
+ * token, but AuthInfos created with access tokens can't be persisted to disk.
73
+ *
74
+ * **See** [Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm)
75
+ *
76
+ * **See** [Salesforce DX Usernames and Orgs](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_usernames_orgs.htm)
77
+ *
78
+ * ```
79
+ * // Creating a new authentication file.
80
+ * const authInfo = await AuthInfo.create({
81
+ * username: myAdminUsername,
82
+ * oauth2Options: {
83
+ * loginUrl, authCode, clientId, clientSecret
84
+ * }
85
+ * );
86
+ * authInfo.save();
87
+ *
88
+ * // Creating an authorization info with an access token.
89
+ * const authInfo = await AuthInfo.create({
90
+ * username: accessToken
91
+ * });
92
+ *
93
+ * // Using an existing authentication file.
94
+ * const authInfo = await AuthInfo.create({
95
+ * username: myAdminUsername
96
+ * });
97
+ *
98
+ * // Using the AuthInfo
99
+ * const connection = await Connection.create({ authInfo });
100
+ * ```
101
+ */
102
+ class AuthInfo extends kit_1.AsyncOptionalCreatable {
103
+ /**
104
+ * Constructor
105
+ * **Do not directly construct instances of this class -- use {@link AuthInfo.create} instead.**
106
+ *
107
+ * @param options The options for the class instance
108
+ */
109
+ constructor(options) {
110
+ super(options);
111
+ // Possibly overridden in create
112
+ this.usingAccessToken = false;
113
+ this.options = options ?? {};
114
+ }
115
+ /**
116
+ * Returns the default instance url
117
+ *
118
+ * @returns {string}
119
+ */
120
+ static getDefaultInstanceUrl() {
121
+ const configuredInstanceUrl = configAggregator_1.ConfigAggregator.getValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL).value;
122
+ return configuredInstanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
123
+ }
124
+ /**
125
+ * Get a list of all authorizations based on auth files stored in the global directory.
126
+ * One can supply a filter (see @param orgAuthFilter) and calling this function without
127
+ * a filter will return all authorizations.
128
+ *
129
+ * @param orgAuthFilter A predicate function that returns true for those org authorizations that are to be retained.
130
+ *
131
+ * @returns {Promise<OrgAuthorization[]>}
132
+ */
133
+ static async listAllAuthorizations(orgAuthFilter = (orgAuth) => !!orgAuth) {
134
+ const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
135
+ const config = (await configAggregator_1.ConfigAggregator.create()).getConfigInfo();
136
+ const orgs = await stateAggregator.orgs.readAll();
137
+ const final = [];
138
+ for (const org of orgs) {
139
+ const username = (0, ts_types_1.ensureString)(org.username);
140
+ const aliases = stateAggregator.aliases.getAll(username) ?? undefined;
141
+ // Get a list of configuration values that are set to either the username or one
142
+ // of the aliases
143
+ const configs = config
144
+ .filter((c) => aliases.includes(c.value) || c.value === username)
145
+ .map((c) => c.key);
146
+ try {
147
+ // prevent ConfigFile collision bug
148
+ // eslint-disable-next-line no-await-in-loop
149
+ const authInfo = await AuthInfo.create({ username });
150
+ const { orgId, instanceUrl, devHubUsername, expirationDate, isDevHub } = authInfo.getFields();
151
+ final.push({
152
+ aliases,
153
+ configs,
154
+ username,
155
+ instanceUrl,
156
+ isScratchOrg: Boolean(devHubUsername),
157
+ isDevHub: isDevHub ?? false,
158
+ // eslint-disable-next-line no-await-in-loop
159
+ isSandbox: await stateAggregator.sandboxes.hasFile(orgId),
160
+ orgId: orgId,
161
+ accessToken: authInfo.getConnectionOptions().accessToken,
162
+ oauthMethod: authInfo.isJwt() ? 'jwt' : authInfo.isOauth() ? 'web' : 'token',
163
+ isExpired: Boolean(devHubUsername) && expirationDate
164
+ ? new Date((0, ts_types_1.ensureString)(expirationDate)).getTime() < new Date().getTime()
165
+ : 'unknown',
166
+ });
167
+ }
168
+ catch (err) {
169
+ final.push({
170
+ aliases,
171
+ configs,
172
+ username,
173
+ orgId: org.orgId,
174
+ instanceUrl: org.instanceUrl,
175
+ accessToken: undefined,
176
+ oauthMethod: 'unknown',
177
+ error: err.message,
178
+ isExpired: 'unknown',
179
+ });
180
+ }
181
+ }
182
+ return final.filter(orgAuthFilter);
183
+ }
184
+ /**
185
+ * Returns true if one or more authentications are persisted.
186
+ */
187
+ static async hasAuthentications() {
188
+ try {
189
+ const auths = await (await stateAggregator_1.StateAggregator.getInstance()).orgs.list();
190
+ return !(0, kit_1.isEmpty)(auths);
191
+ }
192
+ catch (err) {
193
+ const error = err;
194
+ if (error.name === 'OrgDataNotAvailableError' || error.code === 'ENOENT') {
195
+ return false;
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+ /**
201
+ * Get the authorization URL.
202
+ *
203
+ * @param options The options to generate the URL.
204
+ */
205
+ static getAuthorizationUrl(options, oauth2) {
206
+ // Always use a verifier for enhanced security
207
+ options.useVerifier = true;
208
+ const oauth2Verifier = oauth2 ?? new jsforce_1.OAuth2(options);
209
+ // The state parameter allows the redirectUri callback listener to ignore request
210
+ // that don't contain the state value.
211
+ const params = {
212
+ state: (0, crypto_1.randomBytes)(Math.ceil(6)).toString('hex'),
213
+ prompt: 'login',
214
+ // Default connected app is 'refresh_token api web'
215
+ scope: options.scope ?? kit_1.env.getString('SFDX_AUTH_SCOPES', 'refresh_token api web'),
216
+ };
217
+ return oauth2Verifier.getAuthorizationUrl(params);
218
+ }
219
+ /**
220
+ * Parse a sfdx auth url, usually obtained by `authInfo.getSfdxAuthUrl`.
221
+ *
222
+ * @example
223
+ * ```
224
+ * await AuthInfo.create(AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl));
225
+ * ```
226
+ * @param sfdxAuthUrl
227
+ */
228
+ static parseSfdxAuthUrl(sfdxAuthUrl) {
229
+ const match = sfdxAuthUrl.match(/^force:\/\/([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)/);
230
+ if (!match) {
231
+ throw new sfError_1.SfError('Invalid SFDX auth URL. Must be in the format "force://<clientId>:<clientSecret>:<refreshToken>@<instanceUrl>". Note that the SFDX auth URL uses the "force" protocol, and not "http" or "https". Also note that the "instanceUrl" inside the SFDX auth URL doesn\'t include the protocol ("https://").', 'INVALID_SFDX_AUTH_URL');
232
+ }
233
+ const [, clientId, clientSecret, refreshToken, loginUrl] = match;
234
+ return {
235
+ clientId,
236
+ clientSecret,
237
+ refreshToken,
238
+ loginUrl: `https://${loginUrl}`,
239
+ };
240
+ }
241
+ /**
242
+ * Given a set of decrypted fields and an authInfo, determine if the org belongs to an available
243
+ * dev hub.
244
+ *
245
+ * @param fields
246
+ * @param orgAuthInfo
247
+ */
248
+ static async identifyPossibleScratchOrgs(fields, orgAuthInfo) {
249
+ // fields property is passed in because the consumers of this method have performed the decrypt.
250
+ // This is so we don't have to call authInfo.getFields(true) and decrypt again OR accidentally save an
251
+ // authInfo before it is necessary.
252
+ const logger = await logger_1.Logger.child('Common', { tag: 'identifyPossibleScratchOrgs' });
253
+ // return if we already know the hub org we know it is a devhub or prod-like or no orgId present
254
+ if (fields.isDevHub || fields.devHubUsername || !fields.orgId)
255
+ return;
256
+ logger.debug('getting devHubs');
257
+ // TODO: return if url is not sandbox-like to avoid constantly asking about production orgs
258
+ // TODO: someday we make this easier by asking the org if it is a scratch org
259
+ const hubAuthInfos = await AuthInfo.getDevHubAuthInfos();
260
+ logger.debug(`found ${hubAuthInfos.length} DevHubs`);
261
+ if (hubAuthInfos.length === 0)
262
+ return;
263
+ // ask all those orgs if they know this orgId
264
+ await Promise.all(hubAuthInfos.map(async (hubAuthInfo) => {
265
+ try {
266
+ const data = await AuthInfo.queryScratchOrg(hubAuthInfo.username, fields.orgId);
267
+ if (data.totalSize > 0) {
268
+ // if any return a result
269
+ logger.debug(`found orgId ${fields.orgId} in devhub ${hubAuthInfo.username}`);
270
+ try {
271
+ await orgAuthInfo.save({ ...fields, devHubUsername: hubAuthInfo.username });
272
+ logger.debug(`set ${hubAuthInfo.username} as devhub for scratch org ${orgAuthInfo.getUsername()}`);
273
+ }
274
+ catch (error) {
275
+ logger.debug(`error updating auth file for ${orgAuthInfo.getUsername()}`, error);
276
+ }
277
+ }
278
+ }
279
+ catch (error) {
280
+ logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error);
281
+ }
282
+ }));
283
+ }
284
+ /**
285
+ * Find all dev hubs available in the local environment.
286
+ */
287
+ static async getDevHubAuthInfos() {
288
+ return AuthInfo.listAllAuthorizations((possibleHub) => possibleHub?.isDevHub ?? false);
289
+ }
290
+ static async queryScratchOrg(devHubUsername, scratchOrgId) {
291
+ const devHubOrg = await org_1.Org.create({ aliasOrUsername: devHubUsername });
292
+ const conn = devHubOrg.getConnection();
293
+ const data = await conn.query(`select Id from ScratchOrgInfo where ScratchOrg = '${sfdc_1.sfdc.trimTo15(scratchOrgId)}'`);
294
+ return data;
295
+ }
296
+ /**
297
+ * Get the username.
298
+ */
299
+ getUsername() {
300
+ return this.username;
301
+ }
302
+ /**
303
+ * Returns true if `this` is using the JWT flow.
304
+ */
305
+ isJwt() {
306
+ const { refreshToken, privateKey } = this.getFields();
307
+ return !refreshToken && !!privateKey;
308
+ }
309
+ /**
310
+ * Returns true if `this` is using an access token flow.
311
+ */
312
+ isAccessTokenFlow() {
313
+ const { refreshToken, privateKey } = this.getFields();
314
+ return !refreshToken && !privateKey;
315
+ }
316
+ /**
317
+ * Returns true if `this` is using the oauth flow.
318
+ */
319
+ isOauth() {
320
+ return !this.isAccessTokenFlow() && !this.isJwt();
321
+ }
322
+ /**
323
+ * Returns true if `this` is using the refresh token flow.
324
+ */
325
+ isRefreshTokenFlow() {
326
+ const { refreshToken, authCode } = this.getFields();
327
+ return !authCode && !!refreshToken;
328
+ }
329
+ /**
330
+ * Updates the cache and persists the authentication fields (encrypted).
331
+ *
332
+ * @param authData New data to save.
333
+ */
334
+ async save(authData) {
335
+ this.update(authData);
336
+ const username = (0, ts_types_1.ensure)(this.getUsername());
337
+ if (sfdc_1.sfdc.matchesAccessToken(username)) {
338
+ this.logger.debug('Username is an accesstoken. Skip saving authinfo to disk.');
339
+ return this;
340
+ }
341
+ await this.stateAggregator.orgs.write(username);
342
+ this.logger.info(`Saved auth info for username: ${username}`);
343
+ return this;
344
+ }
345
+ /**
346
+ * Update the authorization fields, encrypting sensitive fields, but do not persist.
347
+ * For convenience `this` object is returned.
348
+ *
349
+ * @param authData Authorization fields to update.
350
+ */
351
+ update(authData) {
352
+ if (authData && (0, ts_types_1.isPlainObject)(authData)) {
353
+ this.username = authData.username ?? this.username;
354
+ this.stateAggregator.orgs.update(this.username, authData);
355
+ this.logger.info(`Updated auth info for username: ${this.username}`);
356
+ }
357
+ return this;
358
+ }
359
+ /**
360
+ * Get the auth fields (decrypted) needed to make a connection.
361
+ */
362
+ getConnectionOptions() {
363
+ let opts;
364
+ const decryptedCopy = this.getFields(true);
365
+ const { accessToken, instanceUrl, loginUrl } = decryptedCopy;
366
+ if (this.isAccessTokenFlow()) {
367
+ this.logger.info('Returning fields for a connection using access token.');
368
+ // Just auth with the accessToken
369
+ opts = { accessToken, instanceUrl, loginUrl };
370
+ }
371
+ else if (this.isJwt()) {
372
+ this.logger.info('Returning fields for a connection using JWT config.');
373
+ opts = {
374
+ accessToken,
375
+ instanceUrl,
376
+ refreshFn: this.refreshFn.bind(this),
377
+ };
378
+ }
379
+ else {
380
+ // @TODO: figure out loginUrl and redirectUri (probably get from config class)
381
+ //
382
+ // redirectUri: org.config.getOauthCallbackUrl()
383
+ // loginUrl: this.fields.instanceUrl || this.config.getAppConfig().sfdcLoginUrl
384
+ this.logger.info('Returning fields for a connection using OAuth config.');
385
+ // Decrypt a user provided client secret or use the default.
386
+ opts = {
387
+ oauth2: {
388
+ loginUrl: instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION,
389
+ clientId: this.getClientId(),
390
+ redirectUri: this.getRedirectUri(),
391
+ },
392
+ accessToken,
393
+ instanceUrl,
394
+ refreshFn: this.refreshFn.bind(this),
395
+ };
396
+ }
397
+ // decrypt the fields
398
+ return opts;
399
+ }
400
+ getClientId() {
401
+ return this.getFields()?.clientId ?? exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId;
402
+ }
403
+ getRedirectUri() {
404
+ return 'http://localhost:1717/OauthRedirect';
405
+ }
406
+ /**
407
+ * Get the authorization fields.
408
+ *
409
+ * @param decrypt Decrypt the fields.
410
+ */
411
+ getFields(decrypt) {
412
+ return this.stateAggregator.orgs.get(this.username, decrypt) ?? {};
413
+ }
414
+ /**
415
+ * Get the org front door (used for web based oauth flows)
416
+ */
417
+ getOrgFrontDoorUrl() {
418
+ const authFields = this.getFields(true);
419
+ const base = (0, ts_types_1.ensureString)(authFields.instanceUrl).replace(/\/+$/, '');
420
+ const accessToken = (0, ts_types_1.ensureString)(authFields.accessToken);
421
+ return `${base}/secur/frontdoor.jsp?sid=${accessToken}`;
422
+ }
423
+ /**
424
+ * Returns true if this org is using access token auth.
425
+ */
426
+ isUsingAccessToken() {
427
+ return this.usingAccessToken;
428
+ }
429
+ /**
430
+ * Get the SFDX Auth URL.
431
+ *
432
+ * **See** [SFDX Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_auth.htm#cli_reference_force_auth)
433
+ */
434
+ getSfdxAuthUrl() {
435
+ const decryptedFields = this.getFields(true);
436
+ const instanceUrl = (0, ts_types_1.ensure)(decryptedFields.instanceUrl, 'undefined instanceUrl').replace(/^https?:\/\//, '');
437
+ let sfdxAuthUrl = 'force://';
438
+ if (decryptedFields.clientId) {
439
+ sfdxAuthUrl += `${decryptedFields.clientId}:${decryptedFields.clientSecret ?? ''}:`;
440
+ }
441
+ sfdxAuthUrl += `${(0, ts_types_1.ensure)(decryptedFields.refreshToken, 'undefined refreshToken')}@${instanceUrl}`;
442
+ return sfdxAuthUrl;
443
+ }
444
+ /**
445
+ * Convenience function to handle typical side effects encountered when dealing with an AuthInfo.
446
+ * Given the values supplied in parameter sideEffects, this function will set auth alias, default auth
447
+ * and default dev hub.
448
+ *
449
+ * @param sideEffects - instance of AuthSideEffects
450
+ */
451
+ async handleAliasAndDefaultSettings(sideEffects) {
452
+ if (sideEffects.alias ||
453
+ sideEffects.setDefault ||
454
+ sideEffects.setDefaultDevHub ||
455
+ typeof sideEffects.setTracksSource === 'boolean') {
456
+ if (sideEffects.alias)
457
+ await this.setAlias(sideEffects.alias);
458
+ if (sideEffects.setDefault)
459
+ await this.setAsDefault({ org: true });
460
+ if (sideEffects.setDefaultDevHub)
461
+ await this.setAsDefault({ devHub: true });
462
+ if (typeof sideEffects.setTracksSource === 'boolean') {
463
+ await this.save({ tracksSource: sideEffects.setTracksSource });
464
+ }
465
+ else {
466
+ await this.save();
467
+ }
468
+ }
469
+ }
470
+ /**
471
+ * Set the target-env (default) or the target-dev-hub to the alias if
472
+ * it exists otherwise to the username. Method will try to set the local
473
+ * config first but will default to global config if that fails.
474
+ *
475
+ * @param options
476
+ */
477
+ async setAsDefault(options = { org: true }) {
478
+ let config;
479
+ // if we fail to create the local config, default to the global config
480
+ try {
481
+ config = await config_1.Config.create({ isGlobal: false });
482
+ }
483
+ catch {
484
+ config = await config_1.Config.create({ isGlobal: true });
485
+ }
486
+ const username = (0, ts_types_1.ensureString)(this.getUsername());
487
+ const alias = this.stateAggregator.aliases.get(username);
488
+ const value = alias ?? username;
489
+ if (options.org) {
490
+ config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG, value);
491
+ }
492
+ if (options.devHub) {
493
+ config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB, value);
494
+ }
495
+ await config.write();
496
+ }
497
+ /**
498
+ * Sets the provided alias to the username
499
+ *
500
+ * @param alias alias to set
501
+ */
502
+ async setAlias(alias) {
503
+ this.stateAggregator.aliases.set(alias, this.getUsername());
504
+ await this.stateAggregator.aliases.write();
505
+ }
506
+ /**
507
+ * Initializes an instance of the AuthInfo class.
508
+ */
509
+ async init() {
510
+ this.stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
511
+ const username = this.options.username;
512
+ const authOptions = this.options.oauth2Options ?? this.options.accessTokenOptions;
513
+ // Must specify either username and/or options
514
+ if (!username && !authOptions) {
515
+ throw messages.createError('authInfoCreationError');
516
+ }
517
+ // If a username AND oauth options, ensure an authorization for the username doesn't
518
+ // already exist. Throw if it does so we don't overwrite the authorization.
519
+ if (username && authOptions) {
520
+ if (await this.stateAggregator.orgs.hasFile(username)) {
521
+ throw messages.createError('authInfoOverwriteError');
522
+ }
523
+ }
524
+ const oauthUsername = username ?? authOptions?.username;
525
+ if (oauthUsername) {
526
+ this.username = oauthUsername;
527
+ await this.stateAggregator.orgs.read(oauthUsername, false, false);
528
+ } // Else it will be set in initAuthOptions below.
529
+ // If the username is an access token, use that for auth and don't persist
530
+ if ((0, ts_types_1.isString)(oauthUsername) && sfdc_1.sfdc.matchesAccessToken(oauthUsername)) {
531
+ // Need to initAuthOptions the logger and authInfoCrypto since we don't call init()
532
+ this.logger = await logger_1.Logger.child('AuthInfo');
533
+ const aggregator = await configAggregator_1.ConfigAggregator.create();
534
+ const instanceUrl = this.getInstanceUrl(aggregator, authOptions);
535
+ this.update({
536
+ accessToken: oauthUsername,
537
+ instanceUrl,
538
+ orgId: oauthUsername.split('!')[0],
539
+ loginUrl: instanceUrl,
540
+ });
541
+ this.usingAccessToken = true;
542
+ }
543
+ // If a username with NO oauth options, ensure authorization already exist.
544
+ else if (username && !authOptions && !(await this.stateAggregator.orgs.exists(username))) {
545
+ throw messages.createError('namedOrgNotFound', [username]);
546
+ }
547
+ else {
548
+ await this.initAuthOptions(authOptions);
549
+ }
550
+ }
551
+ getInstanceUrl(aggregator, options) {
552
+ const instanceUrl = options?.instanceUrl ?? aggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL);
553
+ return instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
554
+ }
555
+ /**
556
+ * Initialize this AuthInfo instance with the specified options. If options are not provided, initialize it from cache
557
+ * or by reading from the persistence store. For convenience `this` object is returned.
558
+ *
559
+ * @param options Options to be used for creating an OAuth2 instance.
560
+ *
561
+ * **Throws** *{@link SfError}{ name: 'NamedOrgNotFoundError' }* Org information does not exist.
562
+ * @returns {Promise<AuthInfo>}
563
+ */
564
+ async initAuthOptions(options) {
565
+ this.logger = await logger_1.Logger.child('AuthInfo');
566
+ // If options were passed, use those before checking cache and reading an auth file.
567
+ let authConfig;
568
+ if (options) {
569
+ options = (0, kit_1.cloneJson)(options);
570
+ if (this.isTokenOptions(options)) {
571
+ authConfig = options;
572
+ const userInfo = await this.retrieveUserInfo((0, ts_types_1.ensureString)(options.instanceUrl), (0, ts_types_1.ensureString)(options.accessToken));
573
+ this.update({ username: userInfo?.username, orgId: userInfo?.organizationId });
574
+ }
575
+ else {
576
+ if (this.options.parentUsername) {
577
+ const parentFields = await this.loadDecryptedAuthFromConfig(this.options.parentUsername);
578
+ options.clientId = parentFields.clientId;
579
+ if (process.env.SFDX_CLIENT_SECRET) {
580
+ options.clientSecret = process.env.SFDX_CLIENT_SECRET;
581
+ }
582
+ else {
583
+ // Grab whatever flow is defined
584
+ Object.assign(options, {
585
+ clientSecret: parentFields.clientSecret,
586
+ privateKey: parentFields.privateKey ? (0, path_1.resolve)(parentFields.privateKey) : parentFields.privateKey,
587
+ });
588
+ }
589
+ }
590
+ // jwt flow
591
+ // Support both sfdx and jsforce private key values
592
+ if (!options.privateKey && options.privateKeyFile) {
593
+ options.privateKey = (0, path_1.resolve)(options.privateKeyFile);
594
+ }
595
+ if (options.privateKey) {
596
+ authConfig = await this.authJwt(options);
597
+ }
598
+ else if (!options.authCode && options.refreshToken) {
599
+ // refresh token flow (from sfdxUrl or OAuth refreshFn)
600
+ authConfig = await this.buildRefreshTokenConfig(options);
601
+ }
602
+ else if (this.options.oauth2 instanceof jsforce_1.OAuth2) {
603
+ // authcode exchange / web auth flow
604
+ authConfig = await this.exchangeToken(options, this.options.oauth2);
605
+ }
606
+ else {
607
+ authConfig = await this.exchangeToken(options);
608
+ }
609
+ }
610
+ authConfig.isDevHub = await this.determineIfDevHub((0, ts_types_1.ensureString)(authConfig.instanceUrl), (0, ts_types_1.ensureString)(authConfig.accessToken));
611
+ if (authConfig.username)
612
+ await this.stateAggregator.orgs.read(authConfig.username, false, false);
613
+ // Update the auth fields WITH encryption
614
+ this.update(authConfig);
615
+ }
616
+ return this;
617
+ }
618
+ // eslint-disable-next-line @typescript-eslint/require-await
619
+ async loadDecryptedAuthFromConfig(username) {
620
+ // Fetch from the persisted auth file
621
+ const authInfo = this.stateAggregator.orgs.get(username, true);
622
+ if (!authInfo) {
623
+ throw messages.createError('namedOrgNotFound', [username]);
624
+ }
625
+ return authInfo;
626
+ }
627
+ isTokenOptions(options) {
628
+ // Although OAuth2Config does not contain refreshToken, privateKey, or privateKeyFile, a JS consumer could still pass those in
629
+ // which WILL have an access token as well, but it should be considered an OAuth2Config at that point.
630
+ return ('accessToken' in options &&
631
+ !('refreshToken' in options) &&
632
+ !('privateKey' in options) &&
633
+ !('privateKeyFile' in options) &&
634
+ !('authCode' in options));
635
+ }
636
+ // A callback function for a connection to refresh an access token. This is used
637
+ // both for a JWT connection and an OAuth connection.
638
+ async refreshFn(conn, callback) {
639
+ this.logger.info('Access token has expired. Updating...');
640
+ try {
641
+ const fields = this.getFields(true);
642
+ await this.initAuthOptions(fields);
643
+ await this.save();
644
+ return await callback(null, fields.accessToken);
645
+ }
646
+ catch (err) {
647
+ const error = err;
648
+ if (error?.message?.includes('Data Not Available')) {
649
+ // Set cause to keep original stacktrace
650
+ return await callback(messages.createError('orgDataNotAvailableError', [this.getUsername()], [], error));
651
+ }
652
+ return await callback(error);
653
+ }
654
+ }
655
+ async readJwtKey(keyFile) {
656
+ return fs.promises.readFile(keyFile, 'utf8');
657
+ }
658
+ // Build OAuth config for a JWT auth flow
659
+ async authJwt(options) {
660
+ if (!options.clientId) {
661
+ throw messages.createError('missingClientId');
662
+ }
663
+ const privateKeyContents = await this.readJwtKey((0, ts_types_1.ensureString)(options.privateKey));
664
+ const { loginUrl = sfdcUrl_1.SfdcUrl.PRODUCTION } = options;
665
+ const url = new sfdcUrl_1.SfdcUrl(loginUrl);
666
+ const createdOrgInstance = (this.getFields().createdOrgInstance ?? '').trim().toLowerCase();
667
+ const audienceUrl = await url.getJwtAudienceUrl(createdOrgInstance);
668
+ let authFieldsBuilder;
669
+ const authErrors = [];
670
+ // given that we can no longer depend on instance names or URls to determine audience, let's try them all
671
+ const loginAndAudienceUrls = (0, sfdcUrl_1.getLoginAudienceCombos)(audienceUrl, loginUrl);
672
+ for (const [login, audience] of loginAndAudienceUrls) {
673
+ try {
674
+ // sequentially, in probabilistic order
675
+ // eslint-disable-next-line no-await-in-loop
676
+ authFieldsBuilder = await this.tryJwtAuth(options.clientId, login, audience, privateKeyContents);
677
+ break;
678
+ }
679
+ catch (err) {
680
+ const error = err;
681
+ const message = error.message.includes('audience')
682
+ ? `${error.message} [audience=${audience} login=${login}]`
683
+ : error.message;
684
+ authErrors.push(message);
685
+ }
686
+ }
687
+ if (!authFieldsBuilder) {
688
+ // messages.createError expects names to end in `error` and this one says Errors so do it manually.
689
+ throw new sfError_1.SfError(messages.getMessage('jwtAuthErrors', [authErrors.join('\n')]), 'JwtAuthError');
690
+ }
691
+ const authFields = {
692
+ accessToken: (0, ts_types_1.asString)(authFieldsBuilder.access_token),
693
+ orgId: parseIdUrl((0, ts_types_1.ensureString)(authFieldsBuilder.id)).orgId,
694
+ loginUrl: options.loginUrl,
695
+ privateKey: options.privateKey,
696
+ clientId: options.clientId,
697
+ };
698
+ const instanceUrl = (0, ts_types_1.ensureString)(authFieldsBuilder.instance_url);
699
+ const sfdcUrl = new sfdcUrl_1.SfdcUrl(instanceUrl);
700
+ try {
701
+ // Check if the url is resolvable. This can fail when my-domains have not been replicated.
702
+ await sfdcUrl.lookup();
703
+ authFields.instanceUrl = instanceUrl;
704
+ }
705
+ catch (err) {
706
+ this.logger.debug(
707
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
708
+ `Instance URL [${authFieldsBuilder.instance_url}] is not available. DNS lookup failed. Using loginUrl [${options.loginUrl}] instead. This may result in a "Destination URL not reset" error.`);
709
+ authFields.instanceUrl = options.loginUrl;
710
+ }
711
+ return authFields;
712
+ }
713
+ async tryJwtAuth(clientId, loginUrl, audienceUrl, privateKeyContents) {
714
+ const jwtToken = jwt.sign({
715
+ iss: clientId,
716
+ sub: this.getUsername(),
717
+ aud: audienceUrl,
718
+ exp: Date.now() + 300,
719
+ }, privateKeyContents, {
720
+ algorithm: 'RS256',
721
+ });
722
+ const oauth2 = new jsforce_1.JwtOAuth2({ loginUrl });
723
+ // jsforce has it types as any
724
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
725
+ return (0, ts_types_1.ensureJsonMap)(await oauth2.jwtAuthorize(jwtToken));
726
+ }
727
+ // Build OAuth config for a refresh token auth flow
728
+ async buildRefreshTokenConfig(options) {
729
+ // Ideally, this would be removed at some point in the distant future when all auth files
730
+ // now have the clientId stored in it.
731
+ if (!options.clientId) {
732
+ options.clientId = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId;
733
+ options.clientSecret = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientSecret;
734
+ }
735
+ if (!options.redirectUri) {
736
+ options.redirectUri = this.getRedirectUri();
737
+ }
738
+ const oauth2 = new jsforce_1.OAuth2(options);
739
+ let authFieldsBuilder;
740
+ try {
741
+ authFieldsBuilder = await oauth2.refreshToken((0, ts_types_1.ensure)(options.refreshToken));
742
+ }
743
+ catch (err) {
744
+ throw messages.createError('refreshTokenAuthError', [err.message]);
745
+ }
746
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
747
+ // @ts-ignore
748
+ const { orgId } = parseIdUrl(authFieldsBuilder.id);
749
+ let username = this.getUsername();
750
+ if (!username) {
751
+ const userInfo = await this.retrieveUserInfo(authFieldsBuilder.instance_url, authFieldsBuilder.access_token);
752
+ username = (0, ts_types_1.ensureString)(userInfo?.username);
753
+ }
754
+ return {
755
+ orgId,
756
+ username,
757
+ accessToken: authFieldsBuilder.access_token,
758
+ instanceUrl: authFieldsBuilder.instance_url,
759
+ loginUrl: options.loginUrl ?? authFieldsBuilder.instance_url,
760
+ refreshToken: options.refreshToken,
761
+ clientId: options.clientId,
762
+ clientSecret: options.clientSecret,
763
+ };
764
+ }
765
+ /**
766
+ * Performs an authCode exchange but the Oauth2 feature of jsforce is extended to include a code_challenge
767
+ *
768
+ * @param options The oauth options
769
+ * @param oauth2 The oauth2 extension that includes a code_challenge
770
+ */
771
+ async exchangeToken(options, oauth2 = new jsforce_1.OAuth2(options)) {
772
+ if (!oauth2.redirectUri) {
773
+ oauth2.redirectUri = this.getRedirectUri();
774
+ }
775
+ if (!oauth2.clientId) {
776
+ oauth2.clientId = this.getClientId();
777
+ }
778
+ // Exchange the auth code for an access token and refresh token.
779
+ let authFields;
780
+ try {
781
+ this.logger.info(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`);
782
+ authFields = await oauth2.requestToken((0, ts_types_1.ensure)(options.authCode));
783
+ }
784
+ catch (err) {
785
+ throw messages.createError('authCodeExchangeError', [err.message]);
786
+ }
787
+ const { orgId } = parseIdUrl(authFields.id);
788
+ let username = this.getUsername();
789
+ // Only need to query for the username if it isn't known. For example, a new auth code exchange
790
+ // rather than refreshing a token on an existing connection.
791
+ if (!username) {
792
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
793
+ // @ts-ignore
794
+ const userInfo = await this.retrieveUserInfo(authFields.instance_url, authFields.access_token);
795
+ username = userInfo?.username;
796
+ }
797
+ return {
798
+ accessToken: authFields.access_token,
799
+ instanceUrl: authFields.instance_url,
800
+ orgId,
801
+ username,
802
+ loginUrl: options.loginUrl ?? authFields.instance_url,
803
+ refreshToken: authFields.refresh_token,
804
+ clientId: options.clientId,
805
+ clientSecret: options.clientSecret,
806
+ };
807
+ }
808
+ async retrieveUserInfo(instanceUrl, accessToken) {
809
+ // Make a REST call for the username directly. Normally this is done via a connection
810
+ // but we don't want to create circular dependencies or lots of snowflakes
811
+ // within this file to support it.
812
+ const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
813
+ const instance = (0, ts_types_1.ensure)(instanceUrl);
814
+ const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
815
+ const userInfoUrl = `${baseUrl.toString()}services/oauth2/userinfo`;
816
+ const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
817
+ try {
818
+ this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${userInfoUrl}`);
819
+ let response = await new transport_1.default().httpRequest({ url: userInfoUrl, method: 'GET', headers });
820
+ if (response.statusCode >= 400) {
821
+ this.throwUserGetException(response);
822
+ }
823
+ else {
824
+ const userInfoJson = (0, kit_1.parseJsonMap)(response.body);
825
+ const url = `${baseUrl.toString()}/services/data/${apiVersion}/sobjects/User/${userInfoJson.user_id}`;
826
+ this.logger.info(`Sending request for User SObject after successful auth code exchange to URL: ${url}`);
827
+ response = await new transport_1.default().httpRequest({ url, method: 'GET', headers });
828
+ if (response.statusCode >= 400) {
829
+ this.throwUserGetException(response);
830
+ }
831
+ else {
832
+ // eslint-disable-next-line camelcase
833
+ userInfoJson.preferred_username = (0, kit_1.parseJsonMap)(response.body).Username;
834
+ }
835
+ return { username: userInfoJson.preferred_username, organizationId: userInfoJson.organization_id };
836
+ }
837
+ }
838
+ catch (err) {
839
+ throw messages.createError('authCodeUsernameRetrievalError', [err.message]);
840
+ }
841
+ }
842
+ /**
843
+ * Given an error while getting the User object, handle different possibilities of response.body.
844
+ *
845
+ * @param response
846
+ * @private
847
+ */
848
+ throwUserGetException(response) {
849
+ let errorMsg = '';
850
+ const bodyAsString = response.body ?? JSON.stringify({ message: 'UNKNOWN', errorCode: 'UNKNOWN' });
851
+ try {
852
+ const body = (0, kit_1.parseJson)(bodyAsString);
853
+ if ((0, ts_types_1.isArray)(body)) {
854
+ errorMsg = body.map((line) => line.message ?? line.errorCode ?? 'UNKNOWN').join(os.EOL);
855
+ }
856
+ else {
857
+ errorMsg = body.message ?? body.errorCode ?? 'UNKNOWN';
858
+ }
859
+ }
860
+ catch (err) {
861
+ errorMsg = `${bodyAsString}`;
862
+ }
863
+ throw new sfError_1.SfError(errorMsg);
864
+ }
865
+ /**
866
+ * Returns `true` if the org is a Dev Hub.
867
+ *
868
+ * Check access to the ScratchOrgInfo object to determine if the org is a dev hub.
869
+ */
870
+ async determineIfDevHub(instanceUrl, accessToken) {
871
+ // Make a REST call for the ScratchOrgInfo obj directly. Normally this is done via a connection
872
+ // but we don't want to create circular dependencies or lots of snowflakes
873
+ // within this file to support it.
874
+ const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
875
+ const instance = (0, ts_types_1.ensure)(instanceUrl);
876
+ const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
877
+ const scratchOrgInfoUrl = `${baseUrl.toString()}/services/data/${apiVersion}/query?q=SELECT%20Id%20FROM%20ScratchOrgInfo%20limit%201`;
878
+ const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
879
+ try {
880
+ const res = await new transport_1.default().httpRequest({ url: scratchOrgInfoUrl, method: 'GET', headers });
881
+ if (res.statusCode >= 400) {
882
+ return false;
883
+ }
884
+ return true;
885
+ }
886
+ catch (err) {
887
+ /* Not a dev hub */
888
+ return false;
889
+ }
890
+ }
891
+ }
892
+ exports.AuthInfo = AuthInfo;
893
893
  //# sourceMappingURL=authInfo.js.map