@microsoft/vscode-azext-azureauth 5.0.0 → 5.1.1

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 (32) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/src/AzureDevOpsSubscriptionProvider.js +2 -3
  3. package/dist/cjs/src/VSCodeAzureSubscriptionProvider.js +100 -47
  4. package/dist/cjs/src/index.js +1 -0
  5. package/dist/cjs/src/utils/getUnauthenticatedTenants.js +4 -2
  6. package/dist/cjs/src/utils/isGetSubscriptionsFilter.js +27 -0
  7. package/dist/esm/src/AzureDevOpsSubscriptionProvider.d.ts +6 -6
  8. package/dist/esm/src/AzureDevOpsSubscriptionProvider.js +2 -3
  9. package/dist/esm/src/AzureSubscription.d.ts +2 -2
  10. package/dist/esm/src/AzureTenant.d.ts +2 -2
  11. package/dist/esm/src/VSCodeAzureSubscriptionProvider.d.ts +13 -14
  12. package/dist/esm/src/VSCodeAzureSubscriptionProvider.js +100 -47
  13. package/dist/esm/src/index.d.ts +1 -46
  14. package/dist/esm/src/index.js +1 -0
  15. package/dist/esm/src/utils/getUnauthenticatedTenants.d.ts +4 -1
  16. package/dist/esm/src/utils/getUnauthenticatedTenants.js +4 -2
  17. package/dist/esm/src/utils/isGetSubscriptionsFilter.d.ts +14 -0
  18. package/dist/esm/src/utils/isGetSubscriptionsFilter.js +23 -0
  19. package/package.json +3 -3
  20. package/dist/cjs/src/AzureAuthentication.d.ts +0 -21
  21. package/dist/cjs/src/AzureDevOpsSubscriptionProvider.d.ts +0 -68
  22. package/dist/cjs/src/AzureSubscription.d.ts +0 -49
  23. package/dist/cjs/src/AzureSubscriptionProvider.d.ts +0 -82
  24. package/dist/cjs/src/AzureTenant.d.ts +0 -5
  25. package/dist/cjs/src/NotSignedInError.d.ts +0 -15
  26. package/dist/cjs/src/VSCodeAzureSubscriptionProvider.d.ts +0 -117
  27. package/dist/cjs/src/getSessionFromVSCode.d.ts +0 -13
  28. package/dist/cjs/src/index.d.ts +0 -56
  29. package/dist/cjs/src/signInToTenant.d.ts +0 -6
  30. package/dist/cjs/src/utils/configuredAzureEnv.d.ts +0 -24
  31. package/dist/cjs/src/utils/getUnauthenticatedTenants.d.ts +0 -6
  32. package/dist/cjs/src/utils/isAuthenticationWwwAuthenticateRequest.d.ts +0 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Change Log
2
2
 
