@gitkraken/core-gitlens 0.7.0 → 0.7.2

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 (125) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/dist/git/remotes/azure-devops.js +3 -3
  3. package/dist/git/remotes/azure-devops.js.map +1 -1
  4. package/dist/plus/integrations/authentication/configuredIntegrationService.js +9 -2
  5. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
  6. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
  7. package/dist/plus/integrations/collectionMetadata.d.ts +48 -3
  8. package/dist/plus/integrations/collectionMetadata.js +138 -8
  9. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  10. package/dist/plus/integrations/errors.d.ts +3 -1
  11. package/dist/plus/integrations/errors.js +7 -3
  12. package/dist/plus/integrations/errors.js.map +1 -1
  13. package/dist/plus/integrations/index.d.ts +3 -3
  14. package/dist/plus/integrations/index.js.map +1 -1
  15. package/dist/plus/integrations/integrationService.d.ts +35 -1
  16. package/dist/plus/integrations/integrationService.js +90 -43
  17. package/dist/plus/integrations/integrationService.js.map +1 -1
  18. package/dist/plus/integrations/lite.js +5 -4
  19. package/dist/plus/integrations/lite.js.map +1 -1
  20. package/dist/plus/integrations/manager.d.ts +68 -23
  21. package/dist/plus/integrations/models/gitHostIntegration.d.ts +31 -15
  22. package/dist/plus/integrations/models/gitHostIntegration.js +71 -22
  23. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  24. package/dist/plus/integrations/models/integration.d.ts +70 -12
  25. package/dist/plus/integrations/models/integration.js +56 -1
  26. package/dist/plus/integrations/models/integration.js.map +1 -1
  27. package/dist/plus/integrations/models/issueReads.d.ts +17 -0
  28. package/dist/plus/integrations/models/issuesIntegration.js +2 -0
  29. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  30. package/dist/plus/integrations/models/pullRequestReads.d.ts +26 -0
  31. package/dist/plus/integrations/providers/azure/azure.d.ts +66 -1
  32. package/dist/plus/integrations/providers/azure/azure.js +192 -12
  33. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  34. package/dist/plus/integrations/providers/azure/search.d.ts +144 -0
  35. package/dist/plus/integrations/providers/azure/search.js +250 -0
  36. package/dist/plus/integrations/providers/azure/search.js.map +1 -0
  37. package/dist/plus/integrations/providers/azureDevOps.d.ts +73 -36
  38. package/dist/plus/integrations/providers/azureDevOps.js +191 -122
  39. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  40. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +176 -0
  41. package/dist/plus/integrations/providers/azureDevOpsServer.js +560 -0
  42. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -0
  43. package/dist/plus/integrations/providers/bitbucket-server/discovery.d.ts +12 -0
  44. package/dist/plus/integrations/providers/bitbucket-server/discovery.js +128 -0
  45. package/dist/plus/integrations/providers/bitbucket-server/discovery.js.map +1 -0
  46. package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +10 -1
  47. package/dist/plus/integrations/providers/bitbucket-server/models.js +3 -2
  48. package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
  49. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +58 -0
  50. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +485 -0
  51. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -0
  52. package/dist/plus/integrations/providers/bitbucket-server.d.ts +61 -3
  53. package/dist/plus/integrations/providers/bitbucket-server.js +114 -20
  54. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  55. package/dist/plus/integrations/providers/bitbucket.d.ts +9 -1
  56. package/dist/plus/integrations/providers/bitbucket.js +13 -2
  57. package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
  58. package/dist/plus/integrations/providers/github.d.ts +6 -6
  59. package/dist/plus/integrations/providers/github.js +35 -34
  60. package/dist/plus/integrations/providers/github.js.map +1 -1
  61. package/dist/plus/integrations/providers/gitlab.d.ts +6 -4
  62. package/dist/plus/integrations/providers/gitlab.js +32 -28
  63. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  64. package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
  65. package/dist/plus/integrations/providers/jira-server.d.ts +9 -0
  66. package/dist/plus/integrations/providers/jira-server.js +15 -7
  67. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  68. package/dist/plus/integrations/providers/jira.d.ts +9 -0
  69. package/dist/plus/integrations/providers/jira.js +14 -2
  70. package/dist/plus/integrations/providers/jira.js.map +1 -1
  71. package/dist/plus/integrations/providers/linear.js +2 -2
  72. package/dist/plus/integrations/providers/models.d.ts +10 -1
  73. package/dist/plus/integrations/providers/models.js +86 -0
  74. package/dist/plus/integrations/providers/models.js.map +1 -1
  75. package/dist/plus/integrations/providers/providersApi.d.ts +10 -0
  76. package/dist/plus/integrations/providers/providersApi.js +18 -3
  77. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  78. package/dist/plus/integrations/providers/utils.d.ts +12 -1
  79. package/dist/plus/integrations/providers/utils.js +36 -6
  80. package/dist/plus/integrations/providers/utils.js.map +1 -1
  81. package/dist/plus/integrations/reads/broaden.js +6 -6
  82. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  83. package/dist/plus/integrations/reads/context.d.ts +1 -1
  84. package/dist/plus/integrations/reads/counts.d.ts +18 -4
  85. package/dist/plus/integrations/reads/counts.js +66 -15
  86. package/dist/plus/integrations/reads/counts.js.map +1 -1
  87. package/dist/plus/integrations/reads/cursors.js +1 -1
  88. package/dist/plus/integrations/reads/drains.d.ts +2 -1
  89. package/dist/plus/integrations/reads/drains.js +1 -1
  90. package/dist/plus/integrations/reads/filters.d.ts +1 -1
  91. package/dist/plus/integrations/reads/filters.js +17 -12
  92. package/dist/plus/integrations/reads/filters.js.map +1 -1
  93. package/dist/plus/integrations/reads/hierarchy.js +2 -2
  94. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  95. package/dist/plus/integrations/reads/hierarchy.utils.js +1 -1
  96. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  97. package/dist/plus/integrations/reads/issueTracker.js +2 -2
  98. package/dist/plus/integrations/reads/issues.js +2 -2
  99. package/dist/plus/integrations/reads/paging.js +1 -1
  100. package/dist/plus/integrations/reads/resolveRepository.d.ts +12 -11
  101. package/dist/plus/integrations/reads/resolveRepository.js +110 -19
  102. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  103. package/dist/plus/integrations/reads/searchIssues.js +4 -2
  104. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  105. package/dist/plus/integrations/reads/searchPullRequests.js +9 -3
  106. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
  107. package/dist/plus/integrations/reads/sweeps.js +1 -1
  108. package/dist/plus/integrations/reads/trackerIssue.d.ts +9 -5
  109. package/dist/plus/integrations/reads/trackerIssue.js +15 -9
  110. package/dist/plus/integrations/reads/trackerIssue.js.map +1 -1
  111. package/dist/plus/integrations/reads/warnings.d.ts +6 -2
  112. package/dist/plus/integrations/reads/warnings.js +15 -19
  113. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  114. package/dist/plus/integrations/results.d.ts +84 -6
  115. package/dist/plus/integrations/results.js +9 -2
  116. package/dist/plus/integrations/results.js.map +1 -1
  117. package/dist/plus/integrations/utils/domain.utils.d.ts +19 -0
  118. package/dist/plus/integrations/utils/domain.utils.js +36 -1
  119. package/dist/plus/integrations/utils/domain.utils.js.map +1 -1
  120. package/dist/plus/integrations/utils/integration.utils.d.ts +1 -0
  121. package/dist/plus/integrations/utils/integration.utils.js +12 -1
  122. package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
  123. package/docs/integrations.md +263 -74
  124. package/docs/kepler-read-api-parity.md +19 -6
  125. package/package.json +1 -1
@@ -3,22 +3,25 @@ import { Emitter, Event } from "../../../utils/event.js";
3
3
  import { ResourceDescriptor } from "../../../git/models/resourceDescriptor.js";
4
4
  import { AutolinkReference, DynamicAutolinkReference } from "../../../git/models/autolink.js";
5
5
  import { IssueOrPullRequest, IssueOrPullRequestType } from "../../../git/models/issueOrPullRequest.js";
6
- import { Issue, IssueShape } from "../../../git/models/issue.js";
6
+ import { Issue as Issue$1, IssueShape } from "../../../git/models/issue.js";
7
7
  import { PullRequest } from "../../../git/models/pullRequest.js";
8
8
  import { ScopedLogger } from "../../../utils/logger.scoped.js";
9
- import { Account } from "../../../git/models/author.js";
9
+ import { Account as Account$1 } from "../../../git/models/author.js";
10
10
  import { ProviderAuthenticationSession } from "../authentication/models.js";
11
11
  import { GitCloudHostIntegrationId, IntegrationIds, IssuesCloudHostIntegrationId, IssuesHostIntegrationIds } from "../constants.js";
12
12
  import { Sources } from "../telemetry.js";
13
+ import { ProviderWarningCause, ProviderWarningScope } from "../results.js";
14
+ import { ProviderRefusal } from "../collectionMetadata.js";
15
+ import { ProviderPullRequestCount, ProviderPullRequestSearchPage } from "./pullRequestReads.js";
13
16
  import { ProvidersApi } from "../providers/providersApi.js";
14
- import { AccountWideIssuesResult, ProviderIssueSearchPage, SearchMyIssuesOptions } from "./issueReads.js";
17
+ import { AccountWideIssuesResult, ProviderIssueSearchPage, ProviderSearchCount, SearchMyIssuesOptions } from "./issueReads.js";
15
18
  import { GitHostIntegration } from "./gitHostIntegration.js";
16
19
  import { IssuesIntegration } from "./issuesIntegration.js";
17
- import { ProviderPullRequestSearchPage } from "./pullRequestReads.js";
18
20
  import { IntegrationConnectionChangeEvent } from "../integrationService.js";
19
21
  import { IntegrationAuthenticationService } from "../authentication/integrationAuthenticationService.js";
20
22
  import { IntegrationAuthenticationProviderDescriptor, IntegrationAuthenticationSessionDescriptor } from "../authentication/integrationAuthenticationProvider.js";
21
23
  import { IntegrationServiceContext } from "../context.js";
24
+ import { CollectionMetadata } from "@gitkraken/provider-apis";
22
25
  //#region ../plus/integrations/src/models/integration.d.ts
23
26
  export type Integration = GitHostIntegration | IssuesIntegration;
24
27
  export type IntegrationById<T extends IntegrationIds> = T extends IssuesHostIntegrationIds ? IssuesIntegration : GitHostIntegration;
@@ -130,6 +133,61 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
130
133
  private refreshAfterSwitch;
131
134
  private skippedNonCloudReported;
132
135
  syncCloudConnection(state: 'connected' | 'disconnected', forceSync: boolean): Promise<void>;
136
+ /**
137
+ * Proves the credential with an uncached check, rejecting with the provider's `AuthenticationError` when it is
138
+ * refused, and with anything else when the check proves nothing. A `403` proves nothing either: the credential
139
+ * authenticated and was only denied the check's request.
140
+ *
141
+ * Implemented by the providers whose reads can reach their scopes without an uncached request to the connection
142
+ * first: discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud), or an SDK fan-out across
143
+ * the requested repositories (Bitbucket Data Center). There, a dead token comes back as scoped refusals with no
144
+ * failure of the connection: the same shape as one scope refusing a sound credential.
145
+ * {@link confirmScopedAuthFailures} uses this to tell the two apart.
146
+ */
147
+ protected validateCredential?(session: ProviderAuthenticationSession): Promise<void>;
148
+ /**
149
+ * Credentials that passed {@link validateCredential} within the last minute. A probe already in flight is
150
+ * shared rather than repeated, and a refused one is not kept. Cleared whenever a credential is refused (see
151
+ * {@link handleProviderException}), so a pass is never vouched for past a refusal this instance has seen.
152
+ */
153
+ private readonly _validatedCredentials;
154
+ /**
155
+ * Whether a scope's refusal is the credential's own, by the provider's account of it: e.g. a token missing the
156
+ * OAuth scopes the read needs. The probe cannot see that, because it proves the token authenticates, not that
157
+ * it is authorized. So {@link confirmScopedAuthFailures} publishes one for the connection, unscoped, which asks
158
+ * for the reconnect (consenting to the scopes again) that fixes it, and keeps the results of the scopes that
159
+ * answered.
160
+ */
161
+ protected isCredentialRefusal?(refusal: ProviderRefusal): boolean;
162
+ /**
163
+ * Names why a confirmed credential was refused by one scope, from what the refusal said (see
164
+ * `ProviderWarning.cause`). Called only by {@link confirmScopedAuthFailures}, once the credential passed: the
165
+ * refusals this names are indistinguishable from a dead credential until then.
166
+ */
167
+ protected describeRefusal?(session: ProviderAuthenticationSession, refusal: ProviderRefusal, scope: ProviderWarningScope): ProviderWarningCause | undefined;
168
+ /**
169
+ * Settles what a read's only-scoped authentication failures mean, when the provider can tell (see
170
+ * {@link validateCredential}).
171
+ *
172
+ * A refused credential is thrown: the caller's `catch` then fails the read as a whole, exactly as it does when
173
+ * discovery is not cached and the first request is the one refused. Without this, a token revoked after
174
+ * discovery would publish scoped `auth` warnings, which a consumer must not answer with a reconnect (see
175
+ * `ProviderWarning.scope`), next to results served from the cache, for as long as the token stays stored.
176
+ *
177
+ * A confirmed credential makes each refusal the scope's own, so its cause is named through
178
+ * {@link describeRefusal}. A probe that fails for another reason proves nothing either way and leaves the read
179
+ * as it was.
180
+ *
181
+ * A refusal the provider pins on the credential itself (see {@link isCredentialRefusal}) is published for the
182
+ * connection instead, without a probe.
183
+ *
184
+ * A token that passed within the last minute is not probed again, so a scope that keeps refusing it costs one
185
+ * probe a minute, and a revocation right after a probe can take up to a minute to surface: until then its
186
+ * refusals stay scoped, and may be named. A probe that proved nothing is not remembered, so the next read probes
187
+ * again, and a credential no probe can confirm (e.g. a Bitbucket Data Center project access token) is probed on
188
+ * every read that has scoped refusals.
189
+ */
190
+ protected confirmScopedAuthFailures(session: ProviderAuthenticationSession, metadata: CollectionMetadata | undefined): Promise<void>;
133
191
  protected handleProviderException(syncReqUsecase: SyncReqUsecase, ex: Error, options?: {
134
192
  scope?: ScopedLogger | undefined;
135
193
  silent?: boolean;
@@ -176,30 +234,30 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
176
234
  getIssue(resource: T, id: string, options?: {
177
235
  connectionId?: string;
178
236
  expiryOverride?: boolean | number;
179
- }): Promise<Issue | undefined>;
237
+ }): Promise<Issue$1 | undefined>;
180
238
  getIssueResult(resource: T, id: string, options?: {
181
239
  connectionId?: string;
182
240
  expiryOverride?: boolean | number;
183
- }): Promise<IntegrationResult<Issue | undefined>>;
241
+ }): Promise<IntegrationResult<Issue$1 | undefined>>;
184
242
  protected getIssueResultCore(resource: ResourceDescriptor, id: string, options: {
185
243
  connectionId?: string;
186
244
  expiryOverride?: boolean | number;
187
- } | undefined, getProviderIssue: (session: ProviderAuthenticationSession) => Promise<Issue | undefined>): Promise<IntegrationResult<Issue | undefined>>;
188
- protected abstract getProviderIssue(session: ProviderAuthenticationSession, resource: T, id: string): Promise<Issue | undefined>;
245
+ } | undefined, getProviderIssue: (session: ProviderAuthenticationSession) => Promise<Issue$1 | undefined>): Promise<IntegrationResult<Issue$1 | undefined>>;
246
+ protected abstract getProviderIssue(session: ProviderAuthenticationSession, resource: T, id: string): Promise<Issue$1 | undefined>;
189
247
  getCurrentAccount(options?: {
190
248
  avatarSize?: number;
191
249
  connectionId?: string;
192
250
  expiryOverride?: boolean | number;
193
- }): Promise<Account | undefined>;
251
+ }): Promise<Account$1 | undefined>;
194
252
  protected getProviderCurrentAccount?(session: ProviderAuthenticationSession, options?: {
195
253
  avatarSize?: number;
196
- }): Promise<Account | undefined>;
254
+ }): Promise<Account$1 | undefined>;
197
255
  /**
198
256
  * Resolves the account for a specific session/token — including connections other than the current
199
257
  * primary (multi-account) — using this integration's provider API base URL and auth type. Returns
200
258
  * undefined when the provider doesn't support account lookup. Uncached (callers cache per connection).
201
259
  */
