1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | Object.defineProperty(exports, "__esModule", { value: true });
|
9 | const kit_1 = require("@salesforce/kit");
|
10 | const ts_types_1 = require("@salesforce/ts-types");
|
11 | const crypto_1 = require("crypto");
|
12 | const dns = require("dns");
|
13 | const jsforce_1 = require("jsforce");
|
14 |
|
15 | const Transport = require("jsforce/lib/transport");
|
16 | const jwt = require("jsonwebtoken");
|
17 | const url_1 = require("url");
|
18 | const authInfoConfig_1 = require("./config/authInfoConfig");
|
19 | const configAggregator_1 = require("./config/configAggregator");
|
20 | const connection_1 = require("./connection");
|
21 | const crypto_2 = require("./crypto");
|
22 | const global_1 = require("./global");
|
23 | const logger_1 = require("./logger");
|
24 | const sfdxError_1 = require("./sfdxError");
|
25 | const fs_1 = require("./util/fs");
|
26 |
|
27 | class JwtOAuth2 extends jsforce_1.OAuth2 {
|
28 | constructor(options) {
|
29 | super(options);
|
30 | }
|
31 | async jwtAuthorize(innerToken, callback) {
|
32 |
|
33 |
|
34 | return super._postParams({
|
35 | grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
36 | assertion: innerToken
|
37 | }, callback);
|
38 | }
|
39 | }
|
40 |
|
41 | class AuthCodeOAuth2 extends jsforce_1.OAuth2 {
|
42 | constructor(options) {
|
43 | super(options);
|
44 |
|
45 | this.codeVerifier = base64UrlEscape(crypto_1.randomBytes(Math.ceil(128)).toString('base64'));
|
46 | }
|
47 | |
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | getAuthorizationUrl(params) {
|
54 |
|
55 |
|
56 | const codeChallenge = base64UrlEscape(crypto_1.createHash('sha256')
|
57 | .update(this.codeVerifier)
|
58 | .digest('base64'));
|
59 | kit_1.set(params, 'code_challenge', codeChallenge);
|
60 | return super.getAuthorizationUrl(params);
|
61 | }
|
62 | async requestToken(code, callback) {
|
63 | return super.requestToken(code, callback);
|
64 | }
|
65 | |
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | async _postParams(params, callback) {
|
74 | kit_1.set(params, 'code_verifier', this.codeVerifier);
|
75 |
|
76 | return super._postParams(params, callback);
|
77 | }
|
78 | }
|
79 |
|
80 |
|
81 |
|
82 | var SfdcUrl;
|
83 | (function (SfdcUrl) {
|
84 | SfdcUrl["SANDBOX"] = "https://test.salesforce.com";
|
85 | SfdcUrl["PRODUCTION"] = "https://login.salesforce.com";
|
86 | })(SfdcUrl = exports.SfdcUrl || (exports.SfdcUrl = {}));
|
87 | const INTERNAL_URL_PARTS = [
|
88 | '.internal.',
|
89 | '.vpod.',
|
90 | 'stm.salesforce.com',
|
91 | '.blitz.salesforce.com',
|
92 | 'mobile1.t.salesforce.com'
|
93 | ];
|
94 | function isInternalUrl(loginUrl = '') {
|
95 | return loginUrl.startsWith('https://gs1.') || INTERNAL_URL_PARTS.some(part => loginUrl.includes(part));
|
96 | }
|
97 | function getJwtAudienceUrl(options) {
|
98 |
|
99 | let audienceUrl = SfdcUrl.PRODUCTION;
|
100 | const loginUrl = ts_types_1.getString(options, 'loginUrl', '');
|
101 | const createdOrgInstance = ts_types_1.getString(options, 'createdOrgInstance', '')
|
102 | .trim()
|
103 | .toLowerCase();
|
104 | if (process.env.SFDX_AUDIENCE_URL) {
|
105 | audienceUrl = process.env.SFDX_AUDIENCE_URL;
|
106 | }
|
107 | else if (isInternalUrl(loginUrl)) {
|
108 |
|
109 | audienceUrl = loginUrl;
|
110 | }
|
111 | else if (createdOrgInstance.startsWith('cs') || url_1.parse(loginUrl).hostname === 'test.salesforce.com') {
|
112 | audienceUrl = SfdcUrl.SANDBOX;
|
113 | }
|
114 | else if (createdOrgInstance.startsWith('gs1')) {
|
115 | audienceUrl = 'https://gs1.salesforce.com';
|
116 | }
|
117 | return audienceUrl;
|
118 | }
|
119 |
|
120 |
|
121 | function _parseIdUrl(idUrl) {
|
122 | const idUrls = idUrl.split('/');
|
123 | const userId = idUrls.pop();
|
124 | const orgId = idUrls.pop();
|
125 | return {
|
126 | userId,
|
127 | orgId,
|
128 | url: idUrl
|
129 | };
|
130 | }
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | const DEFAULT_CONNECTED_APP_INFO = {
|
139 | legacyClientId: 'SalesforceDevelopmentExperience',
|
140 | legacyClientSecret: '1384510088588713504'
|
141 | };
|
142 | class AuthInfoCrypto extends crypto_2.Crypto {
|
143 | decryptFields(fields) {
|
144 | return this._crypt(fields, 'decrypt');
|
145 | }
|
146 | encryptFields(fields) {
|
147 | return this._crypt(fields, 'encrypt');
|
148 | }
|
149 | _crypt(fields, method) {
|
150 | const copy = {};
|
151 | for (const key of ts_types_1.keysOf(fields)) {
|
152 | const rawValue = fields[key];
|
153 | if (rawValue !== undefined) {
|
154 | if (ts_types_1.isString(rawValue) && AuthInfoCrypto.encryptedFields.includes(key)) {
|
155 | copy[key] = this[method](ts_types_1.asString(rawValue));
|
156 | }
|
157 | else {
|
158 | copy[key] = rawValue;
|
159 | }
|
160 | }
|
161 | }
|
162 | return copy;
|
163 | }
|
164 | }
|
165 | AuthInfoCrypto.encryptedFields = [
|
166 | 'accessToken',
|
167 | 'refreshToken',
|
168 | 'password',
|
169 | 'clientSecret'
|
170 | ];
|
171 |
|
172 |
|
173 | function base64UrlEscape(base64Encoded) {
|
174 |
|
175 |
|
176 | return base64Encoded
|
177 | .replace(/\+/g, '-')
|
178 | .replace(/\//g, '_')
|
179 | .replace(/=/g, '');
|
180 | }
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 | class AuthInfo extends kit_1.AsyncCreatable {
|
216 | |
217 |
|
218 |
|
219 |
|
220 |
|
221 | constructor(options) {
|
222 | super(options);
|
223 |
|
224 | this.fields = {};
|
225 |
|
226 | this.usingAccessToken = false;
|
227 | this.options = options;
|
228 | }
|
229 | |
230 |
|
231 |
|
232 |
|
233 | static async listAllAuthFiles() {
|
234 | const globalFiles = await fs_1.fs.readdir(global_1.Global.DIR);
|
235 | const authFiles = globalFiles.filter(file => file.match(AuthInfo.authFilenameFilterRegEx));
|
236 |
|
237 | if (kit_1.isEmpty(authFiles)) {
|
238 | const errConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'core', 'NoAuthInfoFound');
|
239 | throw sfdxError_1.SfdxError.create(errConfig);
|
240 | }
|
241 |
|
242 | return authFiles;
|
243 | }
|
244 | |
245 |
|
246 |
|
247 | static async hasAuthentications() {
|
248 | try {
|
249 | const authFiles = await this.listAllAuthFiles();
|
250 | return !kit_1.isEmpty(authFiles);
|
251 | }
|
252 | catch (err) {
|
253 | if (err.name === 'OrgDataNotAvailableError' || err.code === 'ENOENT') {
|
254 | return false;
|
255 | }
|
256 | throw err;
|
257 | }
|
258 | }
|
259 | |
260 |
|
261 |
|
262 |
|
263 | static getAuthorizationUrl(options) {
|
264 | const oauth2 = new AuthCodeOAuth2(options);
|
265 |
|
266 |
|
267 | const params = {
|
268 | state: crypto_1.randomBytes(Math.ceil(6)).toString('hex'),
|
269 | prompt: 'login',
|
270 | scope: 'refresh_token api web'
|
271 | };
|
272 | return oauth2.getAuthorizationUrl(params);
|
273 | }
|
274 | |
275 |
|
276 |
|
277 |
|
278 | static clearCache(username) {
|
279 | if (username) {
|
280 | return AuthInfo.cache.delete(username);
|
281 | }
|
282 | return false;
|
283 | }
|
284 | |
285 |
|
286 |
|
287 | getUsername() {
|
288 | return this.fields.username;
|
289 | }
|
290 | |
291 |
|
292 |
|
293 | isJwt() {
|
294 | const { refreshToken, privateKey } = this.fields;
|
295 | return !refreshToken && !!privateKey;
|
296 | }
|
297 | |
298 |
|
299 |
|
300 | isAccessTokenFlow() {
|
301 | const { refreshToken, privateKey } = this.fields;
|
302 | return !refreshToken && !privateKey;
|
303 | }
|
304 | |
305 |
|
306 |
|
307 | isOauth() {
|
308 | return !this.isAccessTokenFlow() && !this.isJwt();
|
309 | }
|
310 | |
311 |
|
312 |
|
313 | isRefreshTokenFlow() {
|
314 | const { refreshToken, authCode } = this.fields;
|
315 | return !authCode && !!refreshToken;
|
316 | }
|
317 | |
318 |
|
319 |
|
320 |
|
321 | async save(authData) {
|
322 | this.update(authData);
|
323 | const username = ts_types_1.ensure(this.getUsername());
|
324 | AuthInfo.cache.set(username, this.fields);
|
325 | const dataToSave = kit_1.cloneJson(this.fields);
|
326 | this.logger.debug(dataToSave);
|
327 | const config = await authInfoConfig_1.AuthInfoConfig.create(Object.assign({}, authInfoConfig_1.AuthInfoConfig.getOptions(username), { throwOnNotFound: false }));
|
328 | config.setContentsFromObject(dataToSave);
|
329 | await config.write();
|
330 | this.logger.info(`Saved auth info for username: ${this.getUsername()}`);
|
331 | return this;
|
332 | }
|
333 | |
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 | update(authData, encrypt = true) {
|
341 | if (authData && ts_types_1.isPlainObject(authData)) {
|
342 | let copy = kit_1.cloneJson(authData);
|
343 | if (encrypt) {
|
344 | copy = this.authInfoCrypto.encryptFields(copy);
|
345 | }
|
346 | Object.assign(this.fields, copy);
|
347 | this.logger.info(`Updated auth info for username: ${this.getUsername()}`);
|
348 | }
|
349 | return this;
|
350 | }
|
351 | |
352 |
|
353 |
|
354 | getConnectionOptions() {
|
355 | let opts;
|
356 | const { accessToken, instanceUrl } = this.fields;
|
357 | if (this.isAccessTokenFlow()) {
|
358 | this.logger.info('Returning fields for a connection using access token.');
|
359 |
|
360 | opts = { accessToken, instanceUrl };
|
361 | }
|
362 | else if (this.isJwt()) {
|
363 | this.logger.info('Returning fields for a connection using JWT config.');
|
364 | opts = {
|
365 | accessToken,
|
366 | instanceUrl,
|
367 | refreshFn: this.refreshFn.bind(this)
|
368 | };
|
369 | }
|
370 | else {
|
371 |
|
372 |
|
373 |
|
374 |
|
375 | this.logger.info('Returning fields for a connection using OAuth config.');
|
376 |
|
377 | opts = {
|
378 | oauth2: {
|
379 | loginUrl: instanceUrl || 'https://login.salesforce.com',
|
380 | clientId: this.fields.clientId || DEFAULT_CONNECTED_APP_INFO.legacyClientId,
|
381 | redirectUri: 'http://localhost:1717/OauthRedirect'
|
382 | },
|
383 | accessToken,
|
384 | instanceUrl,
|
385 | refreshFn: this.refreshFn.bind(this)
|
386 | };
|
387 | }
|
388 |
|
389 | return this.authInfoCrypto.decryptFields(opts);
|
390 | }
|
391 | |
392 |
|
393 |
|
394 | getFields() {
|
395 | return this.fields;
|
396 | }
|
397 | |
398 |
|
399 |
|
400 | isUsingAccessToken() {
|
401 | return this.usingAccessToken;
|
402 | }
|
403 | |
404 |
|
405 |
|
406 |
|
407 |
|
408 | getSfdxAuthUrl() {
|
409 | const decryptedFields = this.authInfoCrypto.decryptFields(this.fields);
|
410 | const instanceUrl = ts_types_1.ensure(decryptedFields.instanceUrl).replace(/^https?:\/\//, '');
|
411 | let sfdxAuthUrl = 'force://';
|
412 | if (decryptedFields.clientId) {
|
413 | sfdxAuthUrl += `${decryptedFields.clientId}:${decryptedFields.clientSecret}:`;
|
414 | }
|
415 | sfdxAuthUrl += `${decryptedFields.refreshToken}@${instanceUrl}`;
|
416 | return sfdxAuthUrl;
|
417 | }
|
418 | |
419 |
|
420 |
|
421 | async init() {
|
422 |
|
423 | const options = this.options.oauth2Options || this.options.accessTokenOptions;
|
424 | if (!this.options.username && !(this.options.oauth2Options || this.options.accessTokenOptions)) {
|
425 | throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthInfoCreationError');
|
426 | }
|
427 | this.fields.username = this.options.username || ts_types_1.getString(options, 'username') || undefined;
|
428 |
|
429 | const accessTokenMatch = ts_types_1.isString(this.fields.username) && this.fields.username.match(/^(00D\w{12,15})![\.\w]*$/);
|
430 | if (accessTokenMatch) {
|
431 |
|
432 | this.logger = await logger_1.Logger.child('AuthInfo');
|
433 | this.authInfoCrypto = await AuthInfoCrypto.create({
|
434 | noResetOnClose: true
|
435 | });
|
436 | const aggregator = await configAggregator_1.ConfigAggregator.create();
|
437 | const instanceUrl = aggregator.getPropertyValue('instanceUrl') || SfdcUrl.PRODUCTION;
|
438 | this.update({
|
439 | accessToken: this.options.username,
|
440 | instanceUrl,
|
441 | orgId: accessTokenMatch[1]
|
442 | });
|
443 | this.usingAccessToken = true;
|
444 | }
|
445 | else {
|
446 | await this.initAuthOptions(options);
|
447 | }
|
448 | }
|
449 | |
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 | async initAuthOptions(options) {
|
458 | this.logger = await logger_1.Logger.child('AuthInfo');
|
459 | this.authInfoCrypto = await AuthInfoCrypto.create();
|
460 |
|
461 | let authConfig;
|
462 | if (options) {
|
463 | options = kit_1.cloneJson(options);
|
464 | if (this.isTokenOptions(options)) {
|
465 | authConfig = options;
|
466 | }
|
467 | else {
|
468 |
|
469 |
|
470 | if (!options.privateKey && options.privateKeyFile) {
|
471 | options.privateKey = options.privateKeyFile;
|
472 | }
|
473 | if (options.privateKey) {
|
474 | authConfig = await this.buildJwtConfig(options);
|
475 | }
|
476 | else if (!options.authCode && options.refreshToken) {
|
477 |
|
478 | authConfig = await this.buildRefreshTokenConfig(options);
|
479 | }
|
480 | else {
|
481 |
|
482 | authConfig = await this.buildWebAuthConfig(options);
|
483 | }
|
484 | }
|
485 |
|
486 | this.update(authConfig);
|
487 | }
|
488 | else {
|
489 | const username = ts_types_1.ensure(this.getUsername());
|
490 | if (AuthInfo.cache.has(username)) {
|
491 | authConfig = ts_types_1.ensure(AuthInfo.cache.get(username));
|
492 | }
|
493 | else {
|
494 |
|
495 | try {
|
496 | const config = await authInfoConfig_1.AuthInfoConfig.create(Object.assign({}, authInfoConfig_1.AuthInfoConfig.getOptions(username), { throwOnNotFound: true }));
|
497 | authConfig = config.toObject();
|
498 | }
|
499 | catch (e) {
|
500 | if (e.code === 'ENOENT') {
|
501 | throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'NamedOrgNotFound', [username]);
|
502 | }
|
503 | else {
|
504 | throw e;
|
505 | }
|
506 | }
|
507 | }
|
508 |
|
509 | this.update(authConfig, false);
|
510 | }
|
511 |
|
512 | AuthInfo.cache.set(ts_types_1.ensure(this.getUsername()), this.fields);
|
513 | return this;
|
514 | }
|
515 | isTokenOptions(options) {
|
516 |
|
517 |
|
518 | return ('accessToken' in options &&
|
519 | !('refreshToken' in options) &&
|
520 | !('privateKey' in options) &&
|
521 | !('privateKeyFile' in options) &&
|
522 | !('authCode' in options));
|
523 | }
|
524 |
|
525 |
|
526 | async refreshFn(conn, callback) {
|
527 | this.logger.info('Access token has expired. Updating...');
|
528 | try {
|
529 | const fields = this.authInfoCrypto.decryptFields(this.fields);
|
530 | await this.initAuthOptions(fields);
|
531 | await this.save();
|
532 | return await callback(null, fields.accessToken);
|
533 | }
|
534 | catch (err) {
|
535 | if (err.message && err.message.includes('Data Not Available')) {
|
536 | const errConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'core', 'OrgDataNotAvailableError', [
|
537 | this.getUsername()
|
538 | ]);
|
539 | for (let i = 1; i < 5; i++) {
|
540 | errConfig.addAction(`OrgDataNotAvailableErrorAction${i}`);
|
541 | }
|
542 | return await callback(sfdxError_1.SfdxError.create(errConfig));
|
543 | }
|
544 | return await callback(err);
|
545 | }
|
546 | }
|
547 |
|
548 | async buildJwtConfig(options) {
|
549 | const privateKeyContents = await fs_1.fs.readFile(ts_types_1.ensure(options.privateKey), 'utf8');
|
550 | const audienceUrl = getJwtAudienceUrl(options);
|
551 | const jwtToken = await jwt.sign({
|
552 | iss: options.clientId,
|
553 | sub: this.getUsername(),
|
554 | aud: audienceUrl,
|
555 | exp: Date.now() + 300
|
556 | }, privateKeyContents, {
|
557 | algorithm: 'RS256'
|
558 | });
|
559 | const oauth2 = new JwtOAuth2({ loginUrl: options.loginUrl });
|
560 | let _authFields;
|
561 | try {
|
562 | _authFields = ts_types_1.ensureJsonMap(await oauth2.jwtAuthorize(jwtToken));
|
563 | }
|
564 | catch (err) {
|
565 | throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'JWTAuthError', [err.message]);
|
566 | }
|
567 | const authFields = {
|
568 | accessToken: ts_types_1.asString(_authFields.access_token),
|
569 | orgId: _parseIdUrl(ts_types_1.ensureString(_authFields.id)).orgId,
|
570 | loginUrl: options.loginUrl,
|
571 | privateKey: options.privateKey
|
572 | };
|
573 | const instanceUrl = ts_types_1.ensureString(_authFields.instance_url);
|
574 | const parsedUrl = url_1.parse(instanceUrl);
|
575 | try {
|
576 |
|
577 | await this.lookup(ts_types_1.ensure(parsedUrl.hostname));
|
578 | authFields.instanceUrl = instanceUrl;
|
579 | }
|
580 | catch (err) {
|
581 | this.logger.debug(`Instance URL [${_authFields.instance_url}] is not available. DNS lookup failed. Using loginUrl [${options.loginUrl}] instead. This may result in a "Destination URL not reset" error.`);
|
582 | authFields.instanceUrl = options.loginUrl;
|
583 | }
|
584 | return authFields;
|
585 | }
|
586 |
|
587 | async buildRefreshTokenConfig(options) {
|
588 |
|
589 |
|
590 | if (!options.clientId) {
|
591 | options.clientId = DEFAULT_CONNECTED_APP_INFO.legacyClientId;
|
592 | options.clientSecret = DEFAULT_CONNECTED_APP_INFO.legacyClientSecret;
|
593 | }
|
594 | const oauth2 = new jsforce_1.OAuth2(options);
|
595 | let _authFields;
|
596 | try {
|
597 | _authFields = await oauth2.refreshToken(ts_types_1.ensure(options.refreshToken));
|
598 | }
|
599 | catch (err) {
|
600 | throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'RefreshTokenAuthError', [err.message]);
|
601 | }
|
602 | return {
|
603 | accessToken: _authFields.access_token,
|
604 |
|
605 | instanceUrl: _authFields.instance_url,
|
606 |
|
607 | orgId: _parseIdUrl(_authFields.id).orgId,
|
608 |
|
609 | loginUrl: options.loginUrl || _authFields.instance_url,
|
610 | refreshToken: options.refreshToken,
|
611 | clientId: options.clientId,
|
612 | clientSecret: options.clientSecret
|
613 | };
|
614 | }
|
615 |
|
616 | async buildWebAuthConfig(options) {
|
617 | const oauth2 = new AuthCodeOAuth2(options);
|
618 |
|
619 | let _authFields;
|
620 | try {
|
621 | this.logger.info(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`);
|
622 | _authFields = await oauth2.requestToken(ts_types_1.ensure(options.authCode));
|
623 | }
|
624 | catch (err) {
|
625 | throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthCodeExchangeError', [err.message]);
|
626 | }
|
627 |
|
628 | const { userId, orgId } = _parseIdUrl(_authFields.id);
|
629 |
|
630 |
|
631 |
|
632 | const apiVersion = 'v42.0';
|
633 | const instance = ts_types_1.ensure(ts_types_1.getString(_authFields, 'instance_url'));
|
634 | const url = `${instance}/services/data/${apiVersion}/sobjects/User/${userId}`;
|
635 | const headers = Object.assign({ Authorization: `Bearer ${_authFields.access_token}` }, connection_1.SFDX_HTTP_HEADERS);
|
636 | let username;
|
637 | try {
|
638 | this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${url}`);
|
639 | const response = await new Transport().httpRequest({ url, headers });
|
640 | username = ts_types_1.asString(kit_1.parseJsonMap(response.body).Username);
|
641 | }
|
642 | catch (err) {
|
643 | throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthCodeUsernameRetrievalError', [orgId, err.message]);
|
644 | }
|
645 | return {
|
646 | accessToken: _authFields.access_token,
|
647 |
|
648 | instanceUrl: _authFields.instance_url,
|
649 | orgId,
|
650 | username,
|
651 |
|
652 | loginUrl: options.loginUrl || _authFields.instance_url,
|
653 | refreshToken: _authFields.refresh_token
|
654 | };
|
655 | }
|
656 |
|
657 | async lookup(host) {
|
658 | return new Promise((resolve, reject) => {
|
659 | dns.lookup(host, (err, address, family) => {
|
660 | if (err) {
|
661 | reject(err);
|
662 | }
|
663 | else {
|
664 | resolve({ address, family });
|
665 | }
|
666 | });
|
667 | });
|
668 | }
|
669 | }
|
670 |
|
671 | AuthInfo.authFilenameFilterRegEx = /^[^.][^@]*@[^.]+(\.[^.\s]+)+\.json$/;
|
672 |
|
673 | AuthInfo.cache = new Map();
|
674 | exports.AuthInfo = AuthInfo;
|
675 |
|
\ | No newline at end of file |