3
+ ## 5.1.1 - 2025-10-28
4
+
5
+ * [#2111](https://github.com/microsoft/vscode-azuretools/pull/2111) Same as https://github.com/microsoft/vscode-azuretools/pull/2110 but a better fix.
6
+
7
+ ## 5.1.0 - 2025-10-27
8
+
9
+ * [#2102](https://github.com/microsoft/vscode-azuretools/pull/2102) Fixes an issue causing infinite event loops especially in https://vscode.dev/azure
10
+ * [#2110](https://github.com/microsoft/vscode-azuretools/pull/2110) `vscode.authentication.onDidChangeSessions()` is no longer subscribed to unless the caller calls `AzureSubscriptionProvider.onDidSignIn()` or `AzureSubscriptionProvider.onDidSignOut()`.
11
+
12
+ ## 5.0.0 - 2025-10-07
13
+
14
+ * [#2092](https://github.com/microsoft/vscode-azuretools/pull/2092) Converts from CJS only to CJS+ESM
15
+ * Adopts finalized auth challenges API
16
+
3
17
  ## 4.2.2 - 2025-09-10
4
18
 
5
19
  * [#2073](https://github.com/microsoft/vscode-azuretools/pull/2073) Changes to adjust to proposed API changes
@@ -6,7 +6,6 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.AzureDevOpsSubscriptionProvider = void 0;
8
8
  exports.createAzureDevOpsSubscriptionProviderFactory = createAzureDevOpsSubscriptionProviderFactory;
9
- const vscode_1 = require("vscode");
10
9
  const configuredAzureEnv_1 = require("./utils/configuredAzureEnv");
11
10
  let azureDevOpsSubscriptionProvider;
12
11
  function createAzureDevOpsSubscriptionProviderFactory(initializer) {
@@ -144,8 +143,8 @@ class AzureDevOpsSubscriptionProvider {
144
143
  }
145
144
  };
146
145
  }
147
- onDidSignIn = () => { return new vscode_1.Disposable(() => { }); };
148
- onDidSignOut = () => { return new vscode_1.Disposable(() => { }); };
146
+ onDidSignIn = () => { return { dispose() { } }; };
147
+ onDidSignOut = () => { return { dispose() { } }; };
149
148
  }
150
149
  exports.AzureDevOpsSubscriptionProvider = AzureDevOpsSubscriptionProvider;
151
150
  /*
@@ -43,42 +43,92 @@ const getSessionFromVSCode_1 = require("./getSessionFromVSCode");
43
43
  const NotSignedInError_1 = require("./NotSignedInError");
44
44
  const configuredAzureEnv_1 = require("./utils/configuredAzureEnv");
45
45
  const isAuthenticationWwwAuthenticateRequest_1 = require("./utils/isAuthenticationWwwAuthenticateRequest");
46
+ const isGetSubscriptionsFilter_1 = require("./utils/isGetSubscriptionsFilter");
46
47
  const EventDebounce = 5 * 1000; // 5 seconds
48
+ let armSubs;
47
49
  /**
48
50
  * A class for obtaining Azure subscription information using VSCode's built-in authentication
49
51
  * provider.
50
52
  */
51
- class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
53
+ class VSCodeAzureSubscriptionProvider {
52
54
  logger;
55
+ listenerDisposable;
53
56
  onDidSignInEmitter = new vscode.EventEmitter();
54
- lastSignInEventFired = 0;
55
- suppressSignInEvents = false;
56
57
  onDidSignOutEmitter = new vscode.EventEmitter();
57
- lastSignOutEventFired = 0;
58
+ lastEventFired = 0;
59
+ suppressEvents = false;
60
+ priorAccounts;
61
+ accountsRemovedPromise;
58
62
  // So that customers can easily share logs, try to only log PII using trace level
59
63
  constructor(logger) {
60
- const disposable = vscode.authentication.onDidChangeSessions(async (e) => {
64
+ this.logger = logger;
65
+ void this.accountsRemoved(); // Initialize priorAccounts
66
+ }
67
+ dispose() {
68
+ this.listenerDisposable?.dispose();
69
+ this.onDidSignInEmitter.dispose();
70
+ this.onDidSignOutEmitter.dispose();
71
+ }
72
+ onDidSignIn(callback, thisArg, disposables) {
73
+ this.listenIfNeeded();
74
+ return this.onDidSignInEmitter.event(callback, thisArg, disposables);
75
+ }
76
+ onDidSignOut(callback, thisArg, disposables) {
77
+ this.listenIfNeeded();
78
+ return this.onDidSignOutEmitter.event(callback, thisArg, disposables);
79
+ }
80
+ listenIfNeeded() {
81
+ if (this.listenerDisposable) {
82
+ return;
83
+ }
84
+ this.listenerDisposable = vscode.authentication.onDidChangeSessions(async (e) => {
61
85
  // Ignore any sign in that isn't for the configured auth provider
62
86
  if (e.provider.id !== (0, configuredAzureEnv_1.getConfiguredAuthProviderId)()) {
63
87
  return;
64
88
  }
65
- if (await this.isSignedIn()) {
66
- if (!this.suppressSignInEvents && Date.now() > this.lastSignInEventFired + EventDebounce) {
67
- this.lastSignInEventFired = Date.now();
68
- this.onDidSignInEmitter.fire();
69
- }
89
+ if (this.suppressEvents || Date.now() < this.lastEventFired + EventDebounce) {
90
+ return;
91
+ }
92
+ this.lastEventFired = Date.now();
93
+ if (!await this.accountsRemoved()) {
94
+ this.logger?.debug('auth: Firing onDidSignIn event');
95
+ this.onDidSignInEmitter.fire();
70
96
  }
71
- else if (Date.now() > this.lastSignOutEventFired + EventDebounce) {
72
- this.lastSignOutEventFired = Date.now();
97
+ else {
98
+ this.logger?.debug('auth: Firing onDidSignOut event');
73
99
  this.onDidSignOutEmitter.fire();
74
100
  }
75
101
  });
76
- super(() => {
77
- this.onDidSignInEmitter.dispose();
78
- this.onDidSignOutEmitter.dispose();
79
- disposable.dispose();
80
- });
81
- this.logger = logger;
102
+ }
103
+ async accountsRemoved() {
104
+ // If there's already an ongoing accountsRemoved operation, return its result
105
+ if (this.accountsRemovedPromise) {
106
+ return this.accountsRemovedPromise;
107
+ }
108
+ // Create a new promise for this operation
109
+ this.accountsRemovedPromise = (async () => {
110
+ try {
111
+ this.suppressEvents = true;
112
+ const currentAccounts = Array.from(await vscode.authentication.getAccounts((0, configuredAzureEnv_1.getConfiguredAuthProviderId)()));
113
+ const priorAccountCount = this.priorAccounts?.length ?? 0;
114
+ this.priorAccounts = currentAccounts;
115
+ // The only way a sign out happens is if an account is removed entirely from the list of accounts
116
+ if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) {
117
+ return true;
118
+ }
119
+ return false;
120
+ }
121
+ finally {
122
+ this.suppressEvents = false;
123
+ }
124
+ })();
125
+ try {
126
+ return await this.accountsRemovedPromise;
127
+ }
128
+ finally {
129
+ // Clear the promise when done so future calls can proceed
130
+ this.accountsRemovedPromise = undefined;
131
+ }
82
132
  }
83
133
  /**
84
134
  * Gets a list of tenants available to the user.
@@ -124,24 +174,28 @@ class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
124
174
  async getSubscriptions(filter = true) {
125
175
  this.logger?.debug('auth: Loading subscriptions...');
126
176
  const startTime = Date.now();
127
- const configuredTenantFilter = await this.getTenantFilters();
128
- const tenantIdsToFilterBy =
129
- // Only filter by the tenant ID option if it is provided
130
- (typeof filter === 'object' && filter.tenantId ? [filter.tenantId] :
177
+ let tenantIdsToFilterBy;
178
+ if ((0, isGetSubscriptionsFilter_1.isGetSubscriptionsTenantFilter)(filter)) {
179
+ // Only filter by the tenant ID option if it is provided
180
+ tenantIdsToFilterBy = [filter.tenantId];
181
+ }
182
+ else if (filter === true) {
131
183
  // Only filter by the configured filter if `filter` is true AND there are tenants in the configured filter
132
- filter === true && configuredTenantFilter.length > 0 ? configuredTenantFilter :
133
- undefined);
184
+ const configuredTenantFilter = await this.getTenantFilters();
185
+ if (configuredTenantFilter.length > 0) {
186
+ tenantIdsToFilterBy = configuredTenantFilter;
187
+ }
188
+ }
134
189
  const allSubscriptions = [];
135
190
  let accountCount; // only used for logging
136
191
  try {
137
- this.suppressSignInEvents = true;
192
+ this.suppressEvents = true;
138
193
  // Get the list of tenants from each account (filtered or all)
139
- const accounts = typeof filter === 'object' && filter.account ? [filter.account] : await vscode.authentication.getAccounts((0, configuredAzureEnv_1.getConfiguredAuthProviderId)());
194
+ const accounts = (0, isGetSubscriptionsFilter_1.isGetSubscriptionsAccountFilter)(filter) ? [filter.account] : await vscode.authentication.getAccounts((0, configuredAzureEnv_1.getConfiguredAuthProviderId)());
140
195
  accountCount = accounts.length;
141
196
  for (const account of accounts) {
142
- for (const tenant of await this.getTenants(account)) {
143
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
144
- const tenantId = tenant.tenantId;
197
+ const tenantIds = (0, isGetSubscriptionsFilter_1.isGetSubscriptionsTenantFilter)(filter) ? [filter.tenantId] : (await this.getTenants(account)).map(t => t.tenantId);
198
+ for (const tenantId of tenantIds) {
145
199
  if (tenantIdsToFilterBy?.includes(tenantId) === false) {
146
200
  continue;
147
201
  }
@@ -153,12 +207,13 @@ class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
153
207
  }
154
208
  }
155
209
  finally {
156
- this.suppressSignInEvents = false;
210
+ this.suppressEvents = false;
157
211
  }
158
212
  // It's possible that by listing subscriptions in all tenants and the "home" tenant there could be duplicate subscriptions
159
213
  // Thus, we remove duplicate subscriptions. However, if multiple accounts have the same subscription, we keep them.
214
+ // There are also cases where the same subscription could appear in different tenants under the same account so we also need to keep those
160
215
  const subscriptionMap = new Map();
161
- allSubscriptions.forEach(sub => subscriptionMap.set(`${sub.account.id}/${sub.subscriptionId}`, sub));
216
+ allSubscriptions.forEach(sub => subscriptionMap.set(`${sub.account.id}/${sub.tenantId}/${sub.subscriptionId}`, sub));
162
217
  const uniqueSubscriptions = Array.from(subscriptionMap.values());
163
218
  const endTime = Date.now();
164
219
  this.logger?.debug(`auth: Got ${uniqueSubscriptions.length} subscriptions from ${accountCount} accounts in ${endTime - startTime}ms`);
@@ -214,18 +269,20 @@ class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
214
269
  */
215
270
  async signIn(tenantId, account) {
216
271
  this.logger?.debug(`auth: Signing in (account="${account?.label ?? 'none'}") (tenantId="${tenantId ?? 'none'}")`);
217
- const session = await (0, getSessionFromVSCode_1.getSessionFromVSCode)([], tenantId, {
218
- createIfNone: true,
219
- // If no account is provided, then clear the session preference which tells VS Code to show the account picker
220
- clearSessionPreference: !account,
221
- account,
222
- });
223
- return !!session;
272
+ try {
273
+ this.suppressEvents = true;
274
+ const session = await (0, getSessionFromVSCode_1.getSessionFromVSCode)([], tenantId, {
275
+ createIfNone: true,
276
+ // If no account is provided, then clear the session preference which tells VS Code to show the account picker
277
+ clearSessionPreference: !account,
278
+ account,
279
+ });
280
+ return !!session;
281
+ }
282
+ finally {
283
+ this.suppressEvents = false;
284
+ }
224
285
  }
225
- /**
226
- * An event that is fired when the user signs in. Debounced to fire at most once every 5 seconds.
227
- */
228
- onDidSignIn = this.onDidSignInEmitter.event;
229
286
  /**
230
287
  * Signs the user out
231
288
  *
@@ -234,10 +291,6 @@ class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
234
291
  signOut() {
235
292
  throw new Error(vscode.l10n.t('Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.'));
236
293
  }
237
- /**
238
- * An event that is fired when the user signs out. Debounced to fire at most once every 5 seconds.
239
- */
240
- onDidSignOut = this.onDidSignOutEmitter.event;
241
294
  /**
242
295
  * Gets the tenant filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
243
296
  * override the settings with a custom filter, implement a child class with `getSubscriptionFilters()`
@@ -308,7 +361,7 @@ class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
308
361
  * @returns A client, the credential used by the client, and the authentication function
309
362
  */
310
363
  async getSubscriptionClient(account, tenantId, scopes) {
311
- const armSubs = await import('@azure/arm-resources-subscriptions');
364
+ armSubs ||= await import('@azure/arm-resources-subscriptions');
312
365
  const session = await (0, getSessionFromVSCode_1.getSessionFromVSCode)(scopes, tenantId, { createIfNone: false, silent: true, account });
313
366
  if (!session) {
314
367
  throw new NotSignedInError_1.NotSignedInError();
@@ -23,6 +23,7 @@ __exportStar(require("./AzureDevOpsSubscriptionProvider"), exports);
23
23
  __exportStar(require("./AzureSubscription"), exports);
24
24
  __exportStar(require("./AzureSubscriptionProvider"), exports);
25
25
  __exportStar(require("./AzureTenant"), exports);
26
+ __exportStar(require("./getSessionFromVSCode"), exports);
26
27
  __exportStar(require("./NotSignedInError"), exports);
27
28
  __exportStar(require("./signInToTenant"), exports);
28
29
  __exportStar(require("./utils/configuredAzureEnv"), exports);
@@ -6,10 +6,12 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.getUnauthenticatedTenants = getUnauthenticatedTenants;
8
8
  /**
9
+ * @param subscriptionProvider The {@link AzureSubscriptionProvider} to use
10
+ * @param account (Optional) The account to get unauthenticated tenants for
9
11
  * @returns list of tenants that VS Code doesn't have sessions for
10
12
  */
11
- async function getUnauthenticatedTenants(subscriptionProvider) {
12
- const tenants = await subscriptionProvider.getTenants();
13
+ async function getUnauthenticatedTenants(subscriptionProvider, account) {
14
+ const tenants = await subscriptionProvider.getTenants(account);
13
15
  const unauthenticatedTenants = [];
14
16
  for await (const tenant of tenants) {
15
17
  if (!await subscriptionProvider.isSignedIn(tenant.tenantId, tenant.account)) {
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) Microsoft Corporation. All rights reserved.
4
+ * Licensed under the MIT License. See License.md in the project root for license information.
5
+ *--------------------------------------------------------------------------------------------*/
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.isGetSubscriptionsTenantFilter = isGetSubscriptionsTenantFilter;
8
+ exports.isGetSubscriptionsAccountFilter = isGetSubscriptionsAccountFilter;
9
+ /**
10
+ * Check if an object is a {@link GetSubscriptionsFilter} with a tenantId.
11
+ */
12
+ function isGetSubscriptionsTenantFilter(obj) {
13
+ if (typeof obj === 'object' && !!obj && 'tenantId' in obj && typeof obj.tenantId === 'string' && !!obj.tenantId) {
14
+ return true;
15
+ }
16
+ return false;
17
+ }
18
+ /**
19
+ * Check if an object is a {@link GetSubscriptionsFilter} with an account.
20
+ */
21
+ function isGetSubscriptionsAccountFilter(obj) {
22
+ if (typeof obj === 'object' && !!obj && 'account' in obj && typeof obj.account === 'object' && !!obj.account) {
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+ //# sourceMappingURL=isGetSubscriptionsFilter.js.map
@@ -1,7 +1,7 @@
1
- import { Event } from 'vscode';
2
- import { AzureSubscription } from './AzureSubscription';
3
- import { AzureSubscriptionProvider, GetSubscriptionsFilter } from './AzureSubscriptionProvider';
4
- import { AzureTenant } from './AzureTenant';
1
+ import type * as vscode from 'vscode';
2
+ import type { AzureSubscription } from './AzureSubscription';
3
+ import type { AzureSubscriptionProvider, GetSubscriptionsFilter } from './AzureSubscriptionProvider';
4
+ import type { AzureTenant } from './AzureTenant';
5
5
  export interface AzureDevOpsSubscriptionProviderInitializer {
6
6
  /**
7
7
  * The resource ID of the Azure DevOps federated service connection,
@@ -63,6 +63,6 @@ export declare class AzureDevOpsSubscriptionProvider implements AzureSubscriptio
63
63
  * @returns A client, the credential used by the client, and the authentication function
64
64
  */
65
65
  private getSubscriptionClient;
66
- onDidSignIn: Event<void>;
67
- onDidSignOut: Event<void>;
66
+ onDidSignIn: vscode.Event<void>;
67
+ onDidSignOut: vscode.Event<void>;
68
68
  }
@@ -2,7 +2,6 @@
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Licensed under the MIT License. See License.txt in the project root for license information.
4
4
  *--------------------------------------------------------------------------------------------*/
5
- import { Disposable } from 'vscode';
6
5
  import { getConfiguredAzureEnv } from './utils/configuredAzureEnv';
7
6
  let azureDevOpsSubscriptionProvider;
8
7
  export function createAzureDevOpsSubscriptionProviderFactory(initializer) {
@@ -140,8 +139,8 @@ export class AzureDevOpsSubscriptionProvider {
140
139
  }
141
140
  };
142
141
  }
143
- onDidSignIn = () => { return new Disposable(() => { }); };
144
- onDidSignOut = () => { return new Disposable(() => { }); };
142
+ onDidSignIn = () => { return { dispose() { } }; };
143
+ onDidSignOut = () => { return { dispose() { } }; };
145
144
  }
146
145
  /*
147
146
  * @param serviceConnectionId The resource ID of the Azure DevOps federated service connection,
@@ -1,7 +1,7 @@
1
1
  import type { TokenCredential } from '@azure/core-auth';
2
2
  import type { Environment } from '@azure/ms-rest-azure-env';
3
- import * as vscode from "vscode";
4
- import { AzureAuthentication } from './AzureAuthentication';
3
+ import type * as vscode from "vscode";
4
+ import type { AzureAuthentication } from './AzureAuthentication';
5
5
  /**
6
6
  * A type representing an Azure subscription ID, not including the tenant ID.
7
7
  */
@@ -1,5 +1,5 @@
1
- import { TenantIdDescription } from "@azure/arm-resources-subscriptions";
2
- import * as vscode from 'vscode';
1
+ import type { TenantIdDescription } from "@azure/arm-resources-subscriptions";
2
+ import type * as vscode from 'vscode';
3
3
  export interface AzureTenant extends TenantIdDescription {
4
4
  account: vscode.AuthenticationSessionAccountInformation;
5
5
  }
@@ -1,19 +1,26 @@
1
1
  import * as vscode from 'vscode';
2
2
  import { AzureSubscription, SubscriptionId, TenantId } from './AzureSubscription';
3
- import { AzureSubscriptionProvider, GetSubscriptionsFilter } from './AzureSubscriptionProvider';
4
- import { AzureTenant } from './AzureTenant';
3
+ import type { AzureSubscriptionProvider, GetSubscriptionsFilter } from './AzureSubscriptionProvider';
4
+ import type { AzureTenant } from './AzureTenant';
5
5
  /**
6
6
  * A class for obtaining Azure subscription information using VSCode's built-in authentication
7
7
  * provider.
8
8
  */
9
- export declare class VSCodeAzureSubscriptionProvider extends vscode.Disposable implements AzureSubscriptionProvider {
9
+ export declare class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvider, vscode.Disposable {
10
10
  private readonly logger?;
11
+ private listenerDisposable;
11
12
  private readonly onDidSignInEmitter;
12
- private lastSignInEventFired;
13
- private suppressSignInEvents;
14
13
  private readonly onDidSignOutEmitter;
15
- private lastSignOutEventFired;
14
+ private lastEventFired;
15
+ private suppressEvents;
16
+ private priorAccounts;
17
+ private accountsRemovedPromise;
16
18
  constructor(logger?: vscode.LogOutputChannel | undefined);
19
+ dispose(): void;
20
+ onDidSignIn(callback: () => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable;
21
+ onDidSignOut(callback: () => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable;
22
+ private listenIfNeeded;
23
+ private accountsRemoved;
17
24
  /**
18
25
  * Gets a list of tenants available to the user.
19
26
  * Use {@link isSignedIn} to check if the user is signed in to a particular tenant.
@@ -62,20 +69,12 @@ export declare class VSCodeAzureSubscriptionProvider extends vscode.Disposable i
62
69
  * @returns True if the user is signed in, false otherwise.
63
70
  */
64
71
  signIn(tenantId?: string, account?: vscode.AuthenticationSessionAccountInformation): Promise<boolean>;
65
- /**
66
- * An event that is fired when the user signs in. Debounced to fire at most once every 5 seconds.
67
- */
68
- readonly onDidSignIn: vscode.Event<void>;
69
72
  /**
70
73
  * Signs the user out
71
74
  *
72
75
  * @deprecated Not currently supported by VS Code auth providers
73
76
  */
74
77
  signOut(): Promise<void>;
75
- /**
76
- * An event that is fired when the user signs out. Debounced to fire at most once every 5 seconds.
77
- */
78
- readonly onDidSignOut: vscode.Event<void>;
79
78
  /**
80
79
  * Gets the tenant filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
81
80
  * override the settings with a custom filter, implement a child class with `getSubscriptionFilters()`
@@ -7,42 +7,92 @@ import { getSessionFromVSCode } from './getSessionFromVSCode';
7
7
  import { NotSignedInError } from './NotSignedInError';
8
8
  import { getConfiguredAuthProviderId, getConfiguredAzureEnv } from './utils/configuredAzureEnv';
9
9
  import { isAuthenticationWwwAuthenticateRequest } from './utils/isAuthenticationWwwAuthenticateRequest';
10
+ import { isGetSubscriptionsAccountFilter, isGetSubscriptionsTenantFilter } from './utils/isGetSubscriptionsFilter';
10
11
  const EventDebounce = 5 * 1000; // 5 seconds
12
+ let armSubs;
11
13
  /**
12
14
  * A class for obtaining Azure subscription information using VSCode's built-in authentication
13
15
  * provider.
14
16
  */
15
- export class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
17
+ export class VSCodeAzureSubscriptionProvider {
16
18
  logger;
19
+ listenerDisposable;
17
20
  onDidSignInEmitter = new vscode.EventEmitter();
18
- lastSignInEventFired = 0;
19
- suppressSignInEvents = false;
20
21
  onDidSignOutEmitter = new vscode.EventEmitter();
21
- lastSignOutEventFired = 0;
22
+ lastEventFired = 0;
23
+ suppressEvents = false;
24
+ priorAccounts;
25
+ accountsRemovedPromise;
22
26
  // So that customers can easily share logs, try to only log PII using trace level
23
27
  constructor(logger) {
24
- const disposable = vscode.authentication.onDidChangeSessions(async (e) => {
28
+ this.logger = logger;
29
+ void this.accountsRemoved(); // Initialize priorAccounts
30
+ }
31
+ dispose() {
32
+ this.listenerDisposable?.dispose();
33
+ this.onDidSignInEmitter.dispose();
34
+ this.onDidSignOutEmitter.dispose();
35
+ }
36
+ onDidSignIn(callback, thisArg, disposables) {
37
+ this.listenIfNeeded();
38
+ return this.onDidSignInEmitter.event(callback, thisArg, disposables);
39
+ }
40
+ onDidSignOut(callback, thisArg, disposables) {
41
+ this.listenIfNeeded();
42
+ return this.onDidSignOutEmitter.event(callback, thisArg, disposables);
43
+ }
44
+ listenIfNeeded() {
45
+ if (this.listenerDisposable) {
46
+ return;
47
+ }
48
+ this.listenerDisposable = vscode.authentication.onDidChangeSessions(async (e) => {
25
49
  // Ignore any sign in that isn't for the configured auth provider
26
50
  if (e.provider.id !== getConfiguredAuthProviderId()) {
27
51
  return;
28
52
  }
29
- if (await this.isSignedIn()) {
30
- if (!this.suppressSignInEvents && Date.now() > this.lastSignInEventFired + EventDebounce) {
31
- this.lastSignInEventFired = Date.now();
32
- this.onDidSignInEmitter.fire();
33
- }
53
+ if (this.suppressEvents || Date.now() < this.lastEventFired + EventDebounce) {
54
+ return;
55
+ }
56
+ this.lastEventFired = Date.now();
57
+ if (!await this.accountsRemoved()) {
58
+ this.logger?.debug('auth: Firing onDidSignIn event');
59
+ this.onDidSignInEmitter.fire();
34
60
  }
35
- else if (Date.now() > this.lastSignOutEventFired + EventDebounce) {
36
- this.lastSignOutEventFired = Date.now();
61
+ else {
62
+ this.logger?.debug('auth: Firing onDidSignOut event');
37
63
  this.onDidSignOutEmitter.fire();
38
64
  }
39
65
  });
40
- super(() => {
41
- this.onDidSignInEmitter.dispose();
42
- this.onDidSignOutEmitter.dispose();
43
- disposable.dispose();
44
- });
45
- this.logger = logger;
66
+ }
67
+ async accountsRemoved() {
68
+ // If there's already an ongoing accountsRemoved operation, return its result
69
+ if (this.accountsRemovedPromise) {
70
+ return this.accountsRemovedPromise;
71
+ }
72
+ // Create a new promise for this operation
73
+ this.accountsRemovedPromise = (async () => {
74
+ try {
75
+ this.suppressEvents = true;
76
+ const currentAccounts = Array.from(await vscode.authentication.getAccounts(getConfiguredAuthProviderId()));
77
+ const priorAccountCount = this.priorAccounts?.length ?? 0;
78
+ this.priorAccounts = currentAccounts;
79
+ // The only way a sign out happens is if an account is removed entirely from the list of accounts
80
+ if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) {
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ finally {
86
+ this.suppressEvents = false;
87
+ }
88
+ })();
89
+ try {
90
+ return await this.accountsRemovedPromise;
91
+ }
92
+ finally {
93
+ // Clear the promise when done so future calls can proceed
94
+ this.accountsRemovedPromise = undefined;
95
+ }
46
96
  }
47
97
  /**
48
98
  * Gets a list of tenants available to the user.
@@ -88,24 +138,28 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
88
138
  async getSubscriptions(filter = true) {
89
139
  this.logger?.debug('auth: Loading subscriptions...');
90
140
  const startTime = Date.now();
91
- const configuredTenantFilter = await this.getTenantFilters();
92
- const tenantIdsToFilterBy =
93
- // Only filter by the tenant ID option if it is provided
94
- (typeof filter === 'object' && filter.tenantId ? [filter.tenantId] :
141
+ let tenantIdsToFilterBy;
142
+ if (isGetSubscriptionsTenantFilter(filter)) {
143
+ // Only filter by the tenant ID option if it is provided
144
+ tenantIdsToFilterBy = [filter.tenantId];
145
+ }
146
+ else if (filter === true) {
95
147
  // Only filter by the configured filter if `filter` is true AND there are tenants in the configured filter
96
- filter === true && configuredTenantFilter.length > 0 ? configuredTenantFilter :
97
- undefined);
148
+ const configuredTenantFilter = await this.getTenantFilters();
149
+ if (configuredTenantFilter.length > 0) {
150
+ tenantIdsToFilterBy = configuredTenantFilter;
151
+ }
152
+ }
98
153
  const allSubscriptions = [];
99
154
  let accountCount; // only used for logging
100
155
  try {
101
- this.suppressSignInEvents = true;
156
+ this.suppressEvents = true;
102
157
  // Get the list of tenants from each account (filtered or all)
103
- const accounts = typeof filter === 'object' && filter.account ? [filter.account] : await vscode.authentication.getAccounts(getConfiguredAuthProviderId());
158
+ const accounts = isGetSubscriptionsAccountFilter(filter) ? [filter.account] : await vscode.authentication.getAccounts(getConfiguredAuthProviderId());
104
159
  accountCount = accounts.length;
105
160
  for (const account of accounts) {
106
- for (const tenant of await this.getTenants(account)) {
107
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
108
- const tenantId = tenant.tenantId;
161
+ const tenantIds = isGetSubscriptionsTenantFilter(filter) ? [filter.tenantId] : (await this.getTenants(account)).map(t => t.tenantId);
162
+ for (const tenantId of tenantIds) {
109
163
  if (tenantIdsToFilterBy?.includes(tenantId) === false) {
110
164
  continue;
111
165
  }
@@ -117,12 +171,13 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
117
171
  }
118
172
  }
119
173
  finally {
120
- this.suppressSignInEvents = false;
174
+ this.suppressEvents = false;
121
175
  }
122
176
  // It's possible that by listing subscriptions in all tenants and the "home" tenant there could be duplicate subscriptions
123
177
  // Thus, we remove duplicate subscriptions. However, if multiple accounts have the same subscription, we keep them.
178
+ // There are also cases where the same subscription could appear in different tenants under the same account so we also need to keep those
124
179
  const subscriptionMap = new Map();
125
- allSubscriptions.forEach(sub => subscriptionMap.set(`${sub.account.id}/${sub.subscriptionId}`, sub));
180
+ allSubscriptions.forEach(sub => subscriptionMap.set(`${sub.account.id}/${sub.tenantId}/${sub.subscriptionId}`, sub));
126
181
  const uniqueSubscriptions = Array.from(subscriptionMap.values());
127
182
  const endTime = Date.now();
128
183
  this.logger?.debug(`auth: Got ${uniqueSubscriptions.length} subscriptions from ${accountCount} accounts in ${endTime - startTime}ms`);
@@ -178,18 +233,20 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
178
233
  */
179
234
  async signIn(tenantId, account) {
180
235
  this.logger?.debug(`auth: Signing in (account="${account?.label ?? 'none'}") (tenantId="${tenantId ?? 'none'}")`);
181
- const session = await getSessionFromVSCode([], tenantId, {
182
- createIfNone: true,
183
- // If no account is provided, then clear the session preference which tells VS Code to show the account picker
184
- clearSessionPreference: !account,
185
- account,
186
- });
187
- return !!session;
236
+ try {
237
+ this.suppressEvents = true;
238
+ const session = await getSessionFromVSCode([], tenantId, {
239
+ createIfNone: true,
240
+ // If no account is provided, then clear the session preference which tells VS Code to show the account picker
241
+ clearSessionPreference: !account,
242
+ account,
243
+ });
244
+ return !!session;
245
+ }
246
+ finally {
247
+ this.suppressEvents = false;
248
+ }
188
249
  }
189
- /**
190
- * An event that is fired when the user signs in. Debounced to fire at most once every 5 seconds.
191
- */
192
- onDidSignIn = this.onDidSignInEmitter.event;
193
250
  /**
194
251
  * Signs the user out
195
252
  *
@@ -198,10 +255,6 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
198
255
  signOut() {
199
256
  throw new Error(vscode.l10n.t('Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.'));
200
257
  }
201
- /**
202
- * An event that is fired when the user signs out. Debounced to fire at most once every 5 seconds.
203
- */
204
- onDidSignOut = this.onDidSignOutEmitter.event;
205
258
  /**
206
259
  * Gets the tenant filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
207
260
  * override the settings with a custom filter, implement a child class with `getSubscriptionFilters()`
@@ -272,7 +325,7 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable {
272
325
  * @returns A client, the credential used by the client, and the authentication function
273
326
  */
274
327
  async getSubscriptionClient(account, tenantId, scopes) {
275
- const armSubs = await import('@azure/arm-resources-subscriptions');
328
+ armSubs ||= await import('@azure/arm-resources-subscriptions');
276
329
  const session = await getSessionFromVSCode(scopes, tenantId, { createIfNone: false, silent: true, account });
277
330
  if (!session) {
278
331
  throw new NotSignedInError();