202
- getProviderAccountForSession(session: ProviderAuthenticationSession): Promise<Account | undefined>;
260
+ getProviderAccountForSession(session: ProviderAuthenticationSession): Promise<Account$1 | undefined>;
203
261
  getPullRequest(resource: T, id: string, options?: {
204
262
  expiryOverride?: boolean | number;
205
263
  throwOnError?: boolean;
@@ -207,5 +265,5 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
207
265
  protected getProviderPullRequest?(session: ProviderAuthenticationSession, resource: T, id: string): Promise<PullRequest | undefined>;
208
266
  }
209
267
  //#endregion
210
- export type { AccountWideIssuesResult, ProviderIssueSearchPage, ProviderPullRequestSearchPage, SearchMyIssuesOptions };
268
+ export type { AccountWideIssuesResult, ProviderIssueSearchPage, ProviderPullRequestCount, ProviderPullRequestSearchPage, ProviderSearchCount, SearchMyIssuesOptions };
211
269
  //# sourceMappingURL=integration.d.ts.map
@@ -2,6 +2,7 @@ import { isCancellationError } from "../../../utils/cancellation.js";
2
2
  import { Logger as _logger } from "../../../utils/logger.js";
3
3
  import { getScopedLogger } from "../../../utils/logger.scoped.js";
4
4
  import { debug, trace } from "../../../utils/decorators/log.js";
5
+ import { PromiseCache } from "../../../utils/promiseCache.js";
5
6
  import __decorate from "../../../_virtual/@oxc-project+runtime/helpers/esm/decorate.js";
6
7
  import { AuthenticationError, RequestClientError } from "../../../git/errors.js";
7
8
  import { Emitter } from "../../../utils/event.js";
@@ -9,9 +10,10 @@ import { fnv1aHash64 } from "../../../utils/hash.js";
9
10
  import { gate } from "../../../utils/decorators/gate.js";
10
11
  import { toError } from "../errors.js";
11
12
  import "../constants.js";
13
+ import { attributeScopedAuthFailures, hasOnlyScopedAuthFailures, markCredentialRefusals } from "../collectionMetadata.js";
14
+ import { areDomainsOnSameHost } from "../utils/domain.utils.js";
12
15
  import { isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
13
16
  import { providersMetadata } from "../providers/models.js";
14
- import { areDomainsOnSameHost } from "../utils/domain.utils.js";
15
17
  import { RejectedTokenTracker } from "../authentication/rejectedTokenTracker.js";
16
18
  import { getCachedIssue } from "./issueCache.js";
17
19
  //#region ../plus/integrations/src/models/integration.ts
@@ -284,8 +286,60 @@ var IntegrationBase = class IntegrationBase {
284
286
  case "disconnected": await this.disconnect({ silent: true });
285
287
  }
286
288
  }
289
+ /**
290
+ * Credentials that passed {@link validateCredential} within the last minute. A probe already in flight is
291
+ * shared rather than repeated, and a refused one is not kept. Cleared whenever a credential is refused (see
292
+ * {@link handleProviderException}), so a pass is never vouched for past a refusal this instance has seen.
293
+ */
294
+ _validatedCredentials = new PromiseCache({
295
+ createTTL: 6e4,
296
+ capacity: 10
297
+ });
298
+ /**
299
+ * Settles what a read's only-scoped authentication failures mean, when the provider can tell (see
300
+ * {@link validateCredential}).
301
+ *
302
+ * A refused credential is thrown: the caller's `catch` then fails the read as a whole, exactly as it does when
303
+ * discovery is not cached and the first request is the one refused. Without this, a token revoked after
304
+ * discovery would publish scoped `auth` warnings, which a consumer must not answer with a reconnect (see
305
+ * `ProviderWarning.scope`), next to results served from the cache, for as long as the token stays stored.
306
+ *
307
+ * A confirmed credential makes each refusal the scope's own, so its cause is named through
308
+ * {@link describeRefusal}. A probe that fails for another reason proves nothing either way and leaves the read
309
+ * as it was.
310
+ *
311
+ * A refusal the provider pins on the credential itself (see {@link isCredentialRefusal}) is published for the
312
+ * connection instead, without a probe.
313
+ *
314
+ * A token that passed within the last minute is not probed again, so a scope that keeps refusing it costs one
315
+ * probe a minute, and a revocation right after a probe can take up to a minute to surface: until then its
316
+ * refusals stay scoped, and may be named. A probe that proved nothing is not remembered, so the next read probes
317
+ * again, and a credential no probe can confirm (e.g. a Bitbucket Data Center project access token) is probed on
318
+ * every read that has scoped refusals.
319
+ */
320
+ async confirmScopedAuthFailures(session, metadata) {
321
+ if (!hasOnlyScopedAuthFailures(metadata)) return;
322
+ const isCredentialRefusal = this.isCredentialRefusal?.bind(this);
323
+ if (isCredentialRefusal != null && markCredentialRefusals(metadata, isCredentialRefusal)) return;
324
+ const validateCredential = this.validateCredential?.bind(this);
325
+ if (validateCredential == null) return;
326
+ const key = [
327
+ session.domain,
328
+ session.baseUrl ?? "",
329
+ session.accessToken
330
+ ].join("\n");
331
+ try {
332
+ await this._validatedCredentials.getOrCreate(key, () => validateCredential(session));
333
+ } catch (ex) {
334
+ if (ex instanceof AuthenticationError && ex.reason !== 3) throw ex;
335
+ return;
336
+ }
337
+ const describeRefusal = this.describeRefusal?.bind(this);
338
+ if (describeRefusal != null) attributeScopedAuthFailures(metadata, (refusal, scope) => describeRefusal(session, refusal, scope));
339
+ }
287
340
  handleProviderException(syncReqUsecase, ex, options) {
288
341
  if (isCancellationError(ex)) return;
342
+ if (ex instanceof AuthenticationError) this._validatedCredentials.clear();
289
343
  if (options?.scope != null) options.scope.error(ex);
290
344
  else _logger.error(ex);
291
345
  if (ex instanceof AuthenticationError && options?.connectionId) {
@@ -438,6 +492,7 @@ var IntegrationBase = class IntegrationBase {
438
492
  if (session == null) return void 0;
439
493
  try {
440
494
  const result = await this.searchProviderMyIssuesWithTruncation(session, resources != null ? Array.isArray(resources) ? resources : [resources] : void 0, cancellation, options);
495
+ await this.confirmScopedAuthFailures(session, result?.metadata);
441
496
  this.resetRequestExceptionCount("searchMyIssues");
442
497
  return { value: result };
443
498
  } catch (ex) {
@@ -1 +1 @@
1
- {"version":3,"file":"integration.js","names":[],"sources":["../../../../../plus/integrations/src/models/integration.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { PullRequest } from '@gitlens/git/models/pullRequest.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isCancellationError } from '@gitlens/utils/cancellation.js';\nimport { gate } from '@gitlens/utils/decorators/gate.js';\nimport { debug, trace } from '@gitlens/utils/decorators/log.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { fnv1aHash64 } from '@gitlens/utils/hash.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { ScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport { getScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport type {\n\tIntegrationAuthenticationProviderDescriptor,\n\tIntegrationAuthenticationSessionDescriptor,\n} from '../authentication/integrationAuthenticationProvider.js';\nimport type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { RejectedTokenTracker } from '../authentication/rejectedTokenTracker.js';\nimport type { IntegrationIds, IssuesCloudHostIntegrationId, IssuesHostIntegrationIds } from '../constants.js';\nimport { GitCloudHostIntegrationId } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { AuthenticationError, RequestClientError, toError } from '../errors.js';\nimport type { IntegrationConnectionChangeEvent } from '../integrationService.js';\nimport { providersMetadata } from '../providers/models.js';\nimport type { ProvidersApi } from '../providers/providersApi.js';\nimport type { Sources } from '../telemetry.js';\nimport { areDomainsOnSameHost } from '../utils/domain.utils.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { GitHostIntegration } from './gitHostIntegration.js';\nimport { getCachedIssue } from './issueCache.js';\nimport type { AccountWideIssuesResult, SearchMyIssuesOptions } from './issueReads.js';\nimport type { IssuesIntegration } from './issuesIntegration.js';\n\nexport type Integration = GitHostIntegration | IssuesIntegration;\nexport type IntegrationById<T extends IntegrationIds> = T extends IssuesHostIntegrationIds\n\t? IssuesIntegration\n\t: GitHostIntegration;\nexport type IntegrationType = 'git' | 'issues';\n\n// The issue-read contracts live in their own module (pure data, and their relationship to each other is the\n// point of reading them together); re-exported here so the providers that implement these reads keep one import.\nexport type { AccountWideIssuesResult, ProviderIssueSearchPage, SearchMyIssuesOptions } from './issueReads.js';\nexport type { ProviderPullRequestSearchPage } from './pullRequestReads.js';\n\n// Keep this in step with `isSelfManagedHostIntegrationId`: it is the type-level twin of that predicate, and a\n// disagreement compiles cleanly while the runtime writes a domain-keyed key the type says is unkeyed (so the\n// local-disconnect flag is written under one key and read under another).\nexport type IntegrationKey<T extends IntegrationIds = IntegrationIds> = T extends\n\t| GitCloudHostIntegrationId\n\t| IssuesCloudHostIntegrationId\n\t? `${T}`\n\t: `${T}:${string}`;\n\nexport type IntegrationConnectedKey<T extends IntegrationIds = IntegrationIds> = `connected:${IntegrationKey<T>}`;\n\nexport type IntegrationResult<T> =\n\t| { value: T; duration?: number; error?: Error }\n\t| { error: Error; duration?: number; value?: never }\n\t| undefined;\n\ntype SyncReqUsecase = Exclude<\n\t| 'getAccountForCommit'\n\t| 'getAccountForEmail'\n\t| 'getAccountForResource'\n\t| 'getCurrentAccount'\n\t| 'getDefaultBranch'\n\t| 'getIssue'\n\t| 'getIssueOrPullRequest'\n\t| 'getIssuesBatch'\n\t| 'getIssuesForProject'\n\t| 'getIssuesForRepos'\n\t| 'getMyPullRequestsForUser'\n\t| 'getOrganizationsForUser'\n\t| 'getProjectsForOrg'\n\t| 'getProjectsForResources'\n\t| 'getPullRequest'\n\t| 'getRepositoriesForOrg'\n\t| 'getRepositoriesForUser'\n\t| 'getPullRequestForBranch'\n\t| 'getPullRequestForCommit'\n\t| 'getPullRequestsForRepos'\n\t| 'getRepositoryMetadata'\n\t| 'getResourcesForUser'\n\t| 'getSshSigningKeysForEmails'\n\t| 'countIssues'\n\t| 'countPullRequests'\n\t| 'mergePullRequest'\n\t| 'searchIssuesPage'\n\t| 'searchPullRequestsPage'\n\t| 'searchMyIssues'\n\t| 'searchMyPullRequests'\n\t| 'searchPullRequests',\n\t// excluding to show explicitly that we don't want to add 'all' key occasionally\n\t'all'\n>;\n\nexport abstract class IntegrationBase<\n\tID extends IntegrationIds = IntegrationIds,\n\tT extends ResourceDescriptor = ResourceDescriptor,\n> implements Disposable {\n\tabstract readonly type: IntegrationType;\n\n\tprivate readonly _onDidChange = new Emitter<void>();\n\tget onDidChange(): Event<void> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tconstructor(\n\t\tprotected readonly ctx: IntegrationServiceContext,\n\t\tprotected readonly authenticationService: IntegrationAuthenticationService,\n\t\tprotected readonly getProvidersApi: () => Promise<ProvidersApi>,\n\t\tprivate readonly didChangeConnection: Emitter<IntegrationConnectionChangeEvent>,\n\t) {}\n\n\tdispose(): void {\n\t\tthis._onDidChange.dispose();\n\t}\n\n\tabstract get authProvider(): IntegrationAuthenticationProviderDescriptor;\n\tabstract get id(): ID;\n\tprotected abstract get key(): IntegrationKey<ID>;\n\tabstract get name(): string;\n\tabstract get domain(): string;\n\n\tget authProviderDescriptor(): IntegrationAuthenticationSessionDescriptor {\n\t\treturn { domain: this.domain, scopes: this.authProvider.scopes };\n\t}\n\n\t/**\n\t * The `gl-provider-<key>` glicon key for this provider, which is NOT always its id.\n\t *\n\t * `providersMetadata.iconKey` is the field that says which glyph a provider draws with, and a\n\t * self-managed variant reuses its cloud family's — Jira Data Center draws with Jira's, because it is the\n\t * same product and the id only selects an icon. Returning the id instead named a glyph the registry has\n\t * no entry for, so a consumer rendering a `gl-provider-` glicon from `issue.provider.icon` requested a\n\t * glyph that does not exist.\n\t */\n\tget icon(): string {\n\t\treturn providersMetadata[this.id]?.iconKey ?? this.id;\n\t}\n\n\taccess(): Promise<boolean> {\n\t\treturn this.ctx.account.isTrialOrPaid();\n\t}\n\n\tautolinks():\n\t\t| (AutolinkReference | DynamicAutolinkReference)[]\n\t\t| Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\treturn [];\n\t}\n\n\tprivate get connectedKey(): IntegrationConnectedKey<ID> {\n\t\treturn `connected:${this.key}`;\n\t}\n\n\tget maybeConnected(): boolean | undefined {\n\t\treturn this._session === undefined ? undefined : this._session !== null;\n\t}\n\n\t/** Hash of the current session's access token. Changes on any token change (account switch or refresh). */\n\tprivate _sessionFingerprint: { session: ProviderAuthenticationSession; hash: string } | undefined;\n\tget sessionFingerprint(): string | undefined {\n\t\tif (this._session == null) return undefined;\n\n\t\tif (this._sessionFingerprint?.session !== this._session) {\n\t\t\tthis._sessionFingerprint = { session: this._session, hash: this.getSessionFingerprint(this._session) };\n\t\t}\n\t\treturn this._sessionFingerprint.hash;\n\t}\n\n\tprotected getSessionFingerprint(session: ProviderAuthenticationSession): string {\n\t\treturn fnv1aHash64(session.accessToken);\n\t}\n\n\tget connectionExpired(): boolean | undefined {\n\t\tif (this._session?.expiresAt == null) return undefined;\n\t\treturn new Date(this._session.expiresAt) < new Date();\n\t}\n\n\tprotected _session: ProviderAuthenticationSession | null | undefined;\n\tgetSession(\n\t\tsource: Sources,\n\t): ProviderAuthenticationSession | Promise<ProviderAuthenticationSession | undefined> | undefined {\n\t\tif (this._session === undefined) {\n\t\t\treturn this.ensureSession({ createIfNeeded: false, source: source });\n\t\t}\n\t\treturn this._session != null && this.isSessionForIntegrationHost(this._session) ? this._session : undefined;\n\t}\n\n\tprivate isSessionForIntegrationHost(session: ProviderAuthenticationSession): boolean {\n\t\tif (!isSelfManagedHostIntegrationId(this.id)) return true;\n\n\t\treturn areDomainsOnSameHost(this.domain, session.domain);\n\t}\n\n\t/**\n\t * Resolves the session to read as, for a per-connection (multi-account) read. When `connectionId` is\n\t * omitted this is the integration's primary session, resolved exactly like the existing read flow\n\t * (ensure-connected + refresh-if-expired). When set, it resolves THAT connection's session directly\n\t * from the auth provider — refreshing it if expired — WITHOUT disturbing the cached primary\n\t * `_session`. Returns undefined when the requested session can't be resolved (e.g. the connection is\n\t * gone or the provider isn't connected), so callers degrade to \"no results\".\n\t */\n\tprotected async resolveReadSession(\n\t\tconnectionId: string | undefined,\n\t\tscope: ScopedLogger | undefined,\n\t\tsource?: Sources,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tif (\n\t\t\tthis.ctx.config.isIntegrationsEnabled?.() === false ||\n\t\t\tthis.ctx.storage.getWorkspace(this.connectedKey) === false\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// A truthy connectionId targets a specific account; an empty string is not a real target, so it falls\n\t\t// through to the primary path below.\n\t\tif (connectionId) {\n\t\t\t// A read of this connection previously failed with an AuthenticationError, so the token in\n\t\t\t// storage is known-refused. Ask for a refresh through the GK cloud before reading again, which\n\t\t\t// is this branch's equivalent of the primary path's `refreshSessionIfExpired`. Claimed here so\n\t\t\t// the forced refresh runs at most once per rejection.\n\t\t\tconst refreshRejectedToken = this._rejectedTokens.claimRefresh(connectionId);\n\t\t\t// Degrade to \"no results\" on failure, matching the primary path (whose ensureSession/\n\t\t\t// refreshSessionIfExpired swallow errors) so read methods keep their never-throws contract.\n\t\t\ttry {\n\t\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\t\tconst session = await authProvider.getSession(\n\t\t\t\t\t{ ...this.authProviderDescriptor, connectionId: connectionId, cloud: true },\n\t\t\t\t\t{ source: source, refreshRejectedToken: refreshRejectedToken },\n\t\t\t\t);\n\t\t\t\treturn session != null && this.isSessionForIntegrationHost(session) ? session : undefined;\n\t\t\t} catch (ex) {\n\t\t\t\tscope?.error(ex);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected) return undefined;\n\n\t\tawait this.refreshSessionIfExpired(scope);\n\t\treturn this._session != null && this.isSessionForIntegrationHost(this._session) ? this._session : undefined;\n\t}\n\n\t@debug()\n\tasync connect(source: Sources): Promise<boolean> {\n\t\ttry {\n\t\t\treturn Boolean(await this.ensureSession({ createIfNeeded: true, source: source }));\n\t\t} catch (_ex) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprotected providerOnConnect?(): void | Promise<void>;\n\n\t@gate()\n\t@debug()\n\tasync disconnect(options?: { silent?: boolean; currentSessionOnly?: boolean }): Promise<void> {\n\t\tif (options?.currentSessionOnly && this._session === null) return;\n\n\t\tconst connected = this._session != null;\n\n\t\tlet signOut = !options?.currentSessionOnly;\n\n\t\tif (connected && !options?.currentSessionOnly && !options?.silent) {\n\t\t\tconst decision = await this.ctx.hooks?.onConfirmDisconnect?.({\n\t\t\t\tintegrationName: this.name,\n\t\t\t\tofferSignOut: this.authenticationService.supports(this.authProvider.id),\n\t\t\t});\n\t\t\tif (decision == null) return;\n\n\t\t\tsignOut = decision.signOut;\n\t\t}\n\n\t\tif (signOut) {\n\t\t\t// Disconnecting a provider signs out of ALL its connected accounts (multi-account), not just the\n\t\t\t// primary — otherwise secondary connections' secrets/config would be orphaned. Removing a single\n\t\t\t// account is done via IntegrationService.deleteConnection instead. Pass this instance's descriptor\n\t\t\t// so self-managed disconnects stay scoped to this host: those group every host under one provider\n\t\t\t// id, so an unscoped clear would sign the user out of unrelated hosts. deleteAllSessions derives an\n\t\t\t// undefined domain for cloud providers, so they still clear every account as intended.\n\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\t// Awaited, not fire-and-forget: a caller that awaits `disconnect()` has to be able to rely on the\n\t\t\t// secrets and descriptors actually being gone when it resumes. Left floating, the only thing that\n\t\t\t// ever made this land in time was incidental scheduling slack — `syncCloudIntegrations` used to\n\t\t\t// await each provider in turn, so a later iteration's suspension let the previous provider's delete\n\t\t\t// finish. Syncing providers concurrently removes that slack and the clear was observably still\n\t\t\t// pending when the sync returned.\n\t\t\tawait authProvider.deleteAllSessions(this.authProviderDescriptor);\n\t\t}\n\n\t\tthis.resetRequestExceptionCount('all');\n\t\tthis._session = null;\n\n\t\tif (connected) {\n\t\t\t// Don't store the disconnected flag if silently disconnecting or disconnecting this only for\n\t\t\t// this current VS Code session (will be re-connected on next restart)\n\t\t\tif (!options?.currentSessionOnly && !options?.silent) {\n\t\t\t\tvoid this.ctx.storage.storeWorkspace(this.connectedKey, false).catch();\n\t\t\t}\n\n\t\t\tthis._onDidChange.fire();\n\t\t\tif (!options?.currentSessionOnly) {\n\t\t\t\tthis.didChangeConnection?.fire({ integration: this, key: this.key, reason: 'disconnected' });\n\t\t\t}\n\t\t}\n\n\t\tawait this.providerOnDisconnect?.();\n\t}\n\n\tprotected providerOnDisconnect?(): void | Promise<void>;\n\n\t@debug()\n\tasync reauthenticate(): Promise<void> {\n\t\t// `forceNewSession` below deletes the stored secrets to reconnect. Ahead of the guard — see\n\t\t// `onStoredTokensReplaced`.\n\t\tthis.onStoredTokensReplaced();\n\n\t\tif (this._session === undefined) return;\n\n\t\tthis._session = undefined;\n\t\tvoid (await this.ensureSession({ createIfNeeded: true, forceNewSession: true }));\n\t}\n\n\trefresh(): void {\n\t\tvoid this.ensureSession({ createIfNeeded: false });\n\t}\n\n\tprivate _syncRequestsPerFailedUsecase = new Set<SyncReqUsecase>();\n\thasSessionSyncRequests(): boolean {\n\t\treturn this._syncRequestsPerFailedUsecase.size > 0;\n\t}\n\trequestSessionSyncForUsecase(syncReqUsecase: SyncReqUsecase): void {\n\t\tthis._syncRequestsPerFailedUsecase.add(syncReqUsecase);\n\t}\n\n\t/**\n\t * The per-connection counterpart of the primary session's expire-and-resync recovery: a token the\n\t * provider refused is refreshed once through the GK cloud, which exchanges the refresh token server-side\n\t * (the client never holds one). Armed by {@link handleProviderException} and consumed by\n\t * {@link resolveReadSession}; a rejection it declines belongs to {@link trackRequestException} instead.\n\t */\n\tprivate readonly _rejectedTokens = new RejectedTokenTracker();\n\n\t/**\n\t * Called by every path that replaces or removes the stored tokens — a disconnect, a forced re-sync, a\n\t * reauthentication, or the connection set changing. A rejection names a specific credential, so once that\n\t * credential is gone the rejection describes nothing and must not force a refresh (or, for a deleted\n\t * connection, outlive it).\n\t *\n\t * Deliberately NOT conditioned on `_session`. A per-connection read resolves through the auth provider and\n\t * never populates the cached primary session, so the connections this recovery exists for are exactly the\n\t * ones with no `_session` to inspect — the same blind spot the recovery itself was added to fix. Callers\n\t * that guard on `_session` therefore invoke this ahead of that guard.\n\t */\n\tprotected onStoredTokensReplaced(): void {\n\t\tthis._rejectedTokens.clear();\n\t}\n\n\tprivate static readonly requestExceptionLimit = 5;\n\tprivate requestExceptionCount = 0;\n\n\tresetRequestExceptionCount(syncReqUsecase: SyncReqUsecase | 'all'): void {\n\t\tthis.requestExceptionCount = 0;\n\t\tif (syncReqUsecase === 'all') {\n\t\t\tthis._syncRequestsPerFailedUsecase.clear();\n\t\t\t// 'all' is the whole-integration reset: a disconnect, or a re-sync that produced a new access token.\n\t\t\tthis.onStoredTokensReplaced();\n\t\t} else {\n\t\t\tthis._syncRequestsPerFailedUsecase.delete(syncReqUsecase);\n\t\t}\n\t}\n\n\t/**\n\t * Resets request exceptions without resetting the amount of syncs\n\t */\n\tsmoothifyRequestExceptionCount(): void {\n\t\t// On resync we reset exception count only to avoid infinitive syncs on failure\n\t\tthis.requestExceptionCount = 0;\n\t}\n\n\tasync reset(): Promise<void> {\n\t\tawait this.disconnect({ silent: true });\n\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\t}\n\n\t/**\n\t * Drops the in-memory session so the next access re-resolves it from storage. Used when the primary\n\t * connection changed underneath a warm integration (e.g. after `setPrimaryConnection`/\n\t * `deleteConnection`). Unlike {@link reset}/{@link disconnect}, it deletes nothing from storage.\n\t */\n\tswitchConnection(): void {\n\t\t// A connection was deleted, or a different one became primary. Ahead of the guard — see\n\t\t// `onStoredTokensReplaced`.\n\t\tthis.onStoredTokensReplaced();\n\n\t\tif (this._session === undefined) return;\n\n\t\tconst wasConnected = this._session != null;\n\t\tthis._session = undefined;\n\t\tthis._onDidChange.fire();\n\t\tvoid this.refreshAfterSwitch(wasConnected);\n\t}\n\n\tprivate async refreshAfterSwitch(wasConnected: boolean): Promise<void> {\n\t\tconst session = await this.ensureSession({ createIfNeeded: false });\n\t\tif (session != null || !wasConnected) return;\n\n\t\tthis._onDidChange.fire();\n\t\tthis.didChangeConnection?.fire({ integration: this, key: this.key, reason: 'disconnected' });\n\t\tawait this.providerOnDisconnect?.();\n\t}\n\n\tprivate skippedNonCloudReported = false;\n\t@debug()\n\tasync syncCloudConnection(state: 'connected' | 'disconnected', forceSync: boolean): Promise<void> {\n\t\tconst scope = getScopedLogger();\n\t\t// Initially the condition on `this._session.cloud` has been added here: https://github.com/gitkraken/vscode-gitlens/commit/e95e70c430bd162924cc3bd5c1e8ab90e6293449#diff-4213141a45cccaab7aa2e40028b155a87eb913b07388485831403e60ce5555e4R237\n\t\t// I'm not sure about reasons, but it seems we want to replace it with the cloud session if it's connected.\n\t\t// Gradually we'll stop having non-cloud sessions.\n\t\t// However this is needed to be tested with PATs, e.g. with a GitLab PAT.\n\t\tif (this._session?.cloud === false && state !== 'connected') {\n\t\t\tif (this.id !== GitCloudHostIntegrationId.GitHub && !this.skippedNonCloudReported) {\n\t\t\t\tthis.ctx.hooks?.session?.onRefreshSkipped?.({\n\t\t\t\t\tid: this.id,\n\t\t\t\t\treason: 'skip-non-cloud',\n\t\t\t\t\tcloud: false,\n\t\t\t\t});\n\t\t\t\tthis.skippedNonCloudReported = true;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (state) {\n\t\t\tcase 'connected': {\n\t\t\t\tconst oldSession = this._session;\n\t\t\t\tlet resyncing = false;\n\t\t\t\tif (forceSync) {\n\t\t\t\t\t// Reset our stored session so that we get a new one from the cloud\n\t\t\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\t\t\tawait authProvider.deleteSession(this.authProviderDescriptor);\n\t\t\t\t\t// The stored token was just deleted. Not left to the token-changed check below, which needs\n\t\t\t\t\t// an `oldSession` — see `onStoredTokensReplaced`.\n\t\t\t\t\tthis.onStoredTokensReplaced();\n\t\t\t\t\t// Reset the session and clear our \"stay disconnected\" flag\n\t\t\t\t\tthis._session = undefined;\n\t\t\t\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\t\t\t\t\tresyncing = true;\n\t\t\t\t} else {\n\t\t\t\t\t// Only sync if we're not connected and not disabled and don't have pending errors\n\t\t\t\t\tif (\n\t\t\t\t\t\tthis._session != null ||\n\t\t\t\t\t\tthis.requestExceptionCount > 0 ||\n\t\t\t\t\t\tthis.ctx.storage.getWorkspace(this.connectedKey) === false\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tforceSync = true;\n\t\t\t\t}\n\n\t\t\t\t// sync option, rather than createIfNeeded, makes sure we don't call connectCloudIntegrations and open a gkdev window\n\t\t\t\t// if there was no session or some problem fetching/refreshing the existing session from the cloud api\n\t\t\t\tlet newSession: ProviderAuthenticationSession | undefined;\n\t\t\t\tlet refetchFailed = false;\n\t\t\t\ttry {\n\t\t\t\t\tnewSession = await this.ensureSession({ sync: forceSync });\n\t\t\t\t} catch (ex) {\n\t\t\t\t\t// Not evidence the connection is gone (#5569). Also leaves `_session` as-is rather than\n\t\t\t\t\t// latching null, so the next access can re-resolve it.\n\t\t\t\t\tif (!isCancellationError(ex)) {\n\t\t\t\t\t\tscope?.error(ex);\n\t\t\t\t\t}\n\t\t\t\t\trefetchFailed = true;\n\t\t\t\t}\n\n\t\t\t\tif (oldSession && newSession && newSession.accessToken !== oldSession.accessToken) {\n\t\t\t\t\tthis.resetRequestExceptionCount('all');\n\t\t\t\t}\n\n\t\t\t\t// The forced re-sync above deleted the cloud secret but kept the descriptor to avoid UI churn.\n\t\t\t\t// Drop it only when the replacement fetch came back definitively empty, so a connection whose\n\t\t\t\t// token is really gone is cleanly disconnected (#5497) while a healthy one survives a blip (#5569).\n\t\t\t\tif (resyncing && newSession == null && !refetchFailed) {\n\t\t\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\t\t\tawait authProvider.deleteSession(this.authProviderDescriptor, { preserveConfigured: false });\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'disconnected':\n\t\t\t\tawait this.disconnect({ silent: true });\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprotected handleProviderException(\n\t\tsyncReqUsecase: SyncReqUsecase,\n\t\tex: Error,\n\t\toptions?: { scope?: ScopedLogger | undefined; silent?: boolean; connectionId?: string },\n\t): void {\n\t\tif (isCancellationError(ex)) return;\n\n\t\tif (options?.scope != null) {\n\t\t\toptions.scope.error(ex);\n\t\t} else {\n\t\t\tLogger.error(ex);\n\t\t}\n\n\t\t// A per-connection (multi-account) read resolved its session through `resolveReadSession`'s\n\t\t// `connectionId` branch, which deliberately never touches the cached primary `_session`. So the\n\t\t// primary-session recovery below cannot apply to it: expiring `_session` would mark a session this\n\t\t// read never used, while the rejected connection kept its stored token and re-sent it on every\n\t\t// later read — a token the provider has already refused (expired scopes, a revoked grant, an\n\t\t// uninstalled app) is not self-healing, so the read failed identically until the user reconnected\n\t\t// by hand. Record the rejection against the connection instead; `resolveReadSession` consumes it\n\t\t// and forces the cloud `/refresh` on the next read of that connection. When there is nothing to\n\t\t// recover, fall through to the shared failure budget, which disconnects after\n\t\t// `requestExceptionLimit` and surfaces the reconnect prompt.\n\t\tif (ex instanceof AuthenticationError && options?.connectionId) {\n\t\t\tif (!this._rejectedTokens.recordRejection(options.connectionId)) {\n\t\t\t\tthis.trackRequestException(options);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (ex instanceof AuthenticationError && this._session?.cloud) {\n\t\t\tif (!this.hasSessionSyncRequests()) {\n\t\t\t\tthis.requestSessionSyncForUsecase(syncReqUsecase);\n\t\t\t\tthis._session = {\n\t\t\t\t\t...this._session,\n\t\t\t\t\texpiresAt: new Date(Date.now() - 1),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tthis.trackRequestException(options);\n\t\t\t}\n\t\t} else if (ex instanceof AuthenticationError || ex instanceof RequestClientError) {\n\t\t\tthis.trackRequestException(options);\n\t\t}\n\t}\n\n\tprivate missingExpirityReported = false;\n\t@gate()\n\tprotected async refreshSessionIfExpired(scope?: ScopedLogger): Promise<void> {\n\t\tif (this._session?.expiresAt != null && this._session.expiresAt < new Date()) {\n\t\t\t// The current session is expired, so get the latest from the cloud and refresh if needed\n\t\t\ttry {\n\t\t\t\tawait this.syncCloudConnection('connected', true);\n\t\t\t} catch (ex) {\n\t\t\t\tscope?.error(ex);\n\t\t\t}\n\t\t} else if (\n\t\t\tthis._session?.expiresAt == null &&\n\t\t\tthis.id !== GitCloudHostIntegrationId.GitHub &&\n\t\t\t!this.missingExpirityReported\n\t\t) {\n\t\t\tthis.ctx.hooks?.session?.onRefreshSkipped?.({\n\t\t\t\tid: this.id,\n\t\t\t\treason: 'missing-expiry',\n\t\t\t\tcloud: this._session?.cloud,\n\t\t\t});\n\t\t\tthis.missingExpirityReported = true;\n\t\t}\n\t}\n\n\t@trace()\n\ttrackRequestException(options?: { silent?: boolean }): void {\n\t\tthis.requestExceptionCount++;\n\n\t\tif (this.requestExceptionCount >= IntegrationBase.requestExceptionLimit && this._session !== null) {\n\t\t\tif (!options?.silent) {\n\t\t\t\tthis.ctx.hooks?.ui?.onDisconnectedAfterTooManyFailures?.(this.name);\n\t\t\t}\n\t\t\tvoid this.disconnect({ currentSessionOnly: true });\n\t\t}\n\t}\n\n\t@gate()\n\t@trace({ exit: true })\n\tasync isConnected(): Promise<boolean> {\n\t\treturn (await this.getSession('integrations')) != null;\n\t}\n\n\t@gate()\n\tprivate async ensureSession(\n\t\toptions:\n\t\t\t| {\n\t\t\t\t\tcreateIfNeeded?: boolean;\n\t\t\t\t\tforceNewSession?: boolean;\n\t\t\t\t\tsync?: never;\n\t\t\t\t\tsource?: Sources;\n\t\t\t }\n\t\t\t| {\n\t\t\t\t\tcreateIfNeeded?: never;\n\t\t\t\t\tforceNewSession?: never;\n\t\t\t\t\tsync: boolean;\n\t\t\t\t\tsource?: Sources;\n\t\t\t },\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst { createIfNeeded, forceNewSession, source, sync } = options;\n\t\tif (this._session != null) {\n\t\t\tif (this.isSessionForIntegrationHost(this._session)) return this._session;\n\n\t\t\tthis._session = null;\n\t\t}\n\t\tif (this.ctx.config.isIntegrationsEnabled?.() === false) return undefined;\n\n\t\tif (createIfNeeded || sync) {\n\t\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\t\t} else if (this.ctx.storage.getWorkspace(this.connectedKey) === false) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tlet session: ProviderAuthenticationSession | undefined | null;\n\t\ttry {\n\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\tsession = await authProvider.getSession(\n\t\t\t\tthis.authProviderDescriptor,\n\t\t\t\tsync\n\t\t\t\t\t? { sync: sync, source: source }\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tcreateIfNeeded: createIfNeeded,\n\t\t\t\t\t\t\tforceNewSession: forceNewSession,\n\t\t\t\t\t\t\tsource: source,\n\t\t\t\t\t\t},\n\t\t\t);\n\n\t\t\tif (session?.expiresAt != null && session.expiresAt < new Date()) {\n\t\t\t\tsession = null;\n\t\t\t}\n\t\t\tif (session != null && !this.isSessionForIntegrationHost(session)) {\n\t\t\t\tsession = undefined;\n\t\t\t}\n\t\t} catch (ex) {\n\t\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\n\t\t\t// Only interactive paths can prompt for consent, so a sync failure must reach the rethrow below.\n\t\t\tif (!sync && ex instanceof Error && ex.message.includes('User did not consent')) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// On a forced re-sync, propagate so syncCloudConnection can tell a failure from a definitive empty\n\t\t\t// result (#5569); other callers keep swallowing to null so reads never throw.\n\t\t\tif (sync) {\n\t\t\t\t// Throwing skips the reset below, and a lingering count blocks the next non-forced sync.\n\t\t\t\tthis.smoothifyRequestExceptionCount();\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\tsession = null;\n\t\t}\n\n\t\tif (session === undefined && !createIfNeeded && !sync) {\n\t\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\t\t}\n\n\t\tthis._session = session ?? null;\n\t\tthis.smoothifyRequestExceptionCount();\n\n\t\tif (session != null) {\n\t\t\tawait this.ctx.storage.storeWorkspace(this.connectedKey, true);\n\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tthis._onDidChange.fire();\n\t\t\t\tthis.didChangeConnection?.fire({ integration: this, key: this.key, reason: 'connected' });\n\t\t\t\t// Fired detached, so there is no caller left to catch anything: every implementor is async, and\n\t\t\t\t// a rejection would surface as a process-level unhandled rejection in the host. It is a\n\t\t\t\t// best-effort warm-up, so swallow the failure with a warning instead.\n\t\t\t\tvoid (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.providerOnConnect?.();\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tscope?.warn(\n\t\t\t\t\t\t\t`Failed to run providerOnConnect for ${this.key}: ${ex instanceof Error ? ex.message : String(ex)}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t}\n\n\t\treturn session ?? undefined;\n\t}\n\n\tgetIgnoreSSLErrors(): boolean | 'force' {\n\t\treturn this.authenticationService.ignoreSSLErrors(this);\n\t}\n\n\tasync searchMyIssues(\n\t\tresource?: ResourceDescriptor,\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined>;\n\tasync searchMyIssues(\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined>;\n\t@trace()\n\tasync searchMyIssues(\n\t\tresources?: ResourceDescriptor | ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchMyIssuesResult(resources, cancellation, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link searchMyIssues}. Recovers thrown errors into `{ error }` so callers\n\t * (e.g. the ProviderBackend account-wide issues read) can surface a per-provider warning instead of a\n\t * silent empty result. Returns the normalized {@link IssueShape} (there is no raw account-wide issue read).\n\t */\n\tasync searchMyIssuesResult(\n\t\tresources?: ResourceDescriptor | ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<IssueShape[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\tconst issues = await this.searchProviderMyIssues(\n\t\t\t\tsession,\n\t\t\t\tresources != null ? (Array.isArray(resources) ? resources : [resources]) : undefined,\n\t\t\t\tcancellation,\n\t\t\t);\n\t\t\tthis.resetRequestExceptionCount('searchMyIssues');\n\t\t\treturn { value: issues, duration: performance.now() - start };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });\n\t\t\treturn { error: toError(ex), duration: performance.now() - start };\n\t\t}\n\t}\n\n\tprotected abstract searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined>;\n\n\t/**\n\t * Paging/truncation-aware variant of {@link searchProviderMyIssues}. The default wraps the normalized read\n\t * as a complete single page; providers with native cursors or fan-out metadata override it.\n\t */\n\tprotected async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\t_options?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\t// The default read has no assignee scoping to broaden, so `_options` is inert here; a provider whose\n\t\t// account-wide read is user-scoped (GitHub/GitLab/Azure) overrides this and honors `includeAllAssignees`.\n\t\tconst values = await this.searchProviderMyIssues(session, resources, cancellation);\n\t\tif (values == null) return undefined;\n\t\treturn { values: values, truncated: false };\n\t}\n\n\t/**\n\t * Result-returning, truncation-aware account-wide issue read. Recovers thrown errors into `{ error }` and\n\t * carries the `truncated` flag so the ProviderBackend facade can report an incomplete read honestly.\n\t */\n\tasync searchMyIssuesWithTruncationResult(\n\t\tresources?: ResourceDescriptor | ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<IntegrationResult<AccountWideIssuesResult | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst result = await this.searchProviderMyIssuesWithTruncation(\n\t\t\t\tsession,\n\t\t\t\tresources != null ? (Array.isArray(resources) ? resources : [resources]) : undefined,\n\t\t\t\tcancellation,\n\t\t\t\toptions,\n\t\t\t);\n\t\t\tthis.resetRequestExceptionCount('searchMyIssues');\n\t\t\treturn { value: result };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t@trace()\n\tasync getLinkedIssueOrPullRequest(\n\t\tresource: T,\n\t\tlink: { id: string; key: string },\n\t\toptions?: { expiryOverride?: boolean | number; type?: IssueOrPullRequestType },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected) return undefined;\n\n\t\tawait this.refreshSessionIfExpired(scope);\n\n\t\tconst issueOrPR = this.ctx.cache.getIssueOrPullRequest(\n\t\t\tlink.key,\n\t\t\toptions?.type,\n\t\t\tresource,\n\t\t\tthis,\n\t\t\t() => ({\n\t\t\t\tvalue: (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.getProviderLinkedIssueOrPullRequest(\n\t\t\t\t\t\t\tthis._session!,\n\t\t\t\t\t\t\tresource,\n\t\t\t\t\t\t\tlink,\n\t\t\t\t\t\t\toptions?.type,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.resetRequestExceptionCount('getIssueOrPullRequest');\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tthis.handleProviderException('getIssueOrPullRequest', ex, { scope: scope });\n\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t}\n\t\t\t\t})(),\n\t\t\t}),\n\t\t\toptions,\n\t\t);\n\t\treturn issueOrPR;\n\t}\n\n\tprotected abstract getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t\tlink: { id: string; key: string },\n\t\ttype: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined>;\n\n\tasync getIssue(\n\t\tresource: T,\n\t\tid: string,\n\t\toptions?: { connectionId?: string; expiryOverride?: boolean | number },\n\t): Promise<Issue | undefined> {\n\t\treturn (await this.getIssueResult(resource, id, options))?.value;\n\t}\n\n\tgetIssueResult(\n\t\tresource: T,\n\t\tid: string,\n\t\toptions?: { connectionId?: string; expiryOverride?: boolean | number },\n\t): Promise<IntegrationResult<Issue | undefined>> {\n\t\treturn this.getIssueResultCore(resource, id, options, session => this.getProviderIssue(session, resource, id));\n\t}\n\n\t@trace()\n\tprotected async getIssueResultCore(\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t\toptions: { connectionId?: string; expiryOverride?: boolean | number } | undefined,\n\t\tgetProviderIssue: (session: ProviderAuthenticationSession) => Promise<Issue | undefined>,\n\t): Promise<IntegrationResult<Issue | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\tconst { connectionId: requestedConnectionId, expiryOverride } = options ?? {};\n\t\tconst connectionId = requestedConnectionId || undefined;\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst issue = await getCachedIssue({\n\t\t\t\tcache: this.ctx.cache,\n\t\t\t\tid: id,\n\t\t\t\tresource: resource,\n\t\t\t\tintegration: this,\n\t\t\t\tload: async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst issue = await getProviderIssue(session);\n\t\t\t\t\t\tthis.resetRequestExceptionCount('getIssue');\n\t\t\t\t\t\treturn issue;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tif (!isCancellationError(ex)) {\n\t\t\t\t\t\t\tthis.handleProviderException('getIssue', toError(ex), {\n\t\t\t\t\t\t\t\tscope: scope,\n\t\t\t\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow ex;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tcacheOptions: {\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t\texpiryOverride: expiryOverride,\n\t\t\t\t\tetag: `${this.id}:${this.maybeConnected ?? false}:${this.getSessionFingerprint(session)}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn { value: issue };\n\t\t} catch (ex) {\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t\tid: string,\n\t): Promise<Issue | undefined>;\n\n\tasync getCurrentAccount(options?: {\n\t\tavatarSize?: number;\n\t\tconnectionId?: string;\n\t\texpiryOverride?: boolean | number;\n\t}): Promise<Account | undefined> {\n\t\tconst scope = getScopedLogger();\n\t\tconst { connectionId: requestedConnectionId, expiryOverride, ...opts } = options ?? {};\n\t\tconst connectionId = requestedConnectionId || undefined;\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\tconst sessionFingerprint = this.getSessionFingerprint(session);\n\n\t\tconst currentAccount = await this.ctx.cache.getCurrentAccount(\n\t\t\tthis,\n\n\t\t\t(cacheable: any) => ({\n\t\t\t\tvalue: (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst account = await this.getProviderCurrentAccount?.(session, opts);\n\t\t\t\t\t\tthis.resetRequestExceptionCount('getCurrentAccount');\n\t\t\t\t\t\treturn account;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tif (isCancellationError(ex)) {\n\t\t\t\t\t\t\tcacheable.invalidate();\n\t\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthis.handleProviderException('getCurrentAccount', ex, {\n\t\t\t\t\t\t\tscope: scope,\n\t\t\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Invalidate the cache on error, except for auth errors\n\t\t\t\t\t\tif (!(ex instanceof AuthenticationError)) {\n\t\t\t\t\t\t\tcacheable.invalidate();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Re-throw to the caller\n\t\t\t\t\t\tthrow ex;\n\t\t\t\t\t}\n\t\t\t\t})(),\n\t\t\t}),\n\t\t\t{\n\t\t\t\tconnectionId: connectionId,\n\t\t\t\texpiryOverride: expiryOverride,\n\t\t\t\texpireOnError: false,\n\t\t\t\tetag: `${this.id}:${this.maybeConnected ?? false}:${sessionFingerprint}`,\n\t\t\t},\n\t\t);\n\t\treturn currentAccount;\n\t}\n\n\tprotected getProviderCurrentAccount?(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions?: { avatarSize?: number },\n\t): Promise<Account | undefined>;\n\n\t/**\n\t * Resolves the account for a specific session/token — including connections other than the current\n\t * primary (multi-account) — using this integration's provider API base URL and auth type. Returns\n\t * undefined when the provider doesn't support account lookup. Uncached (callers cache per connection).\n\t */\n\tgetProviderAccountForSession(session: ProviderAuthenticationSession): Promise<Account | undefined> {\n\t\treturn this.getProviderCurrentAccount?.(session) ?? Promise.resolve(undefined);\n\t}\n\n\t@trace()\n\tasync getPullRequest(\n\t\tresource: T,\n\t\tid: string,\n\t\toptions?: { expiryOverride?: boolean | number; throwOnError?: boolean },\n\t): Promise<PullRequest | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected) return undefined;\n\n\t\tawait this.refreshSessionIfExpired(scope);\n\n\t\tconst pr = await this.ctx.cache.getPullRequest(\n\t\t\tid,\n\t\t\tresource,\n\t\t\tthis,\n\t\t\tcacheable => ({\n\t\t\t\tvalue: (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.getProviderPullRequest?.(this._session!, resource, id);\n\t\t\t\t\t\tthis.resetRequestExceptionCount('getPullRequest');\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\t// A failed lookup is not an answer — and the by-id bucket never expires a miss.\n\t\t\t\t\t\tcacheable.invalidate();\n\t\t\t\t\t\tthis.handleProviderException('getPullRequest', ex, { scope: scope });\n\t\t\t\t\t\tif (options?.throwOnError) throw ex;\n\n\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t}\n\t\t\t\t})(),\n\t\t\t}),\n\t\t\t{ expiryOverride: options?.expiryOverride },\n\t\t);\n\t\treturn pr;\n\t}\n\n\tprotected getProviderPullRequest?(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t\tid: string,\n\t): Promise<PullRequest | undefined>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqGA,IAAsB,kBAAtB,MAAsB,gBAGE;CASH;CACA;CACA;CACF;CATlB,eAAgC,IAAI,QAAc;CAClD,IAAI,cAA2B;EAC9B,OAAO,KAAK,aAAa;CAC1B;CAEA,YACC,KACA,uBACA,iBACA,qBACC;EAJkB,KAAA,MAAA;EACA,KAAA,wBAAA;EACA,KAAA,kBAAA;EACF,KAAA,sBAAA;CACf;CAEH,UAAgB;EACf,KAAK,aAAa,QAAQ;CAC3B;CAQA,IAAI,yBAAqE;EACxE,OAAO;GAAE,QAAQ,KAAK;GAAQ,QAAQ,KAAK,aAAa;EAAO;CAChE;;;;;;;;;;CAWA,IAAI,OAAe;EAClB,OAAO,kBAAkB,KAAK,GAAG,EAAE,WAAW,KAAK;CACpD;CAEA,SAA2B;EAC1B,OAAO,KAAK,IAAI,QAAQ,cAAc;CACvC;CAEA,YAE6D;EAC5D,OAAO,CAAC;CACT;CAEA,IAAY,eAA4C;EACvD,OAAO,aAAa,KAAK;CAC1B;CAEA,IAAI,iBAAsC;EACzC,OAAO,KAAK,aAAa,KAAA,IAAY,KAAA,IAAY,KAAK,aAAa;CACpE;;CAGA;CACA,IAAI,qBAAyC;EAC5C,IAAI,KAAK,YAAY,MAAM,OAAO,KAAA;EAElC,IAAI,KAAK,qBAAqB,YAAY,KAAK,UAC9C,KAAK,sBAAsB;GAAE,SAAS,KAAK;GAAU,MAAM,KAAK,sBAAsB,KAAK,QAAQ;EAAE;EAEtG,OAAO,KAAK,oBAAoB;CACjC;CAEA,sBAAgC,SAAgD;EAC/E,OAAO,YAAY,QAAQ,WAAW;CACvC;CAEA,IAAI,oBAAyC;EAC5C,IAAI,KAAK,UAAU,aAAa,MAAM,OAAO,KAAA;EAC7C,OAAO,IAAI,KAAK,KAAK,SAAS,SAAS,oBAAI,IAAI,KAAK;CACrD;CAEA;CACA,WACC,QACiG;EACjG,IAAI,KAAK,aAAa,KAAA,GACrB,OAAO,KAAK,cAAc;GAAE,gBAAgB;GAAe;EAAO,CAAC;EAEpE,OAAO,KAAK,YAAY,QAAQ,KAAK,4BAA4B,KAAK,QAAQ,IAAI,KAAK,WAAW,KAAA;CACnG;CAEA,4BAAoC,SAAiD;EACpF,IAAI,CAAC,+BAA+B,KAAK,EAAE,GAAG,OAAO;EAErD,OAAO,qBAAqB,KAAK,QAAQ,QAAQ,MAAM;CACxD;;;;;;;;;CAUA,MAAgB,mBACf,cACA,OACA,QACqD;EACrD,IACC,KAAK,IAAI,OAAO,wBAAwB,MAAM,SAC9C,KAAK,IAAI,QAAQ,aAAa,KAAK,YAAY,MAAM,OAErD;EAKD,IAAI,cAAc;GAKjB,MAAM,uBAAuB,KAAK,gBAAgB,aAAa,YAAY;GAG3E,IAAI;IAEH,MAAM,UAAU,OAAM,MADK,KAAK,sBAAsB,IAAI,KAAK,aAAa,EAAE,EAAA,CAC3C,WAClC;KAAE,GAAG,KAAK;KAAsC;KAAc,OAAO;IAAK,GAC1E;KAAU;KAA8B;IAAqB,CAC9D;IACA,OAAO,WAAW,QAAQ,KAAK,4BAA4B,OAAO,IAAI,UAAU,KAAA;GACjF,SAAS,IAAI;IACZ,OAAO,MAAM,EAAE;IACf;GACD;EACD;EAGA,IAAI,EADc,KAAK,kBAAmB,MAAM,KAAK,YAAY,IACjD,OAAO,KAAA;EAEvB,MAAM,KAAK,wBAAwB,KAAK;EACxC,OAAO,KAAK,YAAY,QAAQ,KAAK,4BAA4B,KAAK,QAAQ,IAAI,KAAK,WAAW,KAAA;CACnG;CAEA,MACM,QAAQ,QAAmC;EAChD,IAAI;GACH,OAAO,QAAQ,MAAM,KAAK,cAAc;IAAE,gBAAgB;IAAc;GAAO,CAAC,CAAC;EAClF,SAAS,KAAK;GACb,OAAO;EACR;CACD;CAIA,MAEM,WAAW,SAA6E;EAC7F,IAAI,SAAS,sBAAsB,KAAK,aAAa,MAAM;EAE3D,MAAM,YAAY,KAAK,YAAY;EAEnC,IAAI,UAAU,CAAC,SAAS;EAExB,IAAI,aAAa,CAAC,SAAS,sBAAsB,CAAC,SAAS,QAAQ;GAClE,MAAM,WAAW,MAAM,KAAK,IAAI,OAAO,sBAAsB;IAC5D,iBAAiB,KAAK;IACtB,cAAc,KAAK,sBAAsB,SAAS,KAAK,aAAa,EAAE;GACvE,CAAC;GACD,IAAI,YAAY,MAAM;GAEtB,UAAU,SAAS;EACpB;EAEA,IAAI,SAcH,OAAM,MAPqB,KAAK,sBAAsB,IAAI,KAAK,aAAa,EAAE,EAAA,CAO3D,kBAAkB,KAAK,sBAAsB;EAGjE,KAAK,2BAA2B,KAAK;EACrC,KAAK,WAAW;EAEhB,IAAI,WAAW;GAGd,IAAI,CAAC,SAAS,sBAAsB,CAAC,SAAS,QAC7C,KAAU,IAAI,QAAQ,eAAe,KAAK,cAAc,KAAK,CAAC,CAAC,MAAM;GAGtE,KAAK,aAAa,KAAK;GACvB,IAAI,CAAC,SAAS,oBACb,KAAK,qBAAqB,KAAK;IAAE,aAAa;IAAM,KAAK,KAAK;IAAK,QAAQ;GAAe,CAAC;EAE7F;EAEA,MAAM,KAAK,uBAAuB;CACnC;CAIA,MACM,iBAAgC;EAGrC,KAAK,uBAAuB;EAE5B,IAAI,KAAK,aAAa,KAAA,GAAW;EAEjC,KAAK,WAAW,KAAA;EAChB,MAAY,KAAK,cAAc;GAAE,gBAAgB;GAAM,iBAAiB;EAAK,CAAC;CAC/E;CAEA,UAAgB;EACf,KAAU,cAAc,EAAE,gBAAgB,MAAM,CAAC;CAClD;CAEA,gDAAwC,IAAI,IAAoB;CAChE,yBAAkC;EACjC,OAAO,KAAK,8BAA8B,OAAO;CAClD;CACA,6BAA6B,gBAAsC;EAClE,KAAK,8BAA8B,IAAI,cAAc;CACtD;;;;;;;CAQA,kBAAmC,IAAI,qBAAqB;;;;;;;;;;;;CAa5D,yBAAyC;EACxC,KAAK,gBAAgB,MAAM;CAC5B;CAEA,OAAwB,wBAAwB;CAChD,wBAAgC;CAEhC,2BAA2B,gBAA8C;EACxE,KAAK,wBAAwB;EAC7B,IAAI,mBAAmB,OAAO;GAC7B,KAAK,8BAA8B,MAAM;GAEzC,KAAK,uBAAuB;EAC7B,OACC,KAAK,8BAA8B,OAAO,cAAc;CAE1D;;;;CAKA,iCAAuC;EAEtC,KAAK,wBAAwB;CAC9B;CAEA,MAAM,QAAuB;EAC5B,MAAM,KAAK,WAAW,EAAE,QAAQ,KAAK,CAAC;EACtC,MAAM,KAAK,IAAI,QAAQ,gBAAgB,KAAK,YAAY;CACzD;;;;;;CAOA,mBAAyB;EAGxB,KAAK,uBAAuB;EAE5B,IAAI,KAAK,aAAa,KAAA,GAAW;EAEjC,MAAM,eAAe,KAAK,YAAY;EACtC,KAAK,WAAW,KAAA;EAChB,KAAK,aAAa,KAAK;EACvB,KAAU,mBAAmB,YAAY;CAC1C;CAEA,MAAc,mBAAmB,cAAsC;EAEtE,IAAI,MADkB,KAAK,cAAc,EAAE,gBAAgB,MAAM,CAAC,KACnD,QAAQ,CAAC,cAAc;EAEtC,KAAK,aAAa,KAAK;EACvB,KAAK,qBAAqB,KAAK;GAAE,aAAa;GAAM,KAAK,KAAK;GAAK,QAAQ;EAAe,CAAC;EAC3F,MAAM,KAAK,uBAAuB;CACnC;CAEA,0BAAkC;CAClC,MACM,oBAAoB,OAAqC,WAAmC;EACjG,MAAM,QAAQ,gBAAgB;EAK9B,IAAI,KAAK,UAAU,UAAU,SAAS,UAAU,aAAa;GAC5D,IAAI,KAAK,OAAA,YAA2C,CAAC,KAAK,yBAAyB;IAClF,KAAK,IAAI,OAAO,SAAS,mBAAmB;KAC3C,IAAI,KAAK;KACT,QAAQ;KACR,OAAO;IACR,CAAC;IACD,KAAK,0BAA0B;GAChC;GACA;EACD;EAEA,QAAQ,OAAR;GACC,KAAK,aAAa;IACjB,MAAM,aAAa,KAAK;IACxB,IAAI,YAAY;IAChB,IAAI,WAAW;KAGd,OAAM,MADqB,KAAK,sBAAsB,IAAI,KAAK,aAAa,EAAE,EAAA,CAC3D,cAAc,KAAK,sBAAsB;KAG5D,KAAK,uBAAuB;KAE5B,KAAK,WAAW,KAAA;KAChB,MAAM,KAAK,IAAI,QAAQ,gBAAgB,KAAK,YAAY;KACxD,YAAY;IACb,OAAO;KAEN,IACC,KAAK,YAAY,QACjB,KAAK,wBAAwB,KAC7B,KAAK,IAAI,QAAQ,aAAa,KAAK,YAAY,MAAM,OAErD;KAGD,YAAY;IACb;IAIA,IAAI;IACJ,IAAI,gBAAgB;IACpB,IAAI;KACH,aAAa,MAAM,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;IAC1D,SAAS,IAAI;KAGZ,IAAI,CAAC,oBAAoB,EAAE,GAC1B,OAAO,MAAM,EAAE;KAEhB,gBAAgB;IACjB;IAEA,IAAI,cAAc,cAAc,WAAW,gBAAgB,WAAW,aACrE,KAAK,2BAA2B,KAAK;IAMtC,IAAI,aAAa,cAAc,QAAQ,CAAC,eAEvC,OAAM,MADqB,KAAK,sBAAsB,IAAI,KAAK,aAAa,EAAE,EAAA,CAC3D,cAAc,KAAK,wBAAwB,EAAE,oBAAoB,MAAM,CAAC;IAG5F;GACD;GACA,KAAK,gBACJ,MAAM,KAAK,WAAW,EAAE,QAAQ,KAAK,CAAC;EAExC;CACD;CAEA,wBACC,gBACA,IACA,SACO;EACP,IAAI,oBAAoB,EAAE,GAAG;EAE7B,IAAI,SAAS,SAAS,MACrB,QAAQ,MAAM,MAAM,EAAE;OAEtB,QAAO,MAAM,EAAE;EAahB,IAAI,cAAc,uBAAuB,SAAS,cAAc;GAC/D,IAAI,CAAC,KAAK,gBAAgB,gBAAgB,QAAQ,YAAY,GAC7D,KAAK,sBAAsB,OAAO;GAEnC;EACD;EAEA,IAAI,cAAc,uBAAuB,KAAK,UAAU,OAAO;GAC9D,IAAI,CAAC,KAAK,uBAAuB,GAAG;IACnC,KAAK,6BAA6B,cAAc;IAChD,KAAK,WAAW;KACf,GAAG,KAAK;KACR,2BAAW,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC;IACnC;GACD,OACC,KAAK,sBAAsB,OAAO;EAEpC,OAAO,IAAI,cAAc,uBAAuB,cAAc,oBAC7D,KAAK,sBAAsB,OAAO;CAEpC;CAEA,0BAAkC;CAClC,MACgB,wBAAwB,OAAqC;EAC5E,IAAI,KAAK,UAAU,aAAa,QAAQ,KAAK,SAAS,4BAAY,IAAI,KAAK,GAE1E,IAAI;GACH,MAAM,KAAK,oBAAoB,aAAa,IAAI;EACjD,SAAS,IAAI;GACZ,OAAO,MAAM,EAAE;EAChB;OACM,IACN,KAAK,UAAU,aAAa,QAC5B,KAAK,OAAA,YACL,CAAC,KAAK,yBACL;GACD,KAAK,IAAI,OAAO,SAAS,mBAAmB;IAC3C,IAAI,KAAK;IACT,QAAQ;IACR,OAAO,KAAK,UAAU;GACvB,CAAC;GACD,KAAK,0BAA0B;EAChC;CACD;CAEA,sBACsB,SAAsC;EAC3D,KAAK;EAEL,IAAI,KAAK,yBAAyB,gBAAgB,yBAAyB,KAAK,aAAa,MAAM;GAClG,IAAI,CAAC,SAAS,QACb,KAAK,IAAI,OAAO,IAAI,qCAAqC,KAAK,IAAI;GAEnE,KAAU,WAAW,EAAE,oBAAoB,KAAK,CAAC;EAClD;CACD;CAEA,MAEM,cAAgC;EACrC,OAAQ,MAAM,KAAK,WAAW,cAAc,KAAM;CACnD;CAEA,MACc,cACb,SAaqD;EACrD,MAAM,QAAQ,gBAAgB;EAE9B,MAAM,EAAE,gBAAgB,iBAAiB,QAAQ,SAAS;EAC1D,IAAI,KAAK,YAAY,MAAM;GAC1B,IAAI,KAAK,4BAA4B,KAAK,QAAQ,GAAG,OAAO,KAAK;GAEjE,KAAK,WAAW;EACjB;EACA,IAAI,KAAK,IAAI,OAAO,wBAAwB,MAAM,OAAO,OAAO,KAAA;EAEhE,IAAI,kBAAkB,MACrB,MAAM,KAAK,IAAI,QAAQ,gBAAgB,KAAK,YAAY;OAClD,IAAI,KAAK,IAAI,QAAQ,aAAa,KAAK,YAAY,MAAM,OAC/D;EAGD,IAAI;EACJ,IAAI;GAEH,UAAU,OAAM,MADW,KAAK,sBAAsB,IAAI,KAAK,aAAa,EAAE,EAAA,CACjD,WAC5B,KAAK,wBACL,OACG;IAAQ;IAAc;GAAO,IAC7B;IACgB;IACC;IACT;GACT,CACH;GAEA,IAAI,SAAS,aAAa,QAAQ,QAAQ,4BAAY,IAAI,KAAK,GAC9D,UAAU;GAEX,IAAI,WAAW,QAAQ,CAAC,KAAK,4BAA4B,OAAO,GAC/D,UAAU,KAAA;EAEZ,SAAS,IAAI;GACZ,MAAM,KAAK,IAAI,QAAQ,gBAAgB,KAAK,YAAY;GAGxD,IAAI,CAAC,QAAQ,cAAc,SAAS,GAAG,QAAQ,SAAS,sBAAsB,GAC7E;GAKD,IAAI,MAAM;IAET,KAAK,+BAA+B;IACpC,MAAM;GACP;GAEA,UAAU;EACX;EAEA,IAAI,YAAY,KAAA,KAAa,CAAC,kBAAkB,CAAC,MAChD,MAAM,KAAK,IAAI,QAAQ,gBAAgB,KAAK,YAAY;EAGzD,KAAK,WAAW,WAAW;EAC3B,KAAK,+BAA+B;EAEpC,IAAI,WAAW,MAAM;GACpB,MAAM,KAAK,IAAI,QAAQ,eAAe,KAAK,cAAc,IAAI;GAE7D,qBAAqB;IACpB,KAAK,aAAa,KAAK;IACvB,KAAK,qBAAqB,KAAK;KAAE,aAAa;KAAM,KAAK,KAAK;KAAK,QAAQ;IAAY,CAAC;IAIxF,CAAM,YAAY;KACjB,IAAI;MACH,MAAM,KAAK,oBAAoB;KAChC,SAAS,IAAI;MACZ,OAAO,KACN,uCAAuC,KAAK,IAAI,IAAI,cAAc,QAAQ,GAAG,UAAU,OAAO,EAAE,GACjG;KACD;IACD,EAAA,CAAG;GACJ,CAAC;EACF;EAEA,OAAO,WAAW,KAAA;CACnB;CAEA,qBAAwC;EACvC,OAAO,KAAK,sBAAsB,gBAAgB,IAAI;CACvD;CAYA,MACM,eACL,WACA,cACA,cACoC;EACpC,QAAQ,MAAM,KAAK,qBAAqB,WAAW,cAAc,YAAY,EAAA,EAAI;CAClF;;;;;;CAOA,MAAM,qBACL,WACA,cACA,cACuD;EACvD,MAAM,QAAQ,gBAAgB;EAE9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,MAAM,QAAQ,YAAY,IAAI;EAC9B,IAAI;GACH,MAAM,SAAS,MAAM,KAAK,uBACzB,SACA,aAAa,OAAQ,MAAM,QAAQ,SAAS,IAAI,YAAY,CAAC,SAAS,IAAK,KAAA,GAC3E,YACD;GACA,KAAK,2BAA2B,gBAAgB;GAChD,OAAO;IAAE,OAAO;IAAQ,UAAU,YAAY,IAAI,IAAI;GAAM;EAC7D,SAAS,IAAI;GACZ,KAAK,wBAAwB,kBAAkB,IAAI;IAAS;IAAqB;GAAa,CAAC;GAC/F,OAAO;IAAE,OAAO,QAAQ,EAAE;IAAG,UAAU,YAAY,IAAI,IAAI;GAAM;EAClE;CACD;;;;;CAYA,MAAgB,qCACf,SACA,WACA,cACA,UAC+C;EAG/C,MAAM,SAAS,MAAM,KAAK,uBAAuB,SAAS,WAAW,YAAY;EACjF,IAAI,UAAU,MAAM,OAAO,KAAA;EAC3B,OAAO;GAAU;GAAQ,WAAW;EAAM;CAC3C;;;;;CAMA,MAAM,mCACL,WACA,cACA,cACA,SACkE;EAClE,MAAM,QAAQ,gBAAgB;EAC9B,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,IAAI;GACH,MAAM,SAAS,MAAM,KAAK,qCACzB,SACA,aAAa,OAAQ,MAAM,QAAQ,SAAS,IAAI,YAAY,CAAC,SAAS,IAAK,KAAA,GAC3E,cACA,OACD;GACA,KAAK,2BAA2B,gBAAgB;GAChD,OAAO,EAAE,OAAO,OAAO;EACxB,SAAS,IAAI;GACZ,KAAK,wBAAwB,kBAAkB,IAAI;IAAS;IAAqB;GAAa,CAAC;GAC/F,OAAO,EAAE,OAAO,QAAQ,EAAE,EAAE;EAC7B;CACD;CAEA,MACM,4BACL,UACA,MACA,SAC0C;EAC1C,MAAM,QAAQ,gBAAgB;EAG9B,IAAI,EADc,KAAK,kBAAmB,MAAM,KAAK,YAAY,IACjD,OAAO,KAAA;EAEvB,MAAM,KAAK,wBAAwB,KAAK;EA0BxC,OAxBkB,KAAK,IAAI,MAAM,sBAChC,KAAK,KACL,SAAS,MACT,UACA,aACO,EACN,QAAQ,YAAY;GACnB,IAAI;IACH,MAAM,SAAS,MAAM,KAAK,oCACzB,KAAK,UACL,UACA,MACA,SAAS,IACV;IACA,KAAK,2BAA2B,uBAAuB;IACvD,OAAO;GACR,SAAS,IAAI;IACZ,KAAK,wBAAwB,yBAAyB,IAAI,EAAS,MAAM,CAAC;IAC1E;GACD;EACD,EAAA,CAAG,EACJ,IACA,OAEc;CAChB;CASA,MAAM,SACL,UACA,IACA,SAC6B;EAC7B,QAAQ,MAAM,KAAK,eAAe,UAAU,IAAI,OAAO,EAAA,EAAI;CAC5D;CAEA,eACC,UACA,IACA,SACgD;EAChD,OAAO,KAAK,mBAAmB,UAAU,IAAI,UAAS,YAAW,KAAK,iBAAiB,SAAS,UAAU,EAAE,CAAC;CAC9G;CAEA,MACgB,mBACf,UACA,IACA,SACA,kBACgD;EAChD,MAAM,QAAQ,gBAAgB;EAC9B,MAAM,EAAE,cAAc,uBAAuB,mBAAmB,WAAW,CAAC;EAC5E,MAAM,eAAe,yBAAyB,KAAA;EAC9C,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,IAAI;GA2BH,OAAO,EAAE,OAAO,MA1BI,eAAe;IAClC,OAAO,KAAK,IAAI;IACZ;IACM;IACV,aAAa;IACb,MAAM,YAAY;KACjB,IAAI;MACH,MAAM,QAAQ,MAAM,iBAAiB,OAAO;MAC5C,KAAK,2BAA2B,UAAU;MAC1C,OAAO;KACR,SAAS,IAAI;MACZ,IAAI,CAAC,oBAAoB,EAAE,GAC1B,KAAK,wBAAwB,YAAY,QAAQ,EAAE,GAAG;OAC9C;OACO;MACf,CAAC;MAEF,MAAM;KACP;IACD;IACA,cAAc;KACC;KACE;KAChB,MAAM,GAAG,KAAK,GAAG,GAAG,KAAK,kBAAkB,MAAM,GAAG,KAAK,sBAAsB,OAAO;IACvF;GACD,CAAC,EACqB;EACvB,SAAS,IAAI;GACZ,OAAO,EAAE,OAAO,QAAQ,EAAE,EAAE;EAC7B;CACD;CAQA,MAAM,kBAAkB,SAIS;EAChC,MAAM,QAAQ,gBAAgB;EAC9B,MAAM,EAAE,cAAc,uBAAuB,gBAAgB,GAAG,SAAS,WAAW,CAAC;EACrF,MAAM,eAAe,yBAAyB,KAAA;EAC9C,MAAM,UAAU,MAAM,KAAK,mBAAmB,cAAc,KAAK;EACjE,IAAI,WAAW,MAAM,OAAO,KAAA;EAE5B,MAAM,qBAAqB,KAAK,sBAAsB,OAAO;EAuC7D,OAAO,MArCsB,KAAK,IAAI,MAAM,kBAC3C,OAEC,eAAoB,EACpB,QAAQ,YAAY;GACnB,IAAI;IACH,MAAM,UAAU,MAAM,KAAK,4BAA4B,SAAS,IAAI;IACpE,KAAK,2BAA2B,mBAAmB;IACnD,OAAO;GACR,SAAS,IAAI;IACZ,IAAI,oBAAoB,EAAE,GAAG;KAC5B,UAAU,WAAW;KACrB;IACD;IAEA,KAAK,wBAAwB,qBAAqB,IAAI;KAC9C;KACO;IACf,CAAC;IAGD,IAAI,EAAE,cAAc,sBACnB,UAAU,WAAW;IAItB,MAAM;GACP;EACD,EAAA,CAAG,EACJ,IACA;GACe;GACE;GAChB,eAAe;GACf,MAAM,GAAG,KAAK,GAAG,GAAG,KAAK,kBAAkB,MAAM,GAAG;EACrD,CACD;CAED;;;;;;CAYA,6BAA6B,SAAsE;EAClG,OAAO,KAAK,4BAA4B,OAAO,KAAK,QAAQ,QAAQ,KAAA,CAAS;CAC9E;CAEA,MACM,eACL,UACA,IACA,SACmC;EACnC,MAAM,QAAQ,gBAAgB;EAG9B,IAAI,EADc,KAAK,kBAAmB,MAAM,KAAK,YAAY,IACjD,OAAO,KAAA;EAEvB,MAAM,KAAK,wBAAwB,KAAK;EAwBxC,OAAO,MAtBU,KAAK,IAAI,MAAM,eAC/B,IACA,UACA,OACA,eAAc,EACb,QAAQ,YAAY;GACnB,IAAI;IACH,MAAM,SAAS,MAAM,KAAK,yBAAyB,KAAK,UAAW,UAAU,EAAE;IAC/E,KAAK,2BAA2B,gBAAgB;IAChD,OAAO;GACR,SAAS,IAAI;IAEZ,UAAU,WAAW;IACrB,KAAK,wBAAwB,kBAAkB,IAAI,EAAS,MAAM,CAAC;IACnE,IAAI,SAAS,cAAc,MAAM;IAEjC;GACD;EACD,EAAA,CAAG,EACJ,IACA,EAAE,gBAAgB,SAAS,eAAe,CAC3C;CAED;AAOD;AAnwBE,WAAA,CAAA,MAAM,CAAA,GAAA,gBAAA,WAAA,WAAA,IAAA;AAWN,WAAA,CAAA,KAAK,GACL,MAAM,CAAA,GAAA,gBAAA,WAAA,cAAA,IAAA;AAwDN,WAAA,CAAA,MAAM,CAAA,GAAA,gBAAA,WAAA,kBAAA,IAAA;AAsGN,WAAA,CAAA,MAAM,CAAA,GAAA,gBAAA,WAAA,uBAAA,IAAA;AAgIN,WAAA,CAAA,KAAK,CAAA,GAAA,gBAAA,WAAA,2BAAA,IAAA;AAuBL,WAAA,CAAA,MAAM,CAAA,GAAA,gBAAA,WAAA,yBAAA,IAAA;AAYN,WAAA,CAAA,KAAK,GACL,MAAM,EAAE,MAAM,KAAK,CAAC,CAAA,GAAA,gBAAA,WAAA,eAAA,IAAA;AAKpB,WAAA,CAAA,KAAK,CAAA,GAAA,gBAAA,WAAA,iBAAA,IAAA;AAoHL,WAAA,CAAA,MAAM,CAAA,GAAA,gBAAA,WAAA,kBAAA,IAAA;AA2FN,WAAA,CAAA,MAAM,CAAA,GAAA,gBAAA,WAAA,+BAAA,IAAA;AA+DN,WAAA,CAAA,MAAM,CAAA,GAAA,gBAAA,WAAA,sBAAA,IAAA;AAuHN,WAAA,CAAA,MAAM,CAAA,GAAA,gBAAA,WAAA,kBAAA,IAAA"}
1
+ {"version":3,"file":"integration.js","names":[],"sources":["../../../../../plus/integrations/src/models/integration.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { PullRequest } from '@gitlens/git/models/pullRequest.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isCancellationError } from '@gitlens/utils/cancellation.js';\nimport { gate } from '@gitlens/utils/decorators/gate.js';\nimport { debug, trace } from '@gitlens/utils/decorators/log.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { fnv1aHash64 } from '@gitlens/utils/hash.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { ScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport { getScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type {\n\tIntegrationAuthenticationProviderDescriptor,\n\tIntegrationAuthenticationSessionDescriptor,\n} from '../authentication/integrationAuthenticationProvider.js';\nimport type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { RejectedTokenTracker } from '../authentication/rejectedTokenTracker.js';\nimport type { ProviderRefusal } from '../collectionMetadata.js';\nimport {\n\tattributeScopedAuthFailures,\n\thasOnlyScopedAuthFailures,\n\tmarkCredentialRefusals,\n} from '../collectionMetadata.js';\nimport type { IntegrationIds, IssuesCloudHostIntegrationId, IssuesHostIntegrationIds } from '../constants.js';\nimport { GitCloudHostIntegrationId } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { AuthenticationError, AuthenticationErrorReason, RequestClientError, toError } from '../errors.js';\nimport type { IntegrationConnectionChangeEvent } from '../integrationService.js';\nimport { providersMetadata } from '../providers/models.js';\nimport type { ProvidersApi } from '../providers/providersApi.js';\nimport type { ProviderWarningCause, ProviderWarningScope } from '../results.js';\nimport type { Sources } from '../telemetry.js';\nimport { areDomainsOnSameHost } from '../utils/domain.utils.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { GitHostIntegration } from './gitHostIntegration.js';\nimport { getCachedIssue } from './issueCache.js';\nimport type { AccountWideIssuesResult, SearchMyIssuesOptions } from './issueReads.js';\nimport type { IssuesIntegration } from './issuesIntegration.js';\n\nexport type Integration = GitHostIntegration | IssuesIntegration;\nexport type IntegrationById<T extends IntegrationIds> = T extends IssuesHostIntegrationIds\n\t? IssuesIntegration\n\t: GitHostIntegration;\nexport type IntegrationType = 'git' | 'issues';\n\n// The issue-read contracts live in their own module (pure data, and their relationship to each other is the\n// point of reading them together); re-exported here so the providers that implement these reads keep one import.\nexport type {\n\tAccountWideIssuesResult,\n\tProviderIssueSearchPage,\n\tProviderSearchCount,\n\tSearchMyIssuesOptions,\n} from './issueReads.js';\nexport type { ProviderPullRequestCount, ProviderPullRequestSearchPage } from './pullRequestReads.js';\n\n// Keep this in step with `isSelfManagedHostIntegrationId`: it is the type-level twin of that predicate, and a\n// disagreement compiles cleanly while the runtime writes a domain-keyed key the type says is unkeyed (so the\n// local-disconnect flag is written under one key and read under another).\nexport type IntegrationKey<T extends IntegrationIds = IntegrationIds> = T extends\n\t| GitCloudHostIntegrationId\n\t| IssuesCloudHostIntegrationId\n\t? `${T}`\n\t: `${T}:${string}`;\n\nexport type IntegrationConnectedKey<T extends IntegrationIds = IntegrationIds> = `connected:${IntegrationKey<T>}`;\n\nexport type IntegrationResult<T> =\n\t| { value: T; duration?: number; error?: Error }\n\t| { error: Error; duration?: number; value?: never }\n\t| undefined;\n\ntype SyncReqUsecase = Exclude<\n\t| 'getAccountForCommit'\n\t| 'getAccountForEmail'\n\t| 'getAccountForResource'\n\t| 'getCurrentAccount'\n\t| 'getDefaultBranch'\n\t| 'getIssue'\n\t| 'getIssueOrPullRequest'\n\t| 'getIssuesBatch'\n\t| 'getIssuesForProject'\n\t| 'getIssuesForRepos'\n\t| 'getMyPullRequestsForUser'\n\t| 'getOrganizationsForUser'\n\t| 'getProjectsForOrg'\n\t| 'getProjectsForResources'\n\t| 'getPullRequest'\n\t| 'getRepositoriesForOrg'\n\t| 'getRepositoriesForUser'\n\t| 'getPullRequestForBranch'\n\t| 'getPullRequestForCommit'\n\t| 'getPullRequestsForRepos'\n\t| 'getRepositoryMetadata'\n\t| 'getResourcesForUser'\n\t| 'getSshSigningKeysForEmails'\n\t| 'countIssues'\n\t| 'countPullRequests'\n\t| 'mergePullRequest'\n\t| 'searchIssuesPage'\n\t| 'searchPullRequestsPage'\n\t| 'searchMyIssues'\n\t| 'searchMyPullRequests'\n\t| 'searchPullRequests',\n\t// excluding to show explicitly that we don't want to add 'all' key occasionally\n\t'all'\n>;\n\nexport abstract class IntegrationBase<\n\tID extends IntegrationIds = IntegrationIds,\n\tT extends ResourceDescriptor = ResourceDescriptor,\n> implements Disposable {\n\tabstract readonly type: IntegrationType;\n\n\tprivate readonly _onDidChange = new Emitter<void>();\n\tget onDidChange(): Event<void> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tconstructor(\n\t\tprotected readonly ctx: IntegrationServiceContext,\n\t\tprotected readonly authenticationService: IntegrationAuthenticationService,\n\t\tprotected readonly getProvidersApi: () => Promise<ProvidersApi>,\n\t\tprivate readonly didChangeConnection: Emitter<IntegrationConnectionChangeEvent>,\n\t) {}\n\n\tdispose(): void {\n\t\tthis._onDidChange.dispose();\n\t}\n\n\tabstract get authProvider(): IntegrationAuthenticationProviderDescriptor;\n\tabstract get id(): ID;\n\tprotected abstract get key(): IntegrationKey<ID>;\n\tabstract get name(): string;\n\tabstract get domain(): string;\n\n\tget authProviderDescriptor(): IntegrationAuthenticationSessionDescriptor {\n\t\treturn { domain: this.domain, scopes: this.authProvider.scopes };\n\t}\n\n\t/**\n\t * The `gl-provider-<key>` glicon key for this provider, which is NOT always its id.\n\t *\n\t * `providersMetadata.iconKey` is the field that says which glyph a provider draws with, and a\n\t * self-managed variant reuses its cloud family's — Jira Data Center draws with Jira's, because it is the\n\t * same product and the id only selects an icon. Returning the id instead named a glyph the registry has\n\t * no entry for, so a consumer rendering a `gl-provider-` glicon from `issue.provider.icon` requested a\n\t * glyph that does not exist.\n\t */\n\tget icon(): string {\n\t\treturn providersMetadata[this.id]?.iconKey ?? this.id;\n\t}\n\n\taccess(): Promise<boolean> {\n\t\treturn this.ctx.account.isTrialOrPaid();\n\t}\n\n\tautolinks():\n\t\t| (AutolinkReference | DynamicAutolinkReference)[]\n\t\t| Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\treturn [];\n\t}\n\n\tprivate get connectedKey(): IntegrationConnectedKey<ID> {\n\t\treturn `connected:${this.key}`;\n\t}\n\n\tget maybeConnected(): boolean | undefined {\n\t\treturn this._session === undefined ? undefined : this._session !== null;\n\t}\n\n\t/** Hash of the current session's access token. Changes on any token change (account switch or refresh). */\n\tprivate _sessionFingerprint: { session: ProviderAuthenticationSession; hash: string } | undefined;\n\tget sessionFingerprint(): string | undefined {\n\t\tif (this._session == null) return undefined;\n\n\t\tif (this._sessionFingerprint?.session !== this._session) {\n\t\t\tthis._sessionFingerprint = { session: this._session, hash: this.getSessionFingerprint(this._session) };\n\t\t}\n\t\treturn this._sessionFingerprint.hash;\n\t}\n\n\tprotected getSessionFingerprint(session: ProviderAuthenticationSession): string {\n\t\treturn fnv1aHash64(session.accessToken);\n\t}\n\n\tget connectionExpired(): boolean | undefined {\n\t\tif (this._session?.expiresAt == null) return undefined;\n\t\treturn new Date(this._session.expiresAt) < new Date();\n\t}\n\n\tprotected _session: ProviderAuthenticationSession | null | undefined;\n\tgetSession(\n\t\tsource: Sources,\n\t): ProviderAuthenticationSession | Promise<ProviderAuthenticationSession | undefined> | undefined {\n\t\tif (this._session === undefined) {\n\t\t\treturn this.ensureSession({ createIfNeeded: false, source: source });\n\t\t}\n\t\treturn this._session != null && this.isSessionForIntegrationHost(this._session) ? this._session : undefined;\n\t}\n\n\tprivate isSessionForIntegrationHost(session: ProviderAuthenticationSession): boolean {\n\t\tif (!isSelfManagedHostIntegrationId(this.id)) return true;\n\n\t\treturn areDomainsOnSameHost(this.domain, session.domain);\n\t}\n\n\t/**\n\t * Resolves the session to read as, for a per-connection (multi-account) read. When `connectionId` is\n\t * omitted this is the integration's primary session, resolved exactly like the existing read flow\n\t * (ensure-connected + refresh-if-expired). When set, it resolves THAT connection's session directly\n\t * from the auth provider — refreshing it if expired — WITHOUT disturbing the cached primary\n\t * `_session`. Returns undefined when the requested session can't be resolved (e.g. the connection is\n\t * gone or the provider isn't connected), so callers degrade to \"no results\".\n\t */\n\tprotected async resolveReadSession(\n\t\tconnectionId: string | undefined,\n\t\tscope: ScopedLogger | undefined,\n\t\tsource?: Sources,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tif (\n\t\t\tthis.ctx.config.isIntegrationsEnabled?.() === false ||\n\t\t\tthis.ctx.storage.getWorkspace(this.connectedKey) === false\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// A truthy connectionId targets a specific account; an empty string is not a real target, so it falls\n\t\t// through to the primary path below.\n\t\tif (connectionId) {\n\t\t\t// A read of this connection previously failed with an AuthenticationError, so the token in\n\t\t\t// storage is known-refused. Ask for a refresh through the GK cloud before reading again, which\n\t\t\t// is this branch's equivalent of the primary path's `refreshSessionIfExpired`. Claimed here so\n\t\t\t// the forced refresh runs at most once per rejection.\n\t\t\tconst refreshRejectedToken = this._rejectedTokens.claimRefresh(connectionId);\n\t\t\t// Degrade to \"no results\" on failure, matching the primary path (whose ensureSession/\n\t\t\t// refreshSessionIfExpired swallow errors) so read methods keep their never-throws contract.\n\t\t\ttry {\n\t\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\t\tconst session = await authProvider.getSession(\n\t\t\t\t\t{ ...this.authProviderDescriptor, connectionId: connectionId, cloud: true },\n\t\t\t\t\t{ source: source, refreshRejectedToken: refreshRejectedToken },\n\t\t\t\t);\n\t\t\t\treturn session != null && this.isSessionForIntegrationHost(session) ? session : undefined;\n\t\t\t} catch (ex) {\n\t\t\t\tscope?.error(ex);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected) return undefined;\n\n\t\tawait this.refreshSessionIfExpired(scope);\n\t\treturn this._session != null && this.isSessionForIntegrationHost(this._session) ? this._session : undefined;\n\t}\n\n\t@debug()\n\tasync connect(source: Sources): Promise<boolean> {\n\t\ttry {\n\t\t\treturn Boolean(await this.ensureSession({ createIfNeeded: true, source: source }));\n\t\t} catch (_ex) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprotected providerOnConnect?(): void | Promise<void>;\n\n\t@gate()\n\t@debug()\n\tasync disconnect(options?: { silent?: boolean; currentSessionOnly?: boolean }): Promise<void> {\n\t\tif (options?.currentSessionOnly && this._session === null) return;\n\n\t\tconst connected = this._session != null;\n\n\t\tlet signOut = !options?.currentSessionOnly;\n\n\t\tif (connected && !options?.currentSessionOnly && !options?.silent) {\n\t\t\tconst decision = await this.ctx.hooks?.onConfirmDisconnect?.({\n\t\t\t\tintegrationName: this.name,\n\t\t\t\tofferSignOut: this.authenticationService.supports(this.authProvider.id),\n\t\t\t});\n\t\t\tif (decision == null) return;\n\n\t\t\tsignOut = decision.signOut;\n\t\t}\n\n\t\tif (signOut) {\n\t\t\t// Disconnecting a provider signs out of ALL its connected accounts (multi-account), not just the\n\t\t\t// primary — otherwise secondary connections' secrets/config would be orphaned. Removing a single\n\t\t\t// account is done via IntegrationService.deleteConnection instead. Pass this instance's descriptor\n\t\t\t// so self-managed disconnects stay scoped to this host: those group every host under one provider\n\t\t\t// id, so an unscoped clear would sign the user out of unrelated hosts. deleteAllSessions derives an\n\t\t\t// undefined domain for cloud providers, so they still clear every account as intended.\n\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\t// Awaited, not fire-and-forget: a caller that awaits `disconnect()` has to be able to rely on the\n\t\t\t// secrets and descriptors actually being gone when it resumes. Left floating, the only thing that\n\t\t\t// ever made this land in time was incidental scheduling slack — `syncCloudIntegrations` used to\n\t\t\t// await each provider in turn, so a later iteration's suspension let the previous provider's delete\n\t\t\t// finish. Syncing providers concurrently removes that slack and the clear was observably still\n\t\t\t// pending when the sync returned.\n\t\t\tawait authProvider.deleteAllSessions(this.authProviderDescriptor);\n\t\t}\n\n\t\tthis.resetRequestExceptionCount('all');\n\t\tthis._session = null;\n\n\t\tif (connected) {\n\t\t\t// Don't store the disconnected flag if silently disconnecting or disconnecting this only for\n\t\t\t// this current VS Code session (will be re-connected on next restart)\n\t\t\tif (!options?.currentSessionOnly && !options?.silent) {\n\t\t\t\tvoid this.ctx.storage.storeWorkspace(this.connectedKey, false).catch();\n\t\t\t}\n\n\t\t\tthis._onDidChange.fire();\n\t\t\tif (!options?.currentSessionOnly) {\n\t\t\t\tthis.didChangeConnection?.fire({ integration: this, key: this.key, reason: 'disconnected' });\n\t\t\t}\n\t\t}\n\n\t\tawait this.providerOnDisconnect?.();\n\t}\n\n\tprotected providerOnDisconnect?(): void | Promise<void>;\n\n\t@debug()\n\tasync reauthenticate(): Promise<void> {\n\t\t// `forceNewSession` below deletes the stored secrets to reconnect. Ahead of the guard — see\n\t\t// `onStoredTokensReplaced`.\n\t\tthis.onStoredTokensReplaced();\n\n\t\tif (this._session === undefined) return;\n\n\t\tthis._session = undefined;\n\t\tvoid (await this.ensureSession({ createIfNeeded: true, forceNewSession: true }));\n\t}\n\n\trefresh(): void {\n\t\tvoid this.ensureSession({ createIfNeeded: false });\n\t}\n\n\tprivate _syncRequestsPerFailedUsecase = new Set<SyncReqUsecase>();\n\thasSessionSyncRequests(): boolean {\n\t\treturn this._syncRequestsPerFailedUsecase.size > 0;\n\t}\n\trequestSessionSyncForUsecase(syncReqUsecase: SyncReqUsecase): void {\n\t\tthis._syncRequestsPerFailedUsecase.add(syncReqUsecase);\n\t}\n\n\t/**\n\t * The per-connection counterpart of the primary session's expire-and-resync recovery: a token the\n\t * provider refused is refreshed once through the GK cloud, which exchanges the refresh token server-side\n\t * (the client never holds one). Armed by {@link handleProviderException} and consumed by\n\t * {@link resolveReadSession}; a rejection it declines belongs to {@link trackRequestException} instead.\n\t */\n\tprivate readonly _rejectedTokens = new RejectedTokenTracker();\n\n\t/**\n\t * Called by every path that replaces or removes the stored tokens — a disconnect, a forced re-sync, a\n\t * reauthentication, or the connection set changing. A rejection names a specific credential, so once that\n\t * credential is gone the rejection describes nothing and must not force a refresh (or, for a deleted\n\t * connection, outlive it).\n\t *\n\t * Deliberately NOT conditioned on `_session`. A per-connection read resolves through the auth provider and\n\t * never populates the cached primary session, so the connections this recovery exists for are exactly the\n\t * ones with no `_session` to inspect — the same blind spot the recovery itself was added to fix. Callers\n\t * that guard on `_session` therefore invoke this ahead of that guard.\n\t */\n\tprotected onStoredTokensReplaced(): void {\n\t\tthis._rejectedTokens.clear();\n\t}\n\n\tprivate static readonly requestExceptionLimit = 5;\n\tprivate requestExceptionCount = 0;\n\n\tresetRequestExceptionCount(syncReqUsecase: SyncReqUsecase | 'all'): void {\n\t\tthis.requestExceptionCount = 0;\n\t\tif (syncReqUsecase === 'all') {\n\t\t\tthis._syncRequestsPerFailedUsecase.clear();\n\t\t\t// 'all' is the whole-integration reset: a disconnect, or a re-sync that produced a new access token.\n\t\t\tthis.onStoredTokensReplaced();\n\t\t} else {\n\t\t\tthis._syncRequestsPerFailedUsecase.delete(syncReqUsecase);\n\t\t}\n\t}\n\n\t/**\n\t * Resets request exceptions without resetting the amount of syncs\n\t */\n\tsmoothifyRequestExceptionCount(): void {\n\t\t// On resync we reset exception count only to avoid infinitive syncs on failure\n\t\tthis.requestExceptionCount = 0;\n\t}\n\n\tasync reset(): Promise<void> {\n\t\tawait this.disconnect({ silent: true });\n\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\t}\n\n\t/**\n\t * Drops the in-memory session so the next access re-resolves it from storage. Used when the primary\n\t * connection changed underneath a warm integration (e.g. after `setPrimaryConnection`/\n\t * `deleteConnection`). Unlike {@link reset}/{@link disconnect}, it deletes nothing from storage.\n\t */\n\tswitchConnection(): void {\n\t\t// A connection was deleted, or a different one became primary. Ahead of the guard — see\n\t\t// `onStoredTokensReplaced`.\n\t\tthis.onStoredTokensReplaced();\n\n\t\tif (this._session === undefined) return;\n\n\t\tconst wasConnected = this._session != null;\n\t\tthis._session = undefined;\n\t\tthis._onDidChange.fire();\n\t\tvoid this.refreshAfterSwitch(wasConnected);\n\t}\n\n\tprivate async refreshAfterSwitch(wasConnected: boolean): Promise<void> {\n\t\tconst session = await this.ensureSession({ createIfNeeded: false });\n\t\tif (session != null || !wasConnected) return;\n\n\t\tthis._onDidChange.fire();\n\t\tthis.didChangeConnection?.fire({ integration: this, key: this.key, reason: 'disconnected' });\n\t\tawait this.providerOnDisconnect?.();\n\t}\n\n\tprivate skippedNonCloudReported = false;\n\t@debug()\n\tasync syncCloudConnection(state: 'connected' | 'disconnected', forceSync: boolean): Promise<void> {\n\t\tconst scope = getScopedLogger();\n\t\t// Initially the condition on `this._session.cloud` has been added here: https://github.com/gitkraken/vscode-gitlens/commit/e95e70c430bd162924cc3bd5c1e8ab90e6293449#diff-4213141a45cccaab7aa2e40028b155a87eb913b07388485831403e60ce5555e4R237\n\t\t// I'm not sure about reasons, but it seems we want to replace it with the cloud session if it's connected.\n\t\t// Gradually we'll stop having non-cloud sessions.\n\t\t// However this is needed to be tested with PATs, e.g. with a GitLab PAT.\n\t\tif (this._session?.cloud === false && state !== 'connected') {\n\t\t\tif (this.id !== GitCloudHostIntegrationId.GitHub && !this.skippedNonCloudReported) {\n\t\t\t\tthis.ctx.hooks?.session?.onRefreshSkipped?.({\n\t\t\t\t\tid: this.id,\n\t\t\t\t\treason: 'skip-non-cloud',\n\t\t\t\t\tcloud: false,\n\t\t\t\t});\n\t\t\t\tthis.skippedNonCloudReported = true;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (state) {\n\t\t\tcase 'connected': {\n\t\t\t\tconst oldSession = this._session;\n\t\t\t\tlet resyncing = false;\n\t\t\t\tif (forceSync) {\n\t\t\t\t\t// Reset our stored session so that we get a new one from the cloud\n\t\t\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\t\t\tawait authProvider.deleteSession(this.authProviderDescriptor);\n\t\t\t\t\t// The stored token was just deleted. Not left to the token-changed check below, which needs\n\t\t\t\t\t// an `oldSession` — see `onStoredTokensReplaced`.\n\t\t\t\t\tthis.onStoredTokensReplaced();\n\t\t\t\t\t// Reset the session and clear our \"stay disconnected\" flag\n\t\t\t\t\tthis._session = undefined;\n\t\t\t\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\t\t\t\t\tresyncing = true;\n\t\t\t\t} else {\n\t\t\t\t\t// Only sync if we're not connected and not disabled and don't have pending errors\n\t\t\t\t\tif (\n\t\t\t\t\t\tthis._session != null ||\n\t\t\t\t\t\tthis.requestExceptionCount > 0 ||\n\t\t\t\t\t\tthis.ctx.storage.getWorkspace(this.connectedKey) === false\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tforceSync = true;\n\t\t\t\t}\n\n\t\t\t\t// sync option, rather than createIfNeeded, makes sure we don't call connectCloudIntegrations and open a gkdev window\n\t\t\t\t// if there was no session or some problem fetching/refreshing the existing session from the cloud api\n\t\t\t\tlet newSession: ProviderAuthenticationSession | undefined;\n\t\t\t\tlet refetchFailed = false;\n\t\t\t\ttry {\n\t\t\t\t\tnewSession = await this.ensureSession({ sync: forceSync });\n\t\t\t\t} catch (ex) {\n\t\t\t\t\t// Not evidence the connection is gone (#5569). Also leaves `_session` as-is rather than\n\t\t\t\t\t// latching null, so the next access can re-resolve it.\n\t\t\t\t\tif (!isCancellationError(ex)) {\n\t\t\t\t\t\tscope?.error(ex);\n\t\t\t\t\t}\n\t\t\t\t\trefetchFailed = true;\n\t\t\t\t}\n\n\t\t\t\tif (oldSession && newSession && newSession.accessToken !== oldSession.accessToken) {\n\t\t\t\t\tthis.resetRequestExceptionCount('all');\n\t\t\t\t}\n\n\t\t\t\t// The forced re-sync above deleted the cloud secret but kept the descriptor to avoid UI churn.\n\t\t\t\t// Drop it only when the replacement fetch came back definitively empty, so a connection whose\n\t\t\t\t// token is really gone is cleanly disconnected (#5497) while a healthy one survives a blip (#5569).\n\t\t\t\tif (resyncing && newSession == null && !refetchFailed) {\n\t\t\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\t\t\tawait authProvider.deleteSession(this.authProviderDescriptor, { preserveConfigured: false });\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'disconnected':\n\t\t\t\tawait this.disconnect({ silent: true });\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * Proves the credential with an uncached check, rejecting with the provider's `AuthenticationError` when it is\n\t * refused, and with anything else when the check proves nothing. A `403` proves nothing either: the credential\n\t * authenticated and was only denied the check's request.\n\t *\n\t * Implemented by the providers whose reads can reach their scopes without an uncached request to the connection\n\t * first: discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud), or an SDK fan-out across\n\t * the requested repositories (Bitbucket Data Center). There, a dead token comes back as scoped refusals with no\n\t * failure of the connection: the same shape as one scope refusing a sound credential.\n\t * {@link confirmScopedAuthFailures} uses this to tell the two apart.\n\t */\n\tprotected validateCredential?(session: ProviderAuthenticationSession): Promise<void>;\n\n\t/**\n\t * Credentials that passed {@link validateCredential} within the last minute. A probe already in flight is\n\t * shared rather than repeated, and a refused one is not kept. Cleared whenever a credential is refused (see\n\t * {@link handleProviderException}), so a pass is never vouched for past a refusal this instance has seen.\n\t */\n\tprivate readonly _validatedCredentials = new PromiseCache<string, void>({ createTTL: 60 * 1000, capacity: 10 });\n\n\t/**\n\t * Whether a scope's refusal is the credential's own, by the provider's account of it: e.g. a token missing the\n\t * OAuth scopes the read needs. The probe cannot see that, because it proves the token authenticates, not that\n\t * it is authorized. So {@link confirmScopedAuthFailures} publishes one for the connection, unscoped, which asks\n\t * for the reconnect (consenting to the scopes again) that fixes it, and keeps the results of the scopes that\n\t * answered.\n\t */\n\tprotected isCredentialRefusal?(refusal: ProviderRefusal): boolean;\n\n\t/**\n\t * Names why a confirmed credential was refused by one scope, from what the refusal said (see\n\t * `ProviderWarning.cause`). Called only by {@link confirmScopedAuthFailures}, once the credential passed: the\n\t * refusals this names are indistinguishable from a dead credential until then.\n\t */\n\tprotected describeRefusal?(\n\t\tsession: ProviderAuthenticationSession,\n\t\trefusal: ProviderRefusal,\n\t\tscope: ProviderWarningScope,\n\t): ProviderWarningCause | undefined;\n\n\t/**\n\t * Settles what a read's only-scoped authentication failures mean, when the provider can tell (see\n\t * {@link validateCredential}).\n\t *\n\t * A refused credential is thrown: the caller's `catch` then fails the read as a whole, exactly as it does when\n\t * discovery is not cached and the first request is the one refused. Without this, a token revoked after\n\t * discovery would publish scoped `auth` warnings, which a consumer must not answer with a reconnect (see\n\t * `ProviderWarning.scope`), next to results served from the cache, for as long as the token stays stored.\n\t *\n\t * A confirmed credential makes each refusal the scope's own, so its cause is named through\n\t * {@link describeRefusal}. A probe that fails for another reason proves nothing either way and leaves the read\n\t * as it was.\n\t *\n\t * A refusal the provider pins on the credential itself (see {@link isCredentialRefusal}) is published for the\n\t * connection instead, without a probe.\n\t *\n\t * A token that passed within the last minute is not probed again, so a scope that keeps refusing it costs one\n\t * probe a minute, and a revocation right after a probe can take up to a minute to surface: until then its\n\t * refusals stay scoped, and may be named. A probe that proved nothing is not remembered, so the next read probes\n\t * again, and a credential no probe can confirm (e.g. a Bitbucket Data Center project access token) is probed on\n\t * every read that has scoped refusals.\n\t */\n\tprotected async confirmScopedAuthFailures(\n\t\tsession: ProviderAuthenticationSession,\n\t\tmetadata: CollectionMetadata | undefined,\n\t): Promise<void> {\n\t\tif (!hasOnlyScopedAuthFailures(metadata)) return;\n\n\t\t// Published for the connection, as a refusal outside any scope would be, while the scopes that answered keep\n\t\t// their results. That already asks for a reconnect, so there is nothing left for a probe to settle.\n\t\tconst isCredentialRefusal = this.isCredentialRefusal?.bind(this);\n\t\tif (isCredentialRefusal != null && markCredentialRefusals(metadata, isCredentialRefusal)) return;\n\n\t\tconst validateCredential = this.validateCredential?.bind(this);\n\t\tif (validateCredential == null) return;\n\n\t\t// Keyed by the address as well as the token: a self-managed instance serves every installation on its host,\n\t\t// and one installation accepting a token says nothing about another.\n\t\tconst key = [session.domain, session.baseUrl ?? '', session.accessToken].join('\\n');\n\t\ttry {\n\t\t\tawait this._validatedCredentials.getOrCreate(key, () => validateCredential(session));\n\t\t} catch (ex) {\n\t\t\t// A 403 answered a credential that authenticated and was only denied the check's own request, so it proves\n\t\t\t// nothing about the scopes' refusals either.\n\t\t\tif (ex instanceof AuthenticationError && ex.reason !== AuthenticationErrorReason.Forbidden) throw ex;\n\n\t\t\treturn;\n\t\t}\n\n\t\tconst describeRefusal = this.describeRefusal?.bind(this);\n\t\tif (describeRefusal != null) {\n\t\t\tattributeScopedAuthFailures(metadata, (refusal, scope) => describeRefusal(session, refusal, scope));\n\t\t}\n\t}\n\n\tprotected handleProviderException(\n\t\tsyncReqUsecase: SyncReqUsecase,\n\t\tex: Error,\n\t\toptions?: { scope?: ScopedLogger | undefined; silent?: boolean; connectionId?: string },\n\t): void {\n\t\tif (isCancellationError(ex)) return;\n\n\t\t// A refused credential may be one a probe passed moments ago: stop vouching for it.\n\t\tif (ex instanceof AuthenticationError) {\n\t\t\tthis._validatedCredentials.clear();\n\t\t}\n\n\t\tif (options?.scope != null) {\n\t\t\toptions.scope.error(ex);\n\t\t} else {\n\t\t\tLogger.error(ex);\n\t\t}\n\n\t\t// A per-connection (multi-account) read resolved its session through `resolveReadSession`'s\n\t\t// `connectionId` branch, which deliberately never touches the cached primary `_session`. So the\n\t\t// primary-session recovery below cannot apply to it: expiring `_session` would mark a session this\n\t\t// read never used, while the rejected connection kept its stored token and re-sent it on every\n\t\t// later read — a token the provider has already refused (expired scopes, a revoked grant, an\n\t\t// uninstalled app) is not self-healing, so the read failed identically until the user reconnected\n\t\t// by hand. Record the rejection against the connection instead; `resolveReadSession` consumes it\n\t\t// and forces the cloud `/refresh` on the next read of that connection. When there is nothing to\n\t\t// recover, fall through to the shared failure budget, which disconnects after\n\t\t// `requestExceptionLimit` and surfaces the reconnect prompt.\n\t\tif (ex instanceof AuthenticationError && options?.connectionId) {\n\t\t\tif (!this._rejectedTokens.recordRejection(options.connectionId)) {\n\t\t\t\tthis.trackRequestException(options);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (ex instanceof AuthenticationError && this._session?.cloud) {\n\t\t\tif (!this.hasSessionSyncRequests()) {\n\t\t\t\tthis.requestSessionSyncForUsecase(syncReqUsecase);\n\t\t\t\tthis._session = {\n\t\t\t\t\t...this._session,\n\t\t\t\t\texpiresAt: new Date(Date.now() - 1),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tthis.trackRequestException(options);\n\t\t\t}\n\t\t} else if (ex instanceof AuthenticationError || ex instanceof RequestClientError) {\n\t\t\tthis.trackRequestException(options);\n\t\t}\n\t}\n\n\tprivate missingExpirityReported = false;\n\t@gate()\n\tprotected async refreshSessionIfExpired(scope?: ScopedLogger): Promise<void> {\n\t\tif (this._session?.expiresAt != null && this._session.expiresAt < new Date()) {\n\t\t\t// The current session is expired, so get the latest from the cloud and refresh if needed\n\t\t\ttry {\n\t\t\t\tawait this.syncCloudConnection('connected', true);\n\t\t\t} catch (ex) {\n\t\t\t\tscope?.error(ex);\n\t\t\t}\n\t\t} else if (\n\t\t\tthis._session?.expiresAt == null &&\n\t\t\tthis.id !== GitCloudHostIntegrationId.GitHub &&\n\t\t\t!this.missingExpirityReported\n\t\t) {\n\t\t\tthis.ctx.hooks?.session?.onRefreshSkipped?.({\n\t\t\t\tid: this.id,\n\t\t\t\treason: 'missing-expiry',\n\t\t\t\tcloud: this._session?.cloud,\n\t\t\t});\n\t\t\tthis.missingExpirityReported = true;\n\t\t}\n\t}\n\n\t@trace()\n\ttrackRequestException(options?: { silent?: boolean }): void {\n\t\tthis.requestExceptionCount++;\n\n\t\tif (this.requestExceptionCount >= IntegrationBase.requestExceptionLimit && this._session !== null) {\n\t\t\tif (!options?.silent) {\n\t\t\t\tthis.ctx.hooks?.ui?.onDisconnectedAfterTooManyFailures?.(this.name);\n\t\t\t}\n\t\t\tvoid this.disconnect({ currentSessionOnly: true });\n\t\t}\n\t}\n\n\t@gate()\n\t@trace({ exit: true })\n\tasync isConnected(): Promise<boolean> {\n\t\treturn (await this.getSession('integrations')) != null;\n\t}\n\n\t@gate()\n\tprivate async ensureSession(\n\t\toptions:\n\t\t\t| {\n\t\t\t\t\tcreateIfNeeded?: boolean;\n\t\t\t\t\tforceNewSession?: boolean;\n\t\t\t\t\tsync?: never;\n\t\t\t\t\tsource?: Sources;\n\t\t\t }\n\t\t\t| {\n\t\t\t\t\tcreateIfNeeded?: never;\n\t\t\t\t\tforceNewSession?: never;\n\t\t\t\t\tsync: boolean;\n\t\t\t\t\tsource?: Sources;\n\t\t\t },\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst { createIfNeeded, forceNewSession, source, sync } = options;\n\t\tif (this._session != null) {\n\t\t\tif (this.isSessionForIntegrationHost(this._session)) return this._session;\n\n\t\t\tthis._session = null;\n\t\t}\n\t\tif (this.ctx.config.isIntegrationsEnabled?.() === false) return undefined;\n\n\t\tif (createIfNeeded || sync) {\n\t\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\t\t} else if (this.ctx.storage.getWorkspace(this.connectedKey) === false) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tlet session: ProviderAuthenticationSession | undefined | null;\n\t\ttry {\n\t\t\tconst authProvider = await this.authenticationService.get(this.authProvider.id);\n\t\t\tsession = await authProvider.getSession(\n\t\t\t\tthis.authProviderDescriptor,\n\t\t\t\tsync\n\t\t\t\t\t? { sync: sync, source: source }\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tcreateIfNeeded: createIfNeeded,\n\t\t\t\t\t\t\tforceNewSession: forceNewSession,\n\t\t\t\t\t\t\tsource: source,\n\t\t\t\t\t\t},\n\t\t\t);\n\n\t\t\tif (session?.expiresAt != null && session.expiresAt < new Date()) {\n\t\t\t\tsession = null;\n\t\t\t}\n\t\t\tif (session != null && !this.isSessionForIntegrationHost(session)) {\n\t\t\t\tsession = undefined;\n\t\t\t}\n\t\t} catch (ex) {\n\t\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\n\t\t\t// Only interactive paths can prompt for consent, so a sync failure must reach the rethrow below.\n\t\t\tif (!sync && ex instanceof Error && ex.message.includes('User did not consent')) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// On a forced re-sync, propagate so syncCloudConnection can tell a failure from a definitive empty\n\t\t\t// result (#5569); other callers keep swallowing to null so reads never throw.\n\t\t\tif (sync) {\n\t\t\t\t// Throwing skips the reset below, and a lingering count blocks the next non-forced sync.\n\t\t\t\tthis.smoothifyRequestExceptionCount();\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\tsession = null;\n\t\t}\n\n\t\tif (session === undefined && !createIfNeeded && !sync) {\n\t\t\tawait this.ctx.storage.deleteWorkspace(this.connectedKey);\n\t\t}\n\n\t\tthis._session = session ?? null;\n\t\tthis.smoothifyRequestExceptionCount();\n\n\t\tif (session != null) {\n\t\t\tawait this.ctx.storage.storeWorkspace(this.connectedKey, true);\n\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tthis._onDidChange.fire();\n\t\t\t\tthis.didChangeConnection?.fire({ integration: this, key: this.key, reason: 'connected' });\n\t\t\t\t// Fired detached, so there is no caller left to catch anything: every implementor is async, and\n\t\t\t\t// a rejection would surface as a process-level unhandled rejection in the host. It is a\n\t\t\t\t// best-effort warm-up, so swallow the failure with a warning instead.\n\t\t\t\tvoid (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.providerOnConnect?.();\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tscope?.warn(\n\t\t\t\t\t\t\t`Failed to run providerOnConnect for ${this.key}: ${ex instanceof Error ? ex.message : String(ex)}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t}\n\n\t\treturn session ?? undefined;\n\t}\n\n\tgetIgnoreSSLErrors(): boolean | 'force' {\n\t\treturn this.authenticationService.ignoreSSLErrors(this);\n\t}\n\n\tasync searchMyIssues(\n\t\tresource?: ResourceDescriptor,\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined>;\n\tasync searchMyIssues(\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined>;\n\t@trace()\n\tasync searchMyIssues(\n\t\tresources?: ResourceDescriptor | ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchMyIssuesResult(resources, cancellation, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link searchMyIssues}. Recovers thrown errors into `{ error }` so callers\n\t * (e.g. the ProviderBackend account-wide issues read) can surface a per-provider warning instead of a\n\t * silent empty result. Returns the normalized {@link IssueShape} (there is no raw account-wide issue read).\n\t */\n\tasync searchMyIssuesResult(\n\t\tresources?: ResourceDescriptor | ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<IssueShape[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\tconst issues = await this.searchProviderMyIssues(\n\t\t\t\tsession,\n\t\t\t\tresources != null ? (Array.isArray(resources) ? resources : [resources]) : undefined,\n\t\t\t\tcancellation,\n\t\t\t);\n\t\t\tthis.resetRequestExceptionCount('searchMyIssues');\n\t\t\treturn { value: issues, duration: performance.now() - start };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });\n\t\t\treturn { error: toError(ex), duration: performance.now() - start };\n\t\t}\n\t}\n\n\tprotected abstract searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined>;\n\n\t/**\n\t * Paging/truncation-aware variant of {@link searchProviderMyIssues}. The default wraps the normalized read\n\t * as a complete single page; providers with native cursors or fan-out metadata override it.\n\t */\n\tprotected async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\t_options?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\t// The default read has no assignee scoping to broaden, so `_options` is inert here; a provider whose\n\t\t// account-wide read is user-scoped (GitHub/GitLab/Azure) overrides this and honors `includeAllAssignees`.\n\t\tconst values = await this.searchProviderMyIssues(session, resources, cancellation);\n\t\tif (values == null) return undefined;\n\t\treturn { values: values, truncated: false };\n\t}\n\n\t/**\n\t * Result-returning, truncation-aware account-wide issue read. Recovers thrown errors into `{ error }` and\n\t * carries the `truncated` flag so the ProviderBackend facade can report an incomplete read honestly.\n\t */\n\tasync searchMyIssuesWithTruncationResult(\n\t\tresources?: ResourceDescriptor | ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\tconnectionId?: string,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<IntegrationResult<AccountWideIssuesResult | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst result = await this.searchProviderMyIssuesWithTruncation(\n\t\t\t\tsession,\n\t\t\t\tresources != null ? (Array.isArray(resources) ? resources : [resources]) : undefined,\n\t\t\t\tcancellation,\n\t\t\t\toptions,\n\t\t\t);\n\t\t\tawait this.confirmScopedAuthFailures(session, result?.metadata);\n\t\t\tthis.resetRequestExceptionCount('searchMyIssues');\n\t\t\treturn { value: result };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t@trace()\n\tasync getLinkedIssueOrPullRequest(\n\t\tresource: T,\n\t\tlink: { id: string; key: string },\n\t\toptions?: { expiryOverride?: boolean | number; type?: IssueOrPullRequestType },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected) return undefined;\n\n\t\tawait this.refreshSessionIfExpired(scope);\n\n\t\tconst issueOrPR = this.ctx.cache.getIssueOrPullRequest(\n\t\t\tlink.key,\n\t\t\toptions?.type,\n\t\t\tresource,\n\t\t\tthis,\n\t\t\t() => ({\n\t\t\t\tvalue: (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.getProviderLinkedIssueOrPullRequest(\n\t\t\t\t\t\t\tthis._session!,\n\t\t\t\t\t\t\tresource,\n\t\t\t\t\t\t\tlink,\n\t\t\t\t\t\t\toptions?.type,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.resetRequestExceptionCount('getIssueOrPullRequest');\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tthis.handleProviderException('getIssueOrPullRequest', ex, { scope: scope });\n\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t}\n\t\t\t\t})(),\n\t\t\t}),\n\t\t\toptions,\n\t\t);\n\t\treturn issueOrPR;\n\t}\n\n\tprotected abstract getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t\tlink: { id: string; key: string },\n\t\ttype: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined>;\n\n\tasync getIssue(\n\t\tresource: T,\n\t\tid: string,\n\t\toptions?: { connectionId?: string; expiryOverride?: boolean | number },\n\t): Promise<Issue | undefined> {\n\t\treturn (await this.getIssueResult(resource, id, options))?.value;\n\t}\n\n\tgetIssueResult(\n\t\tresource: T,\n\t\tid: string,\n\t\toptions?: { connectionId?: string; expiryOverride?: boolean | number },\n\t): Promise<IntegrationResult<Issue | undefined>> {\n\t\treturn this.getIssueResultCore(resource, id, options, session => this.getProviderIssue(session, resource, id));\n\t}\n\n\t@trace()\n\tprotected async getIssueResultCore(\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t\toptions: { connectionId?: string; expiryOverride?: boolean | number } | undefined,\n\t\tgetProviderIssue: (session: ProviderAuthenticationSession) => Promise<Issue | undefined>,\n\t): Promise<IntegrationResult<Issue | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\tconst { connectionId: requestedConnectionId, expiryOverride } = options ?? {};\n\t\tconst connectionId = requestedConnectionId || undefined;\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst issue = await getCachedIssue({\n\t\t\t\tcache: this.ctx.cache,\n\t\t\t\tid: id,\n\t\t\t\tresource: resource,\n\t\t\t\tintegration: this,\n\t\t\t\tload: async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst issue = await getProviderIssue(session);\n\t\t\t\t\t\tthis.resetRequestExceptionCount('getIssue');\n\t\t\t\t\t\treturn issue;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tif (!isCancellationError(ex)) {\n\t\t\t\t\t\t\tthis.handleProviderException('getIssue', toError(ex), {\n\t\t\t\t\t\t\t\tscope: scope,\n\t\t\t\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow ex;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tcacheOptions: {\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t\texpiryOverride: expiryOverride,\n\t\t\t\t\tetag: `${this.id}:${this.maybeConnected ?? false}:${this.getSessionFingerprint(session)}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn { value: issue };\n\t\t} catch (ex) {\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t\tid: string,\n\t): Promise<Issue | undefined>;\n\n\tasync getCurrentAccount(options?: {\n\t\tavatarSize?: number;\n\t\tconnectionId?: string;\n\t\texpiryOverride?: boolean | number;\n\t}): Promise<Account | undefined> {\n\t\tconst scope = getScopedLogger();\n\t\tconst { connectionId: requestedConnectionId, expiryOverride, ...opts } = options ?? {};\n\t\tconst connectionId = requestedConnectionId || undefined;\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\tconst sessionFingerprint = this.getSessionFingerprint(session);\n\n\t\tconst currentAccount = await this.ctx.cache.getCurrentAccount(\n\t\t\tthis,\n\n\t\t\t(cacheable: any) => ({\n\t\t\t\tvalue: (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst account = await this.getProviderCurrentAccount?.(session, opts);\n\t\t\t\t\t\tthis.resetRequestExceptionCount('getCurrentAccount');\n\t\t\t\t\t\treturn account;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\tif (isCancellationError(ex)) {\n\t\t\t\t\t\t\tcacheable.invalidate();\n\t\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthis.handleProviderException('getCurrentAccount', ex, {\n\t\t\t\t\t\t\tscope: scope,\n\t\t\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Invalidate the cache on error, except for auth errors\n\t\t\t\t\t\tif (!(ex instanceof AuthenticationError)) {\n\t\t\t\t\t\t\tcacheable.invalidate();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Re-throw to the caller\n\t\t\t\t\t\tthrow ex;\n\t\t\t\t\t}\n\t\t\t\t})(),\n\t\t\t}),\n\t\t\t{\n\t\t\t\tconnectionId: connectionId,\n\t\t\t\texpiryOverride: expiryOverride,\n\t\t\t\texpireOnError: false,\n\t\t\t\tetag: `${this.id}:${this.maybeConnected ?? false}:${sessionFingerprint}`,\n\t\t\t},\n\t\t);\n\t\treturn currentAccount;\n\t}\n\n\tprotected getProviderCurrentAccount?(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions?: { avatarSize?: number },\n\t): Promise<Account | undefined>;\n\n\t/**\n\t * Resolves the account for a specific session/token — including connections other than the current\n\t * primary (multi-account) — using this integration's provider API base URL and auth type. Returns\n\t * undefined when the provider doesn't support account lookup. Uncached (callers cache per connection).\n\t */\n\tgetProviderAccountForSession(session: ProviderAuthenticationSession): Promise<Account | undefined> {\n\t\treturn this.getProviderCurrentAccount?.(session) ?? Promise.resolve(undefined);\n\t}\n\n\t@trace()\n\tasync getPullRequest(\n\t\tresource: T,\n\t\tid: string,\n\t\toptions?: { expiryOverride?: boolean | number; throwOnError?: boolean },\n\t): Promise<PullRequest | undefined> {\n\t\tconst scope = getScopedLogger();\n\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected) return undefined;\n\n\t\tawait this.refreshSessionIfExpired(scope);\n\n\t\tconst pr = await this.ctx.cache.getPullRequest(\n\t\t\tid,\n\t\t\tresource,\n\t\t\tthis,\n\t\t\tcacheable => ({\n\t\t\t\tvalue: (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await this.getProviderPullRequest?.(this._session!, resource, id);\n\t\t\t\t\t\tthis.resetRequestExceptionCount('getPullRequest');\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\t// A failed lookup is not an answer — and the by-id bucket never expires a miss.\n\t\t\t\t\t\tcacheable.invalidate();\n\t\t\t\t\t\tthis.handleProviderException('getPullRequest', ex, { scope: scope });\n\t\t\t\t\t\tif (options?.throwOnError) throw ex;\n\n\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t}\n\t\t\t\t})(),\n\t\t\t}),\n\t\t\t{ expiryOverride: options?.expiryOverride },\n\t\t);\n\t\treturn pr;\n\t}\n\n\tprotected getProviderPullRequest?(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t\tid: string,\n\t): Promise<PullRequest | undefined>;\n}\n"],"mappings":"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"}
@@ -38,7 +38,24 @@ export type ProviderIssueSearchPage = {
38
38
  hasMore: boolean;
39
39
  page: number;
40
40
  totalCount?: number;
41
+ /**
42
+ * The provider's declared result ceiling was reached, whether or not it reported how many matched. Lets the
43
+ * facade report the ceiling as a `provider-limit` omission for a provider that can prove more matched than it
44
+ * serves but can't say how many (Azure DevOps Server's WIQL), instead of a generic "was truncated".
45
+ */
46
+ limitReached?: boolean;
41
47
  };
48
+ /**
49
+ * One scope's answer from the issue count probe (`countIssuesResult`), positionally. The pull-request probe's slots
50
+ * are `ProviderPullRequestCount | Error`, with the same meaning for an `Error`.
51
+ *
52
+ * - a number is the match count;
53
+ * - `'exceeds-limit'` means more matched than the provider's declared result ceiling, and it can't say how many;
54
+ * - an `Error` refuses that ONE scope for its own reasons (the facade warns and drops only it, as it does for a
55
+ * scope it refuses itself);
56
+ * - `undefined` means the provider reported nothing for it — never zero matches.
57
+ */
58
+ export type ProviderSearchCount = number | 'exceeds-limit' | Error | undefined;
42
59
  /**
43
60
  * Options for an issue tracker's PROJECT-scoped read (`IssuesIntegration.getIssuesForProject*`), which is the
44
61
  * only issue surface a tracker has — its issues live under resource -> project, so there is no account-wide read
@@ -79,6 +79,7 @@ var IssuesIntegration = class extends IntegrationBase {
79
79
  if (session == null) return void 0;
80
80
  try {
81
81
  const projects = await this.getProviderProjectsForResourcesWithMetadata(session, resources);
82
+ await this.confirmScopedAuthFailures(session, projects.metadata);
82
83
  this.resetRequestExceptionCount("getProjectsForResources");
83
84
  return { value: projects };
84
85
  } catch (ex) {
@@ -128,6 +129,7 @@ var IssuesIntegration = class extends IntegrationBase {
128
129
  if (session == null) return void 0;
129
130
  try {
130
131
  const result = await this.getProviderIssuesForProjectWithTruncation(session, project, options);
132
+ await this.confirmScopedAuthFailures(session, result?.metadata);
131
133
  this.resetRequestExceptionCount("getIssuesForProject");
132
134
  return { value: result };
133
135
  } catch (ex) {