@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
package/CHANGELOG.md CHANGED
@@ -6,17 +6,41 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.7.2] - 2026-09-25
10
+
11
+ ### Fixed
12
+
13
+ - Fixes a `ProviderWarning` from a scoped authentication failure being indistinguishable from an account-wide one ([#5890](https://github.com/gitkraken/vscode-gitlens/issues/5890)). A fan-out read that records a failure against one organization, project or repository (for example an Azure DevOps organization with third-party OAuth access disabled answering its project discovery with a 401) produced the same `{ kind: 'auth', isAuth: true }` warning as a dead token, with the scope only in the English `message`, so a consumer could only prompt to reconnect, which can never fix it. Warnings derived from a structured scope failure now carry an optional `scope` (the new `ProviderWarningScope`: `resourceId`, `projectId`, `repositoryId`) naming the part of the read that failed. `kind` and `isAuth` are unchanged, and an absent `scope` still means account-wide or unattributed, so existing consumers keep their behavior. Azure DevOps, Bitbucket and Jira Cloud serve discovery from a per-token cache, where a revoked token would reach only the scopes still requested and look like one scope's refusal. Bitbucket Data Center's repo-scoped pull request read, an SDK fan-out with no request to the connection first, has the same shape. When a read's only authentication failures are scoped, these providers now confirm the credential with one uncached check (at most once a minute for a credential that passed it), and a refused credential fails the read as a connection failure instead, so a scoped `auth` warning means the credential itself was accepted. A refusal the provider pins on the credential, like Bitbucket's or Jira Cloud's for a token missing required OAuth scopes, which a reconnect fixes, is published unscoped too, once for the connection, while the scopes that answered keep their results. Such a warning now also says why the scope refused, through an optional `cause` (the new `ProviderWarningCause` and `ProviderWarningCauseReason`): `reason` is `oauth-app-not-allowed` (the organization's "Third-party application access via OAuth" policy is off, which is the default for new Azure DevOps organizations), `access-denied` or `conditional-access` (`VS403463`), with the provider's `code` and, for the OAuth policy, a `remedyUrl` to the organization's policy page, so a consumer can recommend the fix rather than a reconnect that could never heal it. It is named only once the credential is confirmed, because Azure DevOps answers a disallowed OAuth app with the same bare `401` as an expired token, and only Azure DevOps names causes today. A scope failure's `message` now also carries the provider's own explanation, or the error its response raised (a page reduced to its status), instead of the generic "credentials are either invalid or expired", which said the opposite of a scope refusing a sound credential. Failures of two scopes also stay two warnings rather than relying on their messages differing (plus/integrations)
14
+
15
+ ## [0.7.1] - 2026-09-24
16
+
17
+ ### Added
18
+
19
+ - Adds Bitbucket Data Center to the filtered pull-request search and its count probe ([#5875](https://github.com/gitkraken/vscode-gitlens/issues/5875)). `getSupportedFilters(BitbucketServer).pullRequestSearch` now declares the relationships `Author`, `ReviewRequested` and `Reviewed`, the states `open`/`closed`/`merged`/`all`, `text`, `draft`, `includeArchived`, the repository scope and the sorts `updated:desc`/`updated:asc` — and nothing its REST API cannot express: no assignee or mention (its pull requests have neither), no date filters, no `created` order, and no organization scope, since there is no project-wide pull-request list. `searchPullRequestsPage` reads each requested repository × relationship as its own request, or the user's dashboard for a relationship without repositories (`Reviewed` as its reviewer and participant lists, since the dashboard only filters by review status together with a role), follows every facet's own `nextPageStart` so matches past the first page are found, and binds its cursor to the connection, the configured installation URL (context path included) and the query, refusing it under anything else. Every criterion is re-checked on the rows returned, so a server that ignores one (`draft` before 8.18) narrows the page instead of widening it, and a repository that fails is reported as a scoped warning with `fetchFailed` while the others still answer, and is retried once at the page it missed before being dropped. `countPullRequests` applies the same facets and predicates, so its count is exactly the number of rows the search returns — the union of the requested states, unlike GitHub's per-state maximum. Bitbucket Data Center has no count query, so it reads each facet's first page of up to 1,000 and reports a larger scope as a floor through the new optional `PullRequestCountResult.lowerBound`. Its pull requests now also report `isDraft` from the server's `draft` field rather than always `false` (plus/integrations)
20
+ - Adds Azure DevOps Server to the filtered searches and their counts ([#5876](https://github.com/gitkraken/vscode-gitlens/issues/5876)). `searchIssuesPage` and `countIssues` run one collection-scoped WIQL query, so every relationship is OR-ed in a single query and the count is the exact size of the match set the search pages through. `searchPullRequestsPage` and `countPullRequests` drain each repository or project facet, apply text, draft and dates the pull request API can't filter by, union by repository identity and page the ordered result from a keyset cursor. `getSupportedFilters()` declares only what each search implements, a search that would span collections is refused rather than widened, and past a result ceiling the read reports a `provider-limit` omission (with `limit` and `sort`, and no invented total) and the count `exceedsProviderLimit` instead of an understated number. `ProviderIssueSearchPage`/`ProviderPullRequestSearchPage` gain `limitReached` for such a ceiling, and a provider count slot may now refuse its own scope without failing the batch (an `Error` in `ProviderSearchCount` for issues and in `ProviderPullRequestCount | Error` for pull requests), or, for issues, report `'exceeds-limit'`. Every request stays on the selected connection's installation, with the collection applied once. Scope names are no longer refused for spaces or quotes on this provider, since it never puts them in a query string. `broadenIssues` now uses the collection search for Azure DevOps Server instead of draining repositories. Azure DevOps Services is unchanged (plus/integrations)
21
+
22
+ ### Fixed
23
+
24
+ - Fixes domainless self-managed Git issue associations resolving through an arbitrary primary or cached host ([#5883](https://github.com/gitkraken/vscode-gitlens/issues/5883)). `getIssueFromGitConfigEntityIdentifier` accepts an optional `getConfiguredIntegrations` callback and resolves a legacy association only when those connections identify exactly one host. Missing configuration or multiple hosts leave it unresolved, including cache-only reads. New Bitbucket Server issue identifiers carry their domain; existing domainless identifiers retain the single-host read path, and pull request identifiers keep their persisted shape. Jira Server still requires an explicit domain (plus/integrations)
25
+ - Fixes `IntegrationService.getMyIssues` and `getMyPullRequests` reading only one host of a self-managed provider ([#5873](https://github.com/gitkraken/vscode-gitlens/issues/5873)). Both resolved each id with `get(id)` and no domain, which answers with a single instance (the first cached, else the primary configured host), so a user connected to two GitHub Enterprise servers, GitLab instances, Bitbucket Data Centers, Azure DevOps Servers or Jira Data Center instances got results from one of them and no sign the others were skipped. Every self-managed id is now read on each configured host, local and cloud connections alike, and both reads run within `providerFanOutConcurrency`. A failing host still leaves the others' results in place, and results are not deduplicated on key, since two hosts routinely share `owner/repo#number`. With `openRepositoriesOnly`, multiple hosts receive only repositories whose remotes match exactly one host: by host and port for web remotes, or by hostname for SSH remotes. Ambiguous SSH remotes are skipped. A single host retains all of its provider's open repositories, including SSH aliases. `getRemoteHostMatcher` and `isWebRemoteScheme` move to `utils/domain.utils` for that (plus/integrations)
26
+ - Fixes branch-associated GitHub Enterprise and GitLab self-hosted issues being read back from the wrong host. `getEntityIdentifierInput` only recorded a `domain` when it rewrote a `github`/`gitlab` provider id, so an issue read through the self-managed integration, which already carries the enterprise id, was encoded with no host. It now records the host for those issues. The resolver passed to `getIssueFromGitConfigEntityIdentifier` receives the identifier's `domain` as a second argument, which a self-managed provider must honor. A resolver that ignores it keeps compiling and keeps its old behavior. Pull request identifiers are unchanged, since they are also the Launchpad uuid that stored pins and snoozes key on, and legacy issue identifiers without a host use the single-host compatibility path described above (plus/integrations)
27
+ - Fixes Azure DevOps Server's relationship-filtered pull request and issue reads matching nothing or failing ([#5876](https://github.com/gitkraken/vscode-gitlens/issues/5876)). Verified against a live Azure DevOps Server 2020: a person has one identity id at the server level and another in each collection, and the creator/reviewer filters only match the collection's, so the account-wide "my pull requests" read returned an empty account. It now resolves the id from each collection's own connection data. The repo-scoped reads, and the identity lookup behind their filters, also sent a PAT as a bearer token, which Azure DevOps Server refuses with `401`; they now send it as the Basic credential every other Azure read uses. Azure DevOps Services keeps its single account id, and its PAT sessions now send the PAT the same way on those repo-scoped reads (plus/integrations)
28
+ - Fixes an Azure DevOps Server connection addressed at a collection (`https://server/tfs/DefaultCollection`) failing every account-wide read ([#5876](https://github.com/gitkraken/vscode-gitlens/issues/5876)). Azure DevOps Server only lists collections at the server level and answers the route below a collection with a 404 page, so discovery failed; it now reports the collection that address names, read from the collection's own connection data. Organization and project links, repository lists by organization, account-wide issue reads, single issue reads, merges and the legacy pull request search also applied the collection a second time on such an address, and now apply it once (plus/integrations)
29
+
9
30
  ## [0.7.0] - 2026-09-22
10
31
 
11
32
  ### Fixed
12
33
 
34
+ - Fixes self-managed integrations dropping the installation path from API requests, including Azure DevOps Server virtual directories, while keeping existing connections usable ([#5871](https://github.com/gitkraken/vscode-gitlens/issues/5871))
13
35
  - Rejects a provider response that carries an HTML page under a success status instead of handing it on as data. Azure DevOps answers a rejected credential by redirecting to its sign-in page, which then returns `203 text/html` rather than `401`, and `response.ok` spans the whole 200-299 range — so the page was read as the body and passed to provider code typed as the JSON it is not. It surfaced as an opaque `TypeError` deep inside a provider, or as nothing at all on a route returning `void`, which made a bad credential look like a successful write. Such a response now raises `UnexpectedHtmlResponseError`. On Azure DevOps, the one provider known to answer a rejected credential this way, it is classified as an authentication failure so a fan-out reports what it actually was; on every other provider it stays a request failure, because a 2xx page there is far more likely a maintenance or WAF interstitial and calling that an auth failure would expire a valid cloud session and count against the budget that disconnects the integration. A page mislabelled as JSON is caught the same way, since `<` can never open valid JSON, and content types are matched case-insensitively as the spec requires. An empty body is exempt, since a write answering `204` can carry a stale `text/html` from its endpoint's usual responses and a sign-in page is never empty. The response attached to the error is stripped of its credential-bearing headers (the session cookie Azure sets, the challenges whose payload names the tenant) and its body cut to a diagnostic prefix, because an error is something a consumer may reasonably log or forward. Azure DevOps' own REST client takes the same guard on its direct reads, where the page previously reached `rsp.json()` and died as a bare `SyntaxError` that every caller reported as a missing work item. An `AuthenticationError` now propagates out of every one of those reads — the work-item and linked issue-or-PR lookups, the PR-for-branch and PR-for-commit reads, the current-user read, the repository-metadata and default-branch probes, which degrade quietly on a 404 by design but must not on a rejected credential, and the work-item state-list read, whose empty-list fallback was cached and so pinned every work item of that type to an unknown state long after the session recovery should have run. Everything else keeps its existing degrade-to-undefined behavior. A non-2xx keeps its own status error, which says more
14
36
  - Stops pre-encoding the Azure DevOps credential now that `@gitkraken/provider-apis` 0.59.0 encodes it. That release made `isPAT` produce a real HTTP Basic header, `base64(':' + token)`, where it previously emitted the token verbatim after `Basic ` — so every caller had to encode the pair itself, as this package did via `convertTokentoPAT`. Left alone, the secret would have been encoded twice and refused by Azure DevOps on every SDK-backed read; the raw secret is now handed over and provider-apis encodes it. The one observable change is the Basic username, which goes from `PAT` to empty — the form Azure DevOps documents and the SDK verified live. Azure DevOps' own REST paths in this package build their header directly and are unaffected, as is `getCurrentUser`, which asks for a bearer token
15
37
  - Fixes account-wide GitHub issue reads ignoring `itemsPerPage`. The facade dropped the requested size before `searchMyIssues`, so its three aliased searches defaulted to 100 full issue nodes and could exceed GitHub's GraphQL complexity limit. The page size now reaches every alias; omitted values keep the existing 100-item default. (plus/integrations, plus/git-github)
16
38
 
17
39
  ### Added
18
40
 
19
- - Adds Jira Server/Data Center as a self-hosted issue integration, `jira-server` ([#5864](https://github.com/gitkraken/vscode-gitlens/issues/5864)). It is the first id that is both an issue tracker and bound to a customer-run host, so the two properties are no longer fused: `IssuesSelfManagedHostIntegrationId` names the new family and the host-keyed machinery (integration and secret keys, the `connected:${id}:${domain}` flag, domain normalization, per-host primary selection, multi-account reconcile) moves to `isSelfManagedHostIntegrationId`, which spans both families, while `isGitSelfManagedHostIntegrationId` stays exactly where a read goes on to ask for repositories, pull requests or a git remote. `IntegrationKey`/`IntegrationConnectedKey` widen to match. A self-hosted instance is modelled as its own single synthetic resource, which keeps the resource → project contract of `IssuesIntegration` intact so `listOrgs`, `listProjects` and `listIssueTrackerIssuesPage` work unchanged; `listIssueTrackerIssuesPage` gains the `domain` its siblings already took. Every read is addressed to the connection's own base URL, and `ProviderAuthenticationSession` gains an optional `baseUrl` carrying the address exactly as configured: `domain` is the identity a self-managed connection is keyed by and is normalized to a bare host, so an instance mounted below a context path (`https://jira.example.com/jira`) was previously addressed at the root. The new field is populated from the wire domain on both cloud-session paths, mirrored onto `ConfiguredIntegrationDescriptor` so reconciliation can tell a re-pointed connection from an unchanged one without reading its secret (a routine check-in now re-fetches a connection whose address moved, which a normalized domain alone cannot see, refreshes the warm integration so it re-resolves rather than staying on the old path, and keys the Jira Server project cache by the address it read from rather than by host and token, which a same-token move cannot distinguish). A backend response that reports no domain keeps the address already configured rather than writing none, so a refresh cannot silently drop a context path the user never changed, and refused when it names a different host than the connection's key; `baseUrlFromDomain` (the path-preserving sibling of `hostFromDomain`) builds the URL and is now shared rather than private to `gitHostIntegration.ts`. The account-wide "my issues" read reports `truncated` when it stops at its page backstop or on a continuation the server never advances, instead of publishing the prefix as a complete account. The host guard on a self-managed cloud session now refuses only a session that NAMES a different host, and only when the fetch was scoped by a connection id WE have stored for that host (a caller-supplied or stale id is not evidence, and neither is the absence of one): the backend may return no domain at all (`toSession` defaults it to `''`), and treating that as a mismatch stranded the refresh and left the connection looking permanently disconnected — but an UNSCOPED fetch reaches the provider-global primary endpoint, whose token can belong to any configured host, so a domainless response is still refused there rather than being bound to whatever domain the caller asked for. `Integration.icon` now reports `providersMetadata.iconKey` rather than the integration id, which is what that field documents itself as and what every consumer rendering a `gl-provider-<key>` glicon needs: Jira Data Center draws with Jira's glyph, and the self-managed GitHub and GitLab ids now report the keys their metadata declares instead of ids the icon registry has no entry for. The Commit Graph's issue-integration check enumerates every configured host for a self-managed tracker rather than asking a single resolved instance, so one disconnected host no longer disables issue enrichment while another stays connected. Project issue reads address the project by its numeric id rather than its display name, since JQL resolves a `project = "<value>"` clause against key before name and a name colliding with another project's key would read that project's issues. A key that names no issue is reported as a lookup miss rather than a request failure, matching Jira Cloud's `getJiraIssueByKey` and the generic `getIssue`. Branch-association identifiers encode the host and project, since two self-hosted instances routinely issue the same issue keys; decoding one back is not wired up yet, because the resolver behind it takes no domain and would answer from whichever connection is primary
41
+ - Adds paginated discovery of Bitbucket Data Center projects and repositories, including project-scoped reads, with connection isolation and warnings for incomplete results ([#5874](https://github.com/gitkraken/vscode-gitlens/issues/5874))
42
+ - Adds Jira Server/Data Center as a self-hosted issue integration, `jira-server` ([#5864](https://github.com/gitkraken/vscode-gitlens/issues/5864)). It is the first id that is both an issue tracker and bound to a customer-run host, so the two properties are no longer fused: `IssuesSelfManagedHostIntegrationId` names the new family and the host-keyed machinery (integration and secret keys, the `connected:${id}:${domain}` flag, domain normalization, per-host primary selection, multi-account reconcile) moves to `isSelfManagedHostIntegrationId`, which spans both families, while `isGitSelfManagedHostIntegrationId` stays exactly where a read goes on to ask for repositories, pull requests or a git remote. `IntegrationKey`/`IntegrationConnectedKey` widen to match. A self-hosted instance is modelled as its own single synthetic resource, which keeps the resource → project contract of `IssuesIntegration` intact so `listOrgs`, `listProjects` and `listIssueTrackerIssuesPage` work unchanged; `listIssueTrackerIssuesPage` gains the `domain` its siblings already took. Every read is addressed to the connection's own base URL, and `ProviderAuthenticationSession` gains an optional `baseUrl` carrying the address exactly as configured: `domain` is the identity a self-managed connection is keyed by and is normalized to a bare host, so an instance mounted below a context path (`https://jira.example.com/jira`) was previously addressed at the root. The new field is populated from the wire domain on both cloud-session paths, mirrored onto `ConfiguredIntegrationDescriptor` so reconciliation can tell a re-pointed connection from an unchanged one without reading its secret (a routine check-in now re-fetches a connection whose address moved, which a normalized domain alone cannot see, refreshes the warm integration so it re-resolves rather than staying on the old path, and keys the Jira Server project cache by the address it read from rather than by host and token, which a same-token move cannot distinguish). A backend response that reports no domain keeps the address already configured rather than writing none, so a refresh cannot silently drop a context path the user never changed, and refused when it names a different host than the connection's key; `baseUrlFromDomain` (the path-preserving sibling of `hostFromDomain`) builds the URL and is now shared rather than private to `gitHostIntegration.ts`. The account-wide "my issues" read reports `truncated` when it stops at its page backstop or on a continuation the server never advances, instead of publishing the prefix as a complete account. The host guard on a self-managed cloud session now refuses only a session that NAMES a different host, and only when the fetch was scoped by a connection id WE have stored for that host (a caller-supplied or stale id is not evidence, and neither is the absence of one): the backend may return no domain at all (`toSession` defaults it to `''`), and treating that as a mismatch stranded the refresh and left the connection looking permanently disconnected — but an UNSCOPED fetch reaches the provider-global primary endpoint, whose token can belong to any configured host, so a domainless response is still refused there rather than being bound to whatever domain the caller asked for. `Integration.icon` now reports `providersMetadata.iconKey` rather than the integration id, which is what that field documents itself as and what every consumer rendering a `gl-provider-<key>` glicon needs: Jira Data Center draws with Jira's glyph, and the self-managed GitHub and GitLab ids now report the keys their metadata declares instead of ids the icon registry has no entry for. The Commit Graph's issue-integration check enumerates every configured host for a self-managed tracker rather than asking a single resolved instance, so one disconnected host no longer disables issue enrichment while another stays connected. Project issue reads address the project by its numeric id rather than its display name, since JQL resolves a `project = "<value>"` clause against key before name and a name colliding with another project's key would read that project's issues. A key that names no issue is reported as a lookup miss rather than a request failure, matching Jira Cloud's `getJiraIssueByKey` and the generic `getIssue`. Branch-association identifiers encode the host and project, since two self-hosted instances routinely issue the same issue keys, and decode back through the host they name (#5872, below)
43
+ - Threads the host through the two point reads that serve a Jira Data Center issue, so a branch association for one now round-trips and `getTrackerIssue` serves the tracker ([#5872](https://github.com/gitkraken/vscode-gitlens/issues/5872)). `getIssueFromGitConfigEntityIdentifier`'s resolver now takes `(id, domain?)`, fed from the identifier: a Jira Data Center identifier resolves the integration for the host it names, and one that names no host is dropped rather than answered from the primary connection, which would return a different instance's issue under the same key. Identifiers encoded for the other self-managed hosts (GitHub Enterprise, GitLab Self-Managed, Azure DevOps Server) likewise resolve the integration for the host they carry rather than the primary configured one. `getTrackerIssue` gains the `domain` its paged siblings already took; `resourceId` for Jira Data Center is the host, its single resource, and no `resourceUrl` is needed because the browser link is built from the connection's own base URL. Unlike the paged reads it never falls back to the primary connection for a self-managed tracker: it requires a `domain` or a `connectionId` with a configured host and refuses otherwise, because its `issue: undefined` is a proven absence a caller may cache and two self-hosted instances routinely issue the same keys. `JiraServerIntegration` implements the resource-id point read behind it, addressed to the session's base URL like every other read, so `supportsIssueLookupByResourceId` now reports true for it
20
44
  - Adds `IssueShape.iterations` and `Issue.iterations` so consumers can access Jira sprints and Azure work-item iterations, preserving provider-reported activity and dates without inventing metadata for Azure. Coverage varies per read, and an absent value means the read couldn't report one rather than that the issue has no sprint — see "Sprints and iterations" in `docs/integrations.md`. Azure's iteration reaches both the direct work-item read, parsed here from `System.IterationPath`, and the SDK-backed reads, which requires `@gitkraken/provider-apis` 0.59.0 for the `iteration` field it normalizes; the direct read mirrors that normalization exactly — the path is the identity and is kept verbatim, so one work item cannot end up with two identities depending on which read surfaced it ([#5856](https://github.com/gitkraken/vscode-gitlens/issues/5856)).
21
45
 
22
46
  ## [0.6.0] - 2026-09-14
@@ -354,7 +378,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
354
378
 
355
379
  - Initial release. Bundles `@gitlens/utils`, `@gitlens/git`, `@gitlens/git-cli`, `@gitlens/ai`, and `@gitlens/git-github` into a single core npm package with subpath exports.
356
380
 
357
- [unreleased]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.7.0...HEAD
381
+ [unreleased]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.7.2...HEAD
382
+ [0.7.2]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.7.1...gitkraken:releases/core/v0.7.2
383
+ [0.7.1]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.7.0...gitkraken:releases/core/v0.7.1
358
384
  [0.7.0]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.6.0...gitkraken:releases/core/v0.7.0
359
385
  [0.6.0]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.5.116...gitkraken:releases/core/v0.6.0
360
386
  [0.5.116]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.5.115...gitkraken:releases/core/v0.5.116
@@ -29,10 +29,10 @@ function parseVstsHttpsUrl(url) {
29
29
  }
30
30
  const azureHttpsUrlRegex2 = /([^/]+)\/([^/]+)\/_git\/([^/]+)/;
31
31
  function parseAzureRepositoryPath(path, legacyVsts, cloud) {
32
- const segments = path.split("/");
32
+ const segments = path.split("/").map((segment) => decodeURIComponent(segment));
33
33
  const gitIndex = segments.length - 2;
34
34
  const projectIndex = gitIndex - 1;
35
- if (gitIndex < 1 || segments[gitIndex]?.toLowerCase() !== "_git" || segments.some((segment) => !segment || segment === "." || segment === "..")) return;
35
+ if (gitIndex < 1 || segments[gitIndex]?.toLowerCase() !== "_git" || segments.some((segment) => !segment || segment === "." || segment === ".." || /[/\\]/.test(segment))) return;
36
36
  const project = segments[projectIndex];
37
37
  const repository = segments[gitIndex + 1];
38
38
  if (!project || !repository) return void 0;
@@ -87,8 +87,8 @@ var AzureDevOpsRemoteProvider = class AzureDevOpsRemoteProvider extends RemotePr
87
87
  } else path = `${org}/${project}/_git/${rest}`;
88
88
  }
89
89
  }
90
- path = decodeURIComponent(path);
91
90
  const repositoryPath = parseAzureRepositoryPath(path, isVsts, domain.toLowerCase() === "dev.azure.com");
91
+ path = decodeURIComponent(path);
92
92
  super(domain, path, protocol, name, void 0, context);
93
93
  this.collection = repositoryPath?.collection;
94
94
  this.project = repositoryPath?.project;
@@ -1 +1 @@
1
- {"version":3,"file":"azure-devops.js","names":[],"sources":["../../../../git/src/remotes/azure-devops.ts"],"sourcesContent":["import * as l10n from '@vscode/l10n';\nimport type { Brand, Unbrand } from '@gitlens/utils/brand.js';\nimport type { Uri } from '@gitlens/utils/uri.js';\nimport type { RemoteProviderContext } from '../context.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '../models/autolink.js';\nimport type { LineRange } from '../models/lineRange.js';\nimport type { ParsedRemoteFileUri, RemoteProviderId } from '../models/remoteProvider.js';\nimport { RemoteProvider } from '../models/remoteProvider.js';\nimport type { CreatePullRequestRemoteResource } from '../models/remoteResource.js';\nimport type { GkProviderId } from '../models/repositoryIdentities.js';\nimport type { ResourceDescriptor } from '../models/resourceDescriptor.js';\nimport type { GitRevisionRangeNotation } from '../models/revision.js';\n\nconst gitRegex = /\\/_git\\/?/i;\nconst gitTailRegex = /\\/_git(?:\\/.*)?$/i;\nconst legacyDefaultCollectionRegex = /^DefaultCollection\\//i;\nconst orgAndProjectRegex = /^(.*?)\\/(.*?)\\/(.*)/;\nconst sshDomainRegex = /^(ssh|vs-ssh)\\./i;\nconst sshPathRegex = /^\\/?v\\d\\//i;\n\nconst azureSshUrlRegex = /^(?:[^@]+@)?([^:]+):v\\d\\//;\n\nconst vstsHostnameSuffix = '.visualstudio.com';\n\nexport function isVsts(domain: string): boolean {\n\treturn domain.endsWith(vstsHostnameSuffix);\n}\n\nfunction getVSTSOwner(url: URL): string {\n\treturn url.hostname.split('.')[0];\n}\n\nconst azureProjectRepoRegex = /([^/]+)\\/_git\\/([^/]+)/;\n\nfunction parseVstsHttpsUrl(url: URL): [string, string, string] {\n\tconst owner = getVSTSOwner(url);\n\tconst match = azureProjectRepoRegex.exec(url.pathname);\n\tif (match == null) throw new Error(`Invalid VSTS URL: ${url.toString()}`);\n\n\tconst [, project, repo] = match;\n\treturn [owner, project, repo];\n}\n\nconst azureHttpsUrlRegex2 = /([^/]+)\\/([^/]+)\\/_git\\/([^/]+)/;\n\ntype AzureRepositoryPath = {\n\tcollection: string | undefined;\n\tproject: string;\n\trepositoryPath: string;\n\tvirtualDirectory: string | undefined;\n};\n\nexport type AzureDevOpsRepositoryDescriptor = ResourceDescriptor & {\n\towner: string | undefined;\n\tname: string | undefined;\n\tvirtualDirectory?: string;\n};\n\nfunction parseAzureRepositoryPath(path: string, legacyVsts: boolean, cloud: boolean): AzureRepositoryPath | undefined {\n\tconst segments = path.split('/');\n\tconst gitIndex = segments.length - 2;\n\tconst projectIndex = gitIndex - 1;\n\tif (\n\t\tgitIndex < 1 ||\n\t\tsegments[gitIndex]?.toLowerCase() !== '_git' ||\n\t\tsegments.some(segment => !segment || segment === '.' || segment === '..')\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst project = segments[projectIndex];\n\tconst repository = segments[gitIndex + 1];\n\tif (!project || !repository) return undefined;\n\n\tconst collectionPath = segments.slice(0, projectIndex);\n\tif (legacyVsts) {\n\t\tif (collectionPath.length !== 0) return undefined;\n\n\t\treturn {\n\t\t\tcollection: undefined,\n\t\t\tproject: project,\n\t\t\trepositoryPath: `${project}/_git/${repository}`,\n\t\t\tvirtualDirectory: undefined,\n\t\t};\n\t}\n\n\tif (collectionPath.length === 0 || (cloud && collectionPath.length !== 1)) return undefined;\n\n\treturn {\n\t\tcollection: collectionPath.at(-1),\n\t\tproject: project,\n\t\trepositoryPath: `${project}/_git/${repository}`,\n\t\tvirtualDirectory: collectionPath.length > 1 ? collectionPath.slice(0, -1).join('/') : undefined,\n\t};\n}\n\nfunction parseAzureNewStyleUrl(url: URL): [string, string, string] {\n\tconst match = azureHttpsUrlRegex2.exec(url.pathname);\n\tif (match == null) throw new Error(`Invalid Azure URL: ${url.toString()}`);\n\n\tconst [, owner, project, repo] = match;\n\treturn [owner, project, repo];\n}\n\nexport function parseAzureHttpsUrl(url: string): [owner: string, project: string, repo: string];\nexport function parseAzureHttpsUrl(urlObj: URL): [owner: string, project: string, repo: string];\nexport function parseAzureHttpsUrl(arg: URL | string): [owner: string, project: string, repo: string] {\n\tconst url = typeof arg === 'string' ? new URL(arg) : arg;\n\tif (url.hostname.endsWith(vstsHostnameSuffix)) return parseVstsHttpsUrl(url);\n\treturn parseAzureNewStyleUrl(url);\n}\n\nexport class AzureDevOpsRemoteProvider extends RemoteProvider<AzureDevOpsRepositoryDescriptor> {\n\tprivate readonly collection: string | undefined;\n\tprivate readonly project: string | undefined;\n\tprivate readonly repositoryPath: string | undefined;\n\tprivate readonly virtualDirectory: string | undefined;\n\n\tconstructor(\n\t\tdomain: string,\n\t\tpath: string,\n\t\tprotocol?: string,\n\t\tname?: string,\n\t\tisVsts: boolean = false,\n\t\tcontext?: RemoteProviderContext,\n\t) {\n\t\tif (sshDomainRegex.test(domain)) {\n\t\t\tpath = path.replace(sshPathRegex, '');\n\t\t\tdomain = domain.replace(sshDomainRegex, '');\n\n\t\t\t// Add in /_git/ into ssh urls\n\t\t\tconst match = orgAndProjectRegex.exec(path);\n\t\t\tif (match != null) {\n\t\t\t\tconst [, org, project, rest] = match;\n\n\t\t\t\t// VSTS puts the org in the subdomain; modern Azure DevOps puts it in the path\n\t\t\t\tif (isVsts) {\n\t\t\t\t\tdomain = `${org}.${domain}`;\n\t\t\t\t\tpath = `${project}/_git/${rest}`;\n\t\t\t\t} else {\n\t\t\t\t\tpath = `${org}/${project}/_git/${rest}`;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Azure DevOps allows projects and repository names with spaces. In that situation,\n\t\t// the `path` will be previously encoded during git clone\n\t\t// revert that encoding to avoid double-encoding by gitlens during copy remote and open remote\n\t\tpath = decodeURIComponent(path);\n\t\tconst repositoryPath = parseAzureRepositoryPath(path, isVsts, domain.toLowerCase() === 'dev.azure.com');\n\t\tsuper(domain, path, protocol, name, undefined, context);\n\t\tthis.collection = repositoryPath?.collection;\n\t\tthis.project = repositoryPath?.project;\n\t\tthis.repositoryPath = repositoryPath?.repositoryPath;\n\t\tthis.virtualDirectory = repositoryPath?.virtualDirectory;\n\t}\n\n\tprotected override get issueLinkPattern(): string {\n\t\tconst projectUrl = this.baseUrl.replace(gitTailRegex, '');\n\t\tconst orgUrl = projectUrl.substring(0, projectUrl.lastIndexOf('/'));\n\t\treturn `${orgUrl}/_workitems/edit/<num>`;\n\t}\n\n\tprivate _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;\n\toverride get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {\n\t\t// Strip off any `_git` part from the repo url\n\t\tthis._autolinks ??= [\n\t\t\t...super.autolinks,\n\t\t\t{\n\t\t\t\tprefix: '#',\n\t\t\t\turl: this.issueLinkPattern,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Work Item #{number} on {provider}', { number: '<num>', provider: this.name }),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{provider} Work Item #{number}', { provider: this.name, number: '<num>' }),\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Default Pull request message when merging a PR in ADO. Will not catch commits & pushes following a different pattern.\n\t\t\t\tprefix: 'PR ',\n\t\t\t\turl: `${this.baseUrl}/pullrequest/<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Pull Request #{number} on {provider}', { number: '<num>', provider: this.name }),\n\n\t\t\t\ttype: 'pullrequest',\n\t\t\t\tdescription: l10n.t('{provider} Pull Request #{number}', {\n\t\t\t\t\tprovider: this.name,\n\t\t\t\t\tnumber: '<num>',\n\t\t\t\t}),\n\t\t\t},\n\t\t];\n\t\treturn this._autolinks;\n\t}\n\n\toverride get icon(): string {\n\t\treturn 'azdo';\n\t}\n\n\tget id(): RemoteProviderId {\n\t\treturn 'azure-devops';\n\t}\n\n\tget gkProviderId(): GkProviderId {\n\t\treturn 'azureDevops' satisfies Unbrand<GkProviderId> as Brand<GkProviderId>;\n\t}\n\n\tget name(): string {\n\t\treturn 'Azure DevOps';\n\t}\n\n\toverride get owner(): string | undefined {\n\t\tif (isVsts(this.domain)) {\n\t\t\treturn this.domain.split('.')[0];\n\t\t}\n\t\treturn this.collection ?? super.owner;\n\t}\n\n\toverride get repoName(): string | undefined {\n\t\tif (isVsts(this.domain)) {\n\t\t\treturn this.path;\n\t\t}\n\t\treturn this.repositoryPath ?? super.repoName;\n\t}\n\n\toverride get repoDesc(): AzureDevOpsRepositoryDescriptor {\n\t\tconst descriptor = super.repoDesc;\n\t\treturn this.virtualDirectory == null ? descriptor : { ...descriptor, virtualDirectory: this.virtualDirectory };\n\t}\n\n\toverride get providerDesc():\n\t\t| {\n\t\t\t\tid: GkProviderId;\n\t\t\t\trepoDomain: string;\n\t\t\t\trepoName: string;\n\t\t\t\trepoOwnerDomain: string;\n\t\t }\n\t\t| undefined {\n\t\tif (this.gkProviderId == null || this.owner == null || this.repoName == null || this.project == null) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn {\n\t\t\tid: this.gkProviderId,\n\t\t\trepoDomain: this.project,\n\t\t\trepoName: this.repoName,\n\t\t\trepoOwnerDomain: this.owner,\n\t\t};\n\t}\n\n\tprivate _displayPath: string | undefined;\n\toverride get displayPath(): string {\n\t\tthis._displayPath ??= this.path.replace(gitRegex, '/').replace(legacyDefaultCollectionRegex, '');\n\t\treturn this._displayPath;\n\t}\n\n\tprivate static readonly fileQueryRegex = /path=([^&]+)/i;\n\tprivate static readonly rangeQueryRegex = /line=(\\d+)(?:&lineEnd=(\\d+))?/;\n\n\toverride parseRemoteFileUri(uri: Uri): ParsedRemoteFileUri | undefined {\n\t\tif (uri.authority !== this.domain) return undefined;\n\n\t\tlet startLine;\n\t\tlet endLine;\n\t\tif (uri.query) {\n\t\t\tconst rangeMatch = AzureDevOpsRemoteProvider.rangeQueryRegex.exec(uri.query);\n\t\t\tif (rangeMatch != null) {\n\t\t\t\tconst [, start, end] = rangeMatch;\n\t\t\t\tif (start) {\n\t\t\t\t\tstartLine = parseInt(start, 10);\n\t\t\t\t\tif (end) {\n\t\t\t\t\t\tendLine = parseInt(end, 10);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst fileMatch = AzureDevOpsRemoteProvider.fileQueryRegex.exec(uri.query);\n\t\tif (fileMatch == null) return undefined;\n\n\t\tconst [, path] = fileMatch;\n\n\t\treturn {\n\t\t\tstartLine: startLine,\n\t\t\tendLine: endLine,\n\t\t\tcandidates: [{ type: 'pathOnly', filePath: path }],\n\t\t};\n\t}\n\n\tprotected getUrlForBranches(): string {\n\t\treturn this.encodeUrl(`${this.baseUrl}/branches`);\n\t}\n\n\tprotected getUrlForBranch(branch: string): string {\n\t\treturn this.encodeUrl(`${this.baseUrl}/?version=GB${branch}&_a=history`);\n\t}\n\n\tprotected getUrlForCommit(sha: string): string {\n\t\treturn this.encodeUrl(`${this.baseUrl}/commit/${sha}`);\n\t}\n\n\tprotected override getUrlForComparison(base: string, head: string, _notation: GitRevisionRangeNotation): string {\n\t\treturn this.encodeUrl(`${this.baseUrl}/branchCompare?baseVersion=GB${base}&targetVersion=GB${head}`);\n\t}\n\n\tprotected override async getUrlForCreatePullRequest({\n\t\tbase,\n\t\thead,\n\t}: CreatePullRequestRemoteResource): Promise<string | undefined> {\n\t\tconst query = new URLSearchParams({ sourceRef: head.branch, targetRef: base.branch ?? '' });\n\n\t\tif (base.remote.url !== head.remote.url) {\n\t\t\tconst parsedBaseUrl = parseAzureUrl(base.remote.url);\n\t\t\tif (parsedBaseUrl == null) return undefined;\n\n\t\t\tconst { org: baseOrg, project: baseProject, repo: baseName } = parsedBaseUrl;\n\t\t\tconst repoInfo = await this.context?.getRepositoryInfo?.(this.id, {\n\t\t\t\towner: baseOrg,\n\t\t\t\tname: baseName,\n\t\t\t\tproject: baseProject,\n\t\t\t});\n\t\t\tif (!repoInfo) return undefined;\n\n\t\t\tquery.set('targetRepositoryId', repoInfo.id);\n\t\t}\n\n\t\treturn `${this.encodeUrl(`${this.getRepoBaseUrl(head.remote.path)}/pullrequestcreate`)}?${query.toString()}`;\n\t}\n\n\tprotected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: LineRange): string {\n\t\tlet line;\n\t\tif (range != null) {\n\t\t\tif (range.startLine === range.endLine) {\n\t\t\t\tline = `&line=${range.startLine}&lineStartColumn=${range.startCharacter}&lineEndColumn=${range.endCharacter}`;\n\t\t\t} else {\n\t\t\t\tline = `&line=${range.startLine}&lineEnd=${range.endLine}&lineStartColumn=${range.startCharacter}&lineEndColumn=${range.endCharacter}`;\n\t\t\t}\n\t\t} else {\n\t\t\tline = '';\n\t\t}\n\n\t\tif (sha) return this.encodeUrl(`${this.baseUrl}?path=${fileName}&version=GC${sha}${line}&_a=contents`);\n\t\tif (branch) return this.encodeUrl(`${this.baseUrl}/?path=/${fileName}&version=GB${branch}&_a=contents${line}`);\n\t\treturn this.encodeUrl(`${this.baseUrl}?path=/${fileName}${line}`);\n\t}\n}\n\nfunction parseAzureUrl(url: string): { org: string; project: string; repo: string } | undefined {\n\tif (azureSshUrlRegex.test(url)) {\n\t\t// Examples of SSH urls:\n\t\t// - old one: bbbchiv@vs-ssh.visualstudio.com:v3/bbbchiv/MyFirstProject/test\n\t\t// - modern one: git@ssh.dev.azure.com:v3/bbbchiv2/MyFirstProject/test\n\t\turl = url.replace(azureSshUrlRegex, '');\n\t\tconst match = orgAndProjectRegex.exec(url);\n\t\tif (match != null) {\n\t\t\tconst [, org, project, rest] = match;\n\t\t\treturn { org: org, project: project, repo: rest };\n\t\t}\n\t} else {\n\t\tconst [org, project, rest] = parseAzureHttpsUrl(url);\n\t\treturn { org: org, project: project, repo: rest };\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"azure-devops.js","names":[],"sources":["../../../../git/src/remotes/azure-devops.ts"],"sourcesContent":["import * as l10n from '@vscode/l10n';\nimport type { Brand, Unbrand } from '@gitlens/utils/brand.js';\nimport type { Uri } from '@gitlens/utils/uri.js';\nimport type { RemoteProviderContext } from '../context.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '../models/autolink.js';\nimport type { LineRange } from '../models/lineRange.js';\nimport type { ParsedRemoteFileUri, RemoteProviderId } from '../models/remoteProvider.js';\nimport { RemoteProvider } from '../models/remoteProvider.js';\nimport type { CreatePullRequestRemoteResource } from '../models/remoteResource.js';\nimport type { GkProviderId } from '../models/repositoryIdentities.js';\nimport type { ResourceDescriptor } from '../models/resourceDescriptor.js';\nimport type { GitRevisionRangeNotation } from '../models/revision.js';\n\nconst gitRegex = /\\/_git\\/?/i;\nconst gitTailRegex = /\\/_git(?:\\/.*)?$/i;\nconst legacyDefaultCollectionRegex = /^DefaultCollection\\//i;\nconst orgAndProjectRegex = /^(.*?)\\/(.*?)\\/(.*)/;\nconst sshDomainRegex = /^(ssh|vs-ssh)\\./i;\nconst sshPathRegex = /^\\/?v\\d\\//i;\n\nconst azureSshUrlRegex = /^(?:[^@]+@)?([^:]+):v\\d\\//;\n\nconst vstsHostnameSuffix = '.visualstudio.com';\n\nexport function isVsts(domain: string): boolean {\n\treturn domain.endsWith(vstsHostnameSuffix);\n}\n\nfunction getVSTSOwner(url: URL): string {\n\treturn url.hostname.split('.')[0];\n}\n\nconst azureProjectRepoRegex = /([^/]+)\\/_git\\/([^/]+)/;\n\nfunction parseVstsHttpsUrl(url: URL): [string, string, string] {\n\tconst owner = getVSTSOwner(url);\n\tconst match = azureProjectRepoRegex.exec(url.pathname);\n\tif (match == null) throw new Error(`Invalid VSTS URL: ${url.toString()}`);\n\n\tconst [, project, repo] = match;\n\treturn [owner, project, repo];\n}\n\nconst azureHttpsUrlRegex2 = /([^/]+)\\/([^/]+)\\/_git\\/([^/]+)/;\n\ntype AzureRepositoryPath = {\n\tcollection: string | undefined;\n\tproject: string;\n\trepositoryPath: string;\n\tvirtualDirectory: string | undefined;\n};\n\nexport type AzureDevOpsRepositoryDescriptor = ResourceDescriptor & {\n\towner: string | undefined;\n\tname: string | undefined;\n\tvirtualDirectory?: string;\n};\n\nfunction parseAzureRepositoryPath(path: string, legacyVsts: boolean, cloud: boolean): AzureRepositoryPath | undefined {\n\tconst segments = path.split('/').map(segment => decodeURIComponent(segment));\n\tconst gitIndex = segments.length - 2;\n\tconst projectIndex = gitIndex - 1;\n\tif (\n\t\tgitIndex < 1 ||\n\t\tsegments[gitIndex]?.toLowerCase() !== '_git' ||\n\t\tsegments.some(segment => !segment || segment === '.' || segment === '..' || /[/\\\\]/.test(segment))\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst project = segments[projectIndex];\n\tconst repository = segments[gitIndex + 1];\n\tif (!project || !repository) return undefined;\n\n\tconst collectionPath = segments.slice(0, projectIndex);\n\tif (legacyVsts) {\n\t\tif (collectionPath.length !== 0) return undefined;\n\n\t\treturn {\n\t\t\tcollection: undefined,\n\t\t\tproject: project,\n\t\t\trepositoryPath: `${project}/_git/${repository}`,\n\t\t\tvirtualDirectory: undefined,\n\t\t};\n\t}\n\n\tif (collectionPath.length === 0 || (cloud && collectionPath.length !== 1)) return undefined;\n\n\treturn {\n\t\tcollection: collectionPath.at(-1),\n\t\tproject: project,\n\t\trepositoryPath: `${project}/_git/${repository}`,\n\t\tvirtualDirectory: collectionPath.length > 1 ? collectionPath.slice(0, -1).join('/') : undefined,\n\t};\n}\n\nfunction parseAzureNewStyleUrl(url: URL): [string, string, string] {\n\tconst match = azureHttpsUrlRegex2.exec(url.pathname);\n\tif (match == null) throw new Error(`Invalid Azure URL: ${url.toString()}`);\n\n\tconst [, owner, project, repo] = match;\n\treturn [owner, project, repo];\n}\n\nexport function parseAzureHttpsUrl(url: string): [owner: string, project: string, repo: string];\nexport function parseAzureHttpsUrl(urlObj: URL): [owner: string, project: string, repo: string];\nexport function parseAzureHttpsUrl(arg: URL | string): [owner: string, project: string, repo: string] {\n\tconst url = typeof arg === 'string' ? new URL(arg) : arg;\n\tif (url.hostname.endsWith(vstsHostnameSuffix)) return parseVstsHttpsUrl(url);\n\treturn parseAzureNewStyleUrl(url);\n}\n\nexport class AzureDevOpsRemoteProvider extends RemoteProvider<AzureDevOpsRepositoryDescriptor> {\n\tprivate readonly collection: string | undefined;\n\tprivate readonly project: string | undefined;\n\tprivate readonly repositoryPath: string | undefined;\n\tprivate readonly virtualDirectory: string | undefined;\n\n\tconstructor(\n\t\tdomain: string,\n\t\tpath: string,\n\t\tprotocol?: string,\n\t\tname?: string,\n\t\tisVsts: boolean = false,\n\t\tcontext?: RemoteProviderContext,\n\t) {\n\t\tif (sshDomainRegex.test(domain)) {\n\t\t\tpath = path.replace(sshPathRegex, '');\n\t\t\tdomain = domain.replace(sshDomainRegex, '');\n\n\t\t\t// Add in /_git/ into ssh urls\n\t\t\tconst match = orgAndProjectRegex.exec(path);\n\t\t\tif (match != null) {\n\t\t\t\tconst [, org, project, rest] = match;\n\n\t\t\t\t// VSTS puts the org in the subdomain; modern Azure DevOps puts it in the path\n\t\t\t\tif (isVsts) {\n\t\t\t\t\tdomain = `${org}.${domain}`;\n\t\t\t\t\tpath = `${project}/_git/${rest}`;\n\t\t\t\t} else {\n\t\t\t\t\tpath = `${org}/${project}/_git/${rest}`;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Azure DevOps allows projects and repository names with spaces. In that situation,\n\t\t// the `path` will be previously encoded during git clone\n\t\t// revert that encoding to avoid double-encoding by gitlens during copy remote and open remote\n\t\tconst repositoryPath = parseAzureRepositoryPath(path, isVsts, domain.toLowerCase() === 'dev.azure.com');\n\t\tpath = decodeURIComponent(path);\n\t\tsuper(domain, path, protocol, name, undefined, context);\n\t\tthis.collection = repositoryPath?.collection;\n\t\tthis.project = repositoryPath?.project;\n\t\tthis.repositoryPath = repositoryPath?.repositoryPath;\n\t\tthis.virtualDirectory = repositoryPath?.virtualDirectory;\n\t}\n\n\tprotected override get issueLinkPattern(): string {\n\t\tconst projectUrl = this.baseUrl.replace(gitTailRegex, '');\n\t\tconst orgUrl = projectUrl.substring(0, projectUrl.lastIndexOf('/'));\n\t\treturn `${orgUrl}/_workitems/edit/<num>`;\n\t}\n\n\tprivate _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;\n\toverride get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {\n\t\t// Strip off any `_git` part from the repo url\n\t\tthis._autolinks ??= [\n\t\t\t...super.autolinks,\n\t\t\t{\n\t\t\t\tprefix: '#',\n\t\t\t\turl: this.issueLinkPattern,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Work Item #{number} on {provider}', { number: '<num>', provider: this.name }),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{provider} Work Item #{number}', { provider: this.name, number: '<num>' }),\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Default Pull request message when merging a PR in ADO. Will not catch commits & pushes following a different pattern.\n\t\t\t\tprefix: 'PR ',\n\t\t\t\turl: `${this.baseUrl}/pullrequest/<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Pull Request #{number} on {provider}', { number: '<num>', provider: this.name }),\n\n\t\t\t\ttype: 'pullrequest',\n\t\t\t\tdescription: l10n.t('{provider} Pull Request #{number}', {\n\t\t\t\t\tprovider: this.name,\n\t\t\t\t\tnumber: '<num>',\n\t\t\t\t}),\n\t\t\t},\n\t\t];\n\t\treturn this._autolinks;\n\t}\n\n\toverride get icon(): string {\n\t\treturn 'azdo';\n\t}\n\n\tget id(): RemoteProviderId {\n\t\treturn 'azure-devops';\n\t}\n\n\tget gkProviderId(): GkProviderId {\n\t\treturn 'azureDevops' satisfies Unbrand<GkProviderId> as Brand<GkProviderId>;\n\t}\n\n\tget name(): string {\n\t\treturn 'Azure DevOps';\n\t}\n\n\toverride get owner(): string | undefined {\n\t\tif (isVsts(this.domain)) {\n\t\t\treturn this.domain.split('.')[0];\n\t\t}\n\t\treturn this.collection ?? super.owner;\n\t}\n\n\toverride get repoName(): string | undefined {\n\t\tif (isVsts(this.domain)) {\n\t\t\treturn this.path;\n\t\t}\n\t\treturn this.repositoryPath ?? super.repoName;\n\t}\n\n\toverride get repoDesc(): AzureDevOpsRepositoryDescriptor {\n\t\tconst descriptor = super.repoDesc;\n\t\treturn this.virtualDirectory == null ? descriptor : { ...descriptor, virtualDirectory: this.virtualDirectory };\n\t}\n\n\toverride get providerDesc():\n\t\t| {\n\t\t\t\tid: GkProviderId;\n\t\t\t\trepoDomain: string;\n\t\t\t\trepoName: string;\n\t\t\t\trepoOwnerDomain: string;\n\t\t }\n\t\t| undefined {\n\t\tif (this.gkProviderId == null || this.owner == null || this.repoName == null || this.project == null) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn {\n\t\t\tid: this.gkProviderId,\n\t\t\trepoDomain: this.project,\n\t\t\trepoName: this.repoName,\n\t\t\trepoOwnerDomain: this.owner,\n\t\t};\n\t}\n\n\tprivate _displayPath: string | undefined;\n\toverride get displayPath(): string {\n\t\tthis._displayPath ??= this.path.replace(gitRegex, '/').replace(legacyDefaultCollectionRegex, '');\n\t\treturn this._displayPath;\n\t}\n\n\tprivate static readonly fileQueryRegex = /path=([^&]+)/i;\n\tprivate static readonly rangeQueryRegex = /line=(\\d+)(?:&lineEnd=(\\d+))?/;\n\n\toverride parseRemoteFileUri(uri: Uri): ParsedRemoteFileUri | undefined {\n\t\tif (uri.authority !== this.domain) return undefined;\n\n\t\tlet startLine;\n\t\tlet endLine;\n\t\tif (uri.query) {\n\t\t\tconst rangeMatch = AzureDevOpsRemoteProvider.rangeQueryRegex.exec(uri.query);\n\t\t\tif (rangeMatch != null) {\n\t\t\t\tconst [, start, end] = rangeMatch;\n\t\t\t\tif (start) {\n\t\t\t\t\tstartLine = parseInt(start, 10);\n\t\t\t\t\tif (end) {\n\t\t\t\t\t\tendLine = parseInt(end, 10);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst fileMatch = AzureDevOpsRemoteProvider.fileQueryRegex.exec(uri.query);\n\t\tif (fileMatch == null) return undefined;\n\n\t\tconst [, path] = fileMatch;\n\n\t\treturn {\n\t\t\tstartLine: startLine,\n\t\t\tendLine: endLine,\n\t\t\tcandidates: [{ type: 'pathOnly', filePath: path }],\n\t\t};\n\t}\n\n\tprotected getUrlForBranches(): string {\n\t\treturn this.encodeUrl(`${this.baseUrl}/branches`);\n\t}\n\n\tprotected getUrlForBranch(branch: string): string {\n\t\treturn this.encodeUrl(`${this.baseUrl}/?version=GB${branch}&_a=history`);\n\t}\n\n\tprotected getUrlForCommit(sha: string): string {\n\t\treturn this.encodeUrl(`${this.baseUrl}/commit/${sha}`);\n\t}\n\n\tprotected override getUrlForComparison(base: string, head: string, _notation: GitRevisionRangeNotation): string {\n\t\treturn this.encodeUrl(`${this.baseUrl}/branchCompare?baseVersion=GB${base}&targetVersion=GB${head}`);\n\t}\n\n\tprotected override async getUrlForCreatePullRequest({\n\t\tbase,\n\t\thead,\n\t}: CreatePullRequestRemoteResource): Promise<string | undefined> {\n\t\tconst query = new URLSearchParams({ sourceRef: head.branch, targetRef: base.branch ?? '' });\n\n\t\tif (base.remote.url !== head.remote.url) {\n\t\t\tconst parsedBaseUrl = parseAzureUrl(base.remote.url);\n\t\t\tif (parsedBaseUrl == null) return undefined;\n\n\t\t\tconst { org: baseOrg, project: baseProject, repo: baseName } = parsedBaseUrl;\n\t\t\tconst repoInfo = await this.context?.getRepositoryInfo?.(this.id, {\n\t\t\t\towner: baseOrg,\n\t\t\t\tname: baseName,\n\t\t\t\tproject: baseProject,\n\t\t\t});\n\t\t\tif (!repoInfo) return undefined;\n\n\t\t\tquery.set('targetRepositoryId', repoInfo.id);\n\t\t}\n\n\t\treturn `${this.encodeUrl(`${this.getRepoBaseUrl(head.remote.path)}/pullrequestcreate`)}?${query.toString()}`;\n\t}\n\n\tprotected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: LineRange): string {\n\t\tlet line;\n\t\tif (range != null) {\n\t\t\tif (range.startLine === range.endLine) {\n\t\t\t\tline = `&line=${range.startLine}&lineStartColumn=${range.startCharacter}&lineEndColumn=${range.endCharacter}`;\n\t\t\t} else {\n\t\t\t\tline = `&line=${range.startLine}&lineEnd=${range.endLine}&lineStartColumn=${range.startCharacter}&lineEndColumn=${range.endCharacter}`;\n\t\t\t}\n\t\t} else {\n\t\t\tline = '';\n\t\t}\n\n\t\tif (sha) return this.encodeUrl(`${this.baseUrl}?path=${fileName}&version=GC${sha}${line}&_a=contents`);\n\t\tif (branch) return this.encodeUrl(`${this.baseUrl}/?path=/${fileName}&version=GB${branch}&_a=contents${line}`);\n\t\treturn this.encodeUrl(`${this.baseUrl}?path=/${fileName}${line}`);\n\t}\n}\n\nfunction parseAzureUrl(url: string): { org: string; project: string; repo: string } | undefined {\n\tif (azureSshUrlRegex.test(url)) {\n\t\t// Examples of SSH urls:\n\t\t// - old one: bbbchiv@vs-ssh.visualstudio.com:v3/bbbchiv/MyFirstProject/test\n\t\t// - modern one: git@ssh.dev.azure.com:v3/bbbchiv2/MyFirstProject/test\n\t\turl = url.replace(azureSshUrlRegex, '');\n\t\tconst match = orgAndProjectRegex.exec(url);\n\t\tif (match != null) {\n\t\t\tconst [, org, project, rest] = match;\n\t\t\treturn { org: org, project: project, repo: rest };\n\t\t}\n\t} else {\n\t\tconst [org, project, rest] = parseAzureHttpsUrl(url);\n\t\treturn { org: org, project: project, repo: rest };\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { flatten } from "../../../utils/iterable.js";
2
2
  import { Emitter } from "../../../utils/event.js";
3
3
  import { debounce } from "../../../utils/debounce.js";
4
+ import { areDomainsOnSameHost, baseUrlFromDomain, hostFromDomain, sameConfiguredBaseUrl } from "../utils/domain.utils.js";
4
5
  import { isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
5
6
  import { providersMetadata } from "../providers/models.js";
6
- import { areDomainsOnSameHost, hostFromDomain, sameConfiguredBaseUrl } from "../utils/domain.utils.js";
7
7
  //#region ../plus/integrations/src/authentication/configuredIntegrationService.ts
8
8
  var ConfiguredIntegrationService = class {
9
9
  ctx;
@@ -27,6 +27,7 @@ var ConfiguredIntegrationService = class {
27
27
  if (configured == null) continue;
28
28
  const descriptors = configured.map((d) => ({
29
29
  ...d,
30
+ baseUrl: isSelfManagedHostIntegrationId(id) ? getStoredBaseUrl(d.baseUrl, d.domain) : d.baseUrl,
30
31
  domain: this.normalizeConfiguredDomain(id, d.domain),
31
32
  id: d.id || d.domain || providersMetadata[id]?.domain || id,
32
33
  expiresAt: d.expiresAt ? new Date(d.expiresAt) : void 0
@@ -60,6 +61,7 @@ var ConfiguredIntegrationService = class {
60
61
  async addOrUpdateConfigured(descriptor) {
61
62
  descriptor = {
62
63
  ...descriptor,
64
+ baseUrl: isSelfManagedHostIntegrationId(descriptor.integrationId) ? getStoredBaseUrl(descriptor.baseUrl, descriptor.domain) : descriptor.baseUrl,
63
65
  domain: this.normalizeConfiguredDomain(descriptor.integrationId, descriptor.domain)
64
66
  };
65
67
  const descriptors = this.configured.get(descriptor.integrationId) ?? [];
@@ -390,12 +392,17 @@ function convertStoredSessionToSession(storedSession, descriptor, cloudIfMissing
390
392
  cloud: storedSession.cloud ?? cloudIfMissing,
391
393
  expiresAt: storedSession.expiresAt ? new Date(storedSession.expiresAt) : void 0,
392
394
  domain: storedSession.domain ?? descriptor.domain,
393
- baseUrl: storedSession.baseUrl,
395
+ baseUrl: getStoredBaseUrl(storedSession.baseUrl, storedSession.domain, storedSession.protocol),
394
396
  protocol: storedSession.protocol,
395
397
  type: storedSession.type,
396
398
  appKey: storedSession.appKey
397
399
  };
398
400
  }
401
+ function getStoredBaseUrl(baseUrl, domain, protocol) {
402
+ if (baseUrl != null) return baseUrl;
403
+ const legacyUrl = baseUrlFromDomain(domain, protocol);
404
+ return legacyUrl != null && new URL(legacyUrl).pathname !== "/" ? legacyUrl : void 0;
405
+ }
399
406
  //#endregion
400
407
  export { ConfiguredIntegrationService };
401
408
 
@@ -1 +1 @@
1
- {"version":3,"file":"configuredIntegrationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/configuredIntegrationService.ts"],"sourcesContent":["import { debounce } from '@gitlens/utils/debounce.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 { flatten } from '@gitlens/utils/iterable.js';\nimport type {\n\tIntegrationIds,\n\tStoredConfiguredIntegrationDescriptor,\n\tStoredIntegrationConfigurations,\n} from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { providersMetadata } from '../providers/models.js';\nimport { areDomainsOnSameHost, hostFromDomain, sameConfiguredBaseUrl } from '../utils/domain.utils.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider.js';\nimport type {\n\tCloudIntegrationAuthType,\n\tConfiguredIntegrationDescriptor,\n\tProviderAuthenticationSession,\n} from './models.js';\n\ninterface StoredSession {\n\tid: string;\n\taccessToken: string;\n\taccount?: { label?: string; displayName?: string; id: string };\n\tscopes: string[];\n\tcloud?: boolean;\n\ttype: CloudIntegrationAuthType | undefined;\n\texpiresAt?: string;\n\tdomain?: string;\n\t/** See {@link ProviderAuthenticationSession.baseUrl}: the configured address, path included. */\n\tbaseUrl?: string;\n\tprotocol?: string;\n\t/** The provider app key paired with the token for providers whose client needs one (e.g. Trello). */\n\tappKey?: string;\n}\n\nexport interface ConfiguredIntegrationsChangeEvent {\n\treadonly added: readonly IntegrationIds[];\n\treadonly removed: readonly IntegrationIds[];\n}\n\nexport class ConfiguredIntegrationService implements Disposable {\n\tprivate readonly _onDidChange = new Emitter<ConfiguredIntegrationsChangeEvent>();\n\tprivate storeConfiguredQueue: Promise<void> = Promise.resolve();\n\tget onDidChange(): Event<ConfiguredIntegrationsChangeEvent> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tconstructor(private readonly ctx: IntegrationServiceContext) {}\n\n\tdispose(): void {\n\t\tthis._onDidChange.dispose();\n\t}\n\n\tprivate _configured?: Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>;\n\tprivate get configured(): Map<IntegrationIds, ConfiguredIntegrationDescriptor[]> {\n\t\tif (this._configured == null) {\n\t\t\tthis._configured = new Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>();\n\n\t\t\tconst storedConfigured = this.ctx.storage.get('integrations:configured');\n\t\t\tfor (const [id, configured] of Object.entries(storedConfigured ?? {}) as [\n\t\t\t\tIntegrationIds,\n\t\t\t\tStoredConfiguredIntegrationDescriptor[],\n\t\t\t][]) {\n\t\t\t\tif (configured == null) continue;\n\n\t\t\t\tconst descriptors = configured.map(d => ({\n\t\t\t\t\t...d,\n\t\t\t\t\tdomain: this.normalizeConfiguredDomain(id, d.domain),\n\t\t\t\t\t// Backfill a stable connection id for pre-multi-account stored data: the domain for\n\t\t\t\t\t// self-managed hosts, or the provider's canonical domain for cloud (which is the legacy\n\t\t\t\t\t// secret-key session id), so existing secrets keep resolving with zero migration. Fall back\n\t\t\t\t\t// to the integration id (never empty) when no domain is known — self-managed providers carry\n\t\t\t\t\t// `domain: ''` in metadata, and an empty id would collide across connections and produce\n\t\t\t\t\t// ambiguous secret keys (`...|`).\n\t\t\t\t\tid: d.id || d.domain || providersMetadata[id]?.domain || id,\n\t\t\t\t\texpiresAt: d.expiresAt ? new Date(d.expiresAt) : undefined,\n\t\t\t\t}));\n\t\t\t\tthis._configured.set(id, descriptors);\n\t\t\t}\n\t\t}\n\n\t\treturn this._configured;\n\t}\n\n\t// Synchronous: reads the in-memory configured map (lazily hydrated from storage). No async work —\n\t// the old async variant only existed to weave in the built-in VS Code GitHub session, which is gone.\n\tgetConfigured(\n\t\tid?: IntegrationIds,\n\t\toptions?: { cloud?: boolean; domain?: string },\n\t): ConfiguredIntegrationDescriptor[] {\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\n\t\tconst configured =\n\t\t\tid != null\n\t\t\t\t? this.configured.get(id)\n\t\t\t\t: [...flatten<ConfiguredIntegrationDescriptor>(this.configured.values())];\n\t\tif (!configured?.length) return descriptors;\n\n\t\tif (options?.domain != null || options?.cloud != null) {\n\t\t\tfor (const descriptor of configured) {\n\t\t\t\tif (\n\t\t\t\t\t(options?.domain != null &&\n\t\t\t\t\t\t!this.domainsMatch(id ?? descriptor.integrationId, descriptor.domain, options.domain)) ||\n\t\t\t\t\t(options?.cloud === true && !descriptor.cloud) ||\n\t\t\t\t\t(options?.cloud === false && descriptor.cloud)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdescriptors.push(descriptor);\n\t\t\t}\n\t\t} else {\n\t\t\tdescriptors.push(...configured);\n\t\t}\n\n\t\treturn descriptors;\n\t}\n\n\tprivate async storeConfigured(): Promise<void> {\n\t\t// We need to convert the map to a record to store\n\t\tconst configured: StoredIntegrationConfigurations = {} as unknown as StoredIntegrationConfigurations;\n\t\tfor (const [id, descriptors] of this.configured) {\n\t\t\tconfigured[id] = descriptors.map(d => ({\n\t\t\t\t...d,\n\t\t\t\texpiresAt: d.expiresAt\n\t\t\t\t\t? d.expiresAt instanceof Date\n\t\t\t\t\t\t? d.expiresAt.toISOString()\n\t\t\t\t\t\t: d.expiresAt\n\t\t\t\t\t: undefined,\n\t\t\t}));\n\t\t}\n\n\t\tconst pending = this.storeConfiguredQueue.then(() =>\n\t\t\tthis.ctx.storage.store('integrations:configured', configured),\n\t\t);\n\t\tthis.storeConfiguredQueue = pending.catch(() => {});\n\t\tawait pending;\n\t}\n\n\tprivate async addOrUpdateConfigured(descriptor: ConfiguredIntegrationDescriptor): Promise<void> {\n\t\tdescriptor = {\n\t\t\t...descriptor,\n\t\t\tdomain: this.normalizeConfiguredDomain(descriptor.integrationId, descriptor.domain),\n\t\t};\n\t\tconst descriptors = this.configured.get(descriptor.integrationId) ?? [];\n\t\t// Key connections by their stable id (+ cloud, to preserve legacy local/cloud coexistence) so\n\t\t// multiple accounts on the same provider+domain no longer overwrite each other.\n\t\tconst existing = descriptors.find(d => d.id === descriptor.id && d.cloud === descriptor.cloud);\n\n\t\t// The first connection for a provider/domain is primary by default; an explicit flag (or the\n\t\t// existing value) always wins. Self-managed providers can span multiple hosts under the same\n\t\t// integration id, so each host needs its own primary.\n\t\tconst primaryScope = this.getPrimaryScope(descriptor.integrationId, descriptors, descriptor.domain);\n\t\tconst primary = descriptor.primary ?? existing?.primary ?? !primaryScope.some(d => d.primary);\n\t\t// Preserve a previously-resolved type/accountName when the incoming write doesn't carry them (e.g.\n\t\t// the model re-stores the primary session with an empty account, after reconcile enriched it).\n\t\tconst type = descriptor.type ?? existing?.type;\n\t\tconst accountName = descriptor.accountName ?? existing?.accountName;\n\t\tconst normalized: ConfiguredIntegrationDescriptor = {\n\t\t\t...descriptor,\n\t\t\tprimary: primary,\n\t\t\ttype: type,\n\t\t\taccountName: accountName,\n\t\t};\n\n\t\tlet changed: boolean;\n\t\tif (existing != null) {\n\t\t\t// `baseUrl` is compared alongside `domain`, not folded into it: `domain` is normalized to a bare\n\t\t\t// host, so a connection re-pointed to another context path on the SAME host is identical by domain\n\t\t\t// and would otherwise be treated as unchanged — leaving the stored descriptor, and every warm\n\t\t\t// integration listening for the change, on the old address.\n\t\t\tif (\n\t\t\t\texisting.domain === normalized.domain &&\n\t\t\t\tsameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) &&\n\t\t\t\texisting.expiresAt === normalized.expiresAt &&\n\t\t\t\texisting.scopes === normalized.scopes &&\n\t\t\t\t(existing.primary ?? false) === (normalized.primary ?? false) &&\n\t\t\t\texisting.type === normalized.type &&\n\t\t\t\texisting.accountName === normalized.accountName\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only fire the change event on address/scopes/primary/type/accountName changes (ignore expiresAt churn)\n\t\t\tchanged =\n\t\t\t\texisting.domain !== normalized.domain ||\n\t\t\t\t!sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) ||\n\t\t\t\texisting.scopes !== normalized.scopes ||\n\t\t\t\t(existing.primary ?? false) !== (normalized.primary ?? false) ||\n\t\t\t\texisting.type !== normalized.type ||\n\t\t\t\texisting.accountName !== normalized.accountName;\n\n\t\t\t// remove the existing descriptor from the array\n\t\t\tdescriptors.splice(descriptors.indexOf(existing), 1);\n\t\t} else {\n\t\t\tchanged = true;\n\t\t}\n\n\t\tdescriptors.push(normalized);\n\t\tthis.configured.set(descriptor.integrationId, descriptors);\n\n\t\tif (changed) {\n\t\t\tthis.fireChange(descriptor.integrationId);\n\t\t}\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate async removeConfigured(\n\t\tid: IntegrationIds,\n\t\toptions: { connectionId: string; cloud: boolean | undefined },\n\t): Promise<void> {\n\t\tconst existing = this.configured.get(id);\n\t\tif (existing == null || existing.length === 0) return;\n\n\t\tconst removedPrimaryDomains = new Set<string | undefined>();\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\t\tfor (const d of existing) {\n\t\t\tif (\n\t\t\t\td.id === options.connectionId &&\n\t\t\t\t(options?.cloud == null ||\n\t\t\t\t\t(options?.cloud === true && d.cloud === true) ||\n\t\t\t\t\t(options?.cloud === false && d.cloud === false))\n\t\t\t) {\n\t\t\t\tif (d.primary) {\n\t\t\t\t\tremovedPrimaryDomains.add(d.domain);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tdescriptors.push(d);\n\t\t}\n\n\t\tif (descriptors.length === existing.length) return; // nothing matched\n\n\t\t// Promote a secondary to primary when the removed connection was the primary and others remain\n\t\t// in the same primary scope, so the provider/host stays in a well-defined connected state.\n\t\tfor (const domain of removedPrimaryDomains) {\n\t\t\tif (descriptors.some(d => this.isInPrimaryScope(id, d, domain) && d.primary)) continue;\n\n\t\t\tconst index = descriptors.findIndex(d => this.isInPrimaryScope(id, d, domain));\n\t\t\tif (index !== -1) {\n\t\t\t\tdescriptors[index] = { ...descriptors[index], primary: true };\n\t\t\t}\n\t\t}\n\n\t\tthis.configured.set(id, descriptors);\n\t\tthis.fireChange(undefined, id);\n\t\tawait this.storeConfigured();\n\t}\n\n\tasync storeSession(id: IntegrationIds, session: ProviderAuthenticationSession): Promise<void> {\n\t\tawait this.writeSecret(id, session);\n\t}\n\n\tasync getStoredSession(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tconst sessionId = this.resolveConnectionId(id, descriptor);\n\t\tlet session = descriptor.cloud === true ? undefined : await this.readSecret(id, sessionId, false);\n\n\t\tlet cloudIfMissing = false;\n\t\tif (session != null) {\n\t\t\t// Check the `expiresAt` field\n\t\t\t// If it has an expiresAt property and the key is the old type, then it's a cloud session,\n\t\t\t// so delete it from the local key and\n\t\t\t// store with the \"cloud\" type key, and then use that one.\n\t\t\t// Otherwise it's a local session under the local key, so just return it.\n\t\t\tif (session.expiresAt != null) {\n\t\t\t\tcloudIfMissing = true;\n\t\t\t\tawait Promise.allSettled([this.deleteSecrets(id, session.id), this.writeSecret(id, session)]);\n\t\t\t}\n\t\t}\n\n\t\t// If no local session we try to restore a session with the cloud key\n\t\tif (session == null && descriptor.cloud !== false) {\n\t\t\tcloudIfMissing = true;\n\t\t\tsession = await this.readSecret(id, sessionId, true);\n\t\t}\n\n\t\treturn convertStoredSessionToSession(session, descriptor, cloudIfMissing);\n\t}\n\n\tasync deleteStoredSessions(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tawait this.deleteSecrets(id, this.resolveConnectionId(id, descriptor), cloud, options);\n\t}\n\n\tasync deleteAllStoredSessions(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\tawait this.deleteAllSecrets(id, cloud, domain);\n\t}\n\n\t/**\n\t * Reclaims all stored configuration + secrets for the given (typically retired) integration ids.\n\t * Used to clean up state left behind when an id is removed from the model — e.g. the local\n\t * self-managed `github-enterprise`/`gitlab-self-hosted` ids dropped when integrations went cloud-only.\n\t * Domains are read from the stored config (secrets are keyed by domain); ids with no stored config\n\t * are a no-op.\n\t */\n\tasync purgeStoredConfiguration(ids: readonly string[]): Promise<void> {\n\t\tconst stored =\n\t\t\tthis.ctx.storage.get<Record<string, StoredConfiguredIntegrationDescriptor[] | undefined>>(\n\t\t\t\t'integrations:configured',\n\t\t\t);\n\t\tif (stored == null) return;\n\n\t\tlet changed = false;\n\t\tfor (const id of ids) {\n\t\t\tif (!(id in stored)) continue;\n\n\t\t\tfor (const descriptor of stored[id] ?? []) {\n\t\t\t\t// Mirror the hydration backfill so the secret keys we delete match what was written: fall back\n\t\t\t\t// through domain, the provider's canonical domain, then the integration id (never empty). A\n\t\t\t\t// legacy cloud descriptor stored neither id nor domain, so an empty connection id here would\n\t\t\t\t// miss the canonical-domain secret (e.g. `integration.auth.cloud:github|github.com`) and orphan it.\n\t\t\t\tconst connectionId =\n\t\t\t\t\tdescriptor.id || descriptor.domain || providersMetadata[id as IntegrationIds]?.domain || id;\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tif (descriptor.domain) {\n\t\t\t\t\tawait this.ctx.storage.deleteWorkspace(`connected:${id}:${descriptor.domain}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchanged = true;\n\t\t}\n\t\tif (!changed) return;\n\n\t\tconst remaining = Object.fromEntries(Object.entries(stored).filter(([key]) => !ids.includes(key)));\n\t\tawait this.ctx.storage.store('integrations:configured', remaining as StoredIntegrationConfigurations);\n\t}\n\n\tasync deleteSecrets(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tif (cloud == null || cloud === false) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (cloud == null || cloud === true) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (options?.preserveConfigured) return;\n\n\t\tawait this.removeConfigured(id, { connectionId: connectionId, cloud: cloud });\n\t}\n\n\tasync deleteAllSecrets(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\t// Delete every connection's secret (multi-account): secrets are keyed per connection id, so a\n\t\t// single canonical-domain delete would orphan secondary tokens. When a domain is given (self-managed\n\t\t// disconnect of one host), scope to that host so other hosts under the same provider id survive.\n\t\tconst descriptors = this.configured.get(id);\n\t\tconst connectionIds = [\n\t\t\t...new Set(\n\t\t\t\t(domain != null ? descriptors?.filter(c => this.domainsMatch(id, c.domain, domain)) : descriptors)?.map(\n\t\t\t\t\tc => c.id,\n\t\t\t\t) ?? [],\n\t\t\t),\n\t\t];\n\t\tif (connectionIds.length) {\n\t\t\tfor (const connectionId of connectionIds) {\n\t\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// A domain-scoped clear that matched nothing means this host has no stored connections, so it must be\n\t\t// a no-op. The canonical-domain fallback below would target a bogus key (self-managed canonical domain\n\t\t// is empty), so skip it.\n\t\tif (domain != null) return;\n\n\t\t// No configured connections (e.g. an orphaned secret with no descriptor): fall back to the\n\t\t// provider's canonical domain, which is the legacy session id for cloud providers. Mirror the\n\t\t// hydration backfill's `|| id` guard so a self-managed provider (empty canonical domain) doesn't\n\t\t// target an ambiguous empty connection id.\n\t\tawait this.deleteSecrets(id, providersMetadata[id]?.domain || id, cloud);\n\t}\n\n\tasync writeSecret(id: IntegrationIds, session: ProviderAuthenticationSession | StoredSession): Promise<void> {\n\t\tawait this.ctx.storage.storeSecret(\n\t\t\tthis.getSecretKey(id, session.id, session.cloud ?? false),\n\t\t\tJSON.stringify(session),\n\t\t);\n\n\t\tawait this.addOrUpdateConfigured({\n\t\t\tid: session.id,\n\t\t\tintegrationId: id,\n\t\t\tdomain: isSelfManagedHostIntegrationId(id) ? session.domain : undefined,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? session.baseUrl : undefined,\n\t\t\texpiresAt: session.expiresAt,\n\t\t\tscopes: session.scopes.join(','),\n\t\t\tcloud: session.cloud ?? false,\n\t\t\ttype: session.type,\n\t\t\taccountName: session.account?.label || undefined,\n\t\t});\n\t}\n\n\tasync readSecret(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): Promise<StoredSession | undefined> {\n\t\tlet storedSession: StoredSession | undefined;\n\t\ttry {\n\t\t\tconst sessionJSON = await this.ctx.storage.getSecret(this.getSecretKey(id, sessionId, cloud));\n\t\t\tif (sessionJSON) {\n\t\t\t\tstoredSession = JSON.parse(sessionJSON);\n\t\t\t\tif (storedSession != null) {\n\t\t\t\t\tconst configured = this.configured.get(id);\n\t\t\t\t\tconst connectionId = storedSession.id ?? sessionId;\n\t\t\t\t\tconst sessionCloud = storedSession.cloud ?? cloud;\n\t\t\t\t\tconst domain = isSelfManagedHostIntegrationId(id)\n\t\t\t\t\t\t? (storedSession.domain ?? storedSession.id)\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (\n\t\t\t\t\t\tconfigured == null ||\n\t\t\t\t\t\tconfigured.length === 0 ||\n\t\t\t\t\t\t!configured.some(\n\t\t\t\t\t\t\tc => c.id === connectionId && c.integrationId === id && c.cloud === sessionCloud,\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tawait this.addOrUpdateConfigured({\n\t\t\t\t\t\t\tid: connectionId,\n\t\t\t\t\t\t\tintegrationId: id,\n\t\t\t\t\t\t\tdomain: domain,\n\t\t\t\t\t\t\t// Rebuilt from the secret, so it must carry the address the session does; omitting it\n\t\t\t\t\t\t\t// leaves the descriptor claiming no context path and makes reconcile read every\n\t\t\t\t\t\t\t// routine check-in as a re-pointed connection.\n\t\t\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? storedSession.baseUrl : undefined,\n\t\t\t\t\t\t\texpiresAt: storedSession.expiresAt,\n\t\t\t\t\t\t\tscopes: storedSession.scopes.join(','),\n\t\t\t\t\t\t\tcloud: sessionCloud,\n\t\t\t\t\t\t\ttype: storedSession.type,\n\t\t\t\t\t\t\taccountName: storedSession.account?.label || undefined,\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} catch (_ex) {\n\t\t\ttry {\n\t\t\t\tawait this.deleteSecrets(id, sessionId, cloud);\n\t\t\t} catch {}\n\t\t}\n\t\treturn storedSession;\n\t}\n\n\tprivate getSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): `integration.auth:${IntegrationIds}|${string}` | `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn cloud ? this.getCloudSecretKey(id, sessionId) : this.getLocalSecretKey(id, sessionId);\n\t}\n\n\tprivate getLocalSecretKey(id: IntegrationIds, sessionId: string): `integration.auth:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth:${id}|${sessionId}`;\n\t}\n\n\tprivate getCloudSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t): `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth.cloud:${id}|${sessionId}`;\n\t}\n\n\tprivate getPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptors: ConfiguredIntegrationDescriptor[],\n\t\tdomain: string | undefined,\n\t): ConfiguredIntegrationDescriptor[] {\n\t\treturn isSelfManagedHostIntegrationId(id)\n\t\t\t? descriptors.filter(d => this.isInPrimaryScope(id, d, domain))\n\t\t\t: descriptors;\n\t}\n\n\tprivate isInPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptor: ConfiguredIntegrationDescriptor,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, descriptor.domain, domain);\n\t}\n\n\t/**\n\t * Resolves the connection id used as the secret-key session id for an auth descriptor.\n\t * - If the descriptor targets a specific connection (`connectionId`), that wins.\n\t * - Otherwise returns the primary connection's id for the provider (matching the descriptor's domain\n\t * for self-managed hosts; any connection for cloud hosts, where the stored domain is undefined).\n\t * - Falls back to the descriptor domain when nothing is configured yet, preserving legacy\n\t * (pre-multi-account) reads with zero secret migration.\n\t */\n\tresolveConnectionId(id: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): string {\n\t\t// An empty connectionId is not a real target — treat it as \"no target\" so it resolves the primary\n\t\t// rather than keying a secret under an empty id.\n\t\tif (descriptor.connectionId) return descriptor.connectionId;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? descriptor.domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, domain, descriptor.cloud);\n\t\treturn (candidates?.find(c => c.primary) ?? candidates?.[0])?.id ?? descriptor.domain;\n\t}\n\n\t/**\n\t * Returns the id of the configured connection an unscoped descriptor resolves to (the primary for the\n\t * descriptor's domain, matching {@link resolveConnectionId}'s selection), or `undefined` when nothing is\n\t * configured for it. Unlike {@link resolveConnectionId} this only returns a genuine per-connection cloud\n\t * token id (never a local/PAT or domain-derived legacy id), so callers can safely use it to scope a cloud\n\t * token fetch to one host of a multi-host self-managed provider, or fall through to the provider-global\n\t * primary endpoint on `undefined`.\n\t */\n\t/**\n\t * The address already stored for a self-managed connection — see {@link ConfiguredIntegrationDescriptor.baseUrl}.\n\t *\n\t * Exists so a session rebuilt from a backend response that omits the domain can keep the address the\n\t * connection was configured with, rather than silently falling back to the bare host and losing a context\n\t * path the user never changed. Looked up by connection id first, then by host for an unscoped fetch.\n\t */\n\t/**\n\t * Whether `connectionId` names a cloud connection configured for `domain`'s host.\n\t *\n\t * The caller-facing question is \"is this id evidence that a token belongs to this host\". A connection id\n\t * can arrive from a caller rather than from our own configuration, so it is only evidence once it matches\n\t * a descriptor we stored for that host — see the host guard in `CloudIntegrationAuthenticationProvider`.\n\t */\n\tisConnectionConfiguredForHost(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string | undefined,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\tif (connectionId == null) return false;\n\n\t\tconst connection = this.getConfigured(id, { cloud: true }).find(c => c.id === connectionId);\n\t\tif (connection == null) return false;\n\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, connection.domain, domain);\n\t}\n\n\tgetConfiguredBaseUrl(id: IntegrationIds, connectionId: string | undefined, domain?: string): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return undefined;\n\n\t\tconst configured = this.getConfigured(id, { cloud: true });\n\t\tconst byId = connectionId != null ? configured.find(c => c.id === connectionId) : undefined;\n\t\tif (byId != null) return byId.baseUrl;\n\n\t\treturn configured.find(c => this.domainsMatch(id, c.domain, domain))?.baseUrl;\n\t}\n\n\tgetConfiguredConnectionId(id: IntegrationIds, domain: string | undefined, cloud?: boolean): string | undefined {\n\t\tconst scoped = isSelfManagedHostIntegrationId(id) ? domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, scoped, cloud);\n\t\tconst connection = candidates?.find(c => c.primary) ?? candidates?.[0];\n\t\tif (connection == null) return undefined;\n\n\t\t// Only return a real per-connection cloud token id. Reject a local (PAT) descriptor (surfaced by\n\t\t// scopeConnectionCandidates' non-cloud fallback) and a domain-derived legacy id (the descriptor's own\n\t\t// domain or the provider's canonical domain — see ConfiguredIntegrationDescriptor.id), either of which\n\t\t// would target `v1/provider-tokens/tokens/{domain}` (rejected as a non-uuid). Callers then fall through\n\t\t// to the provider-scoped primary endpoint.\n\t\tif (!connection.cloud) return undefined;\n\n\t\tconst canonicalDomain = providersMetadata[id]?.domain;\n\t\tif (connection.id === connection.domain || (canonicalDomain && connection.id === canonicalDomain)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn connection.id;\n\t}\n\n\t/**\n\t * Selects configured descriptors for a provider matching `domain`, further narrowed to the `cloud`\n\t * variant when specified. A connection id can have both a local (PAT) and cloud descriptor whose ids\n\t * differ (e.g. a legacy local descriptor keyed by domain alongside a real cloud token id); scoping by\n\t * `cloud` keeps a cloud-scoped read/delete from resolving the local id (and vice versa). Falls back to\n\t * the domain-only set when no variant matches, so legacy single-variant resolution is unchanged.\n\t */\n\tprivate scopeConnectionCandidates(\n\t\tid: IntegrationIds,\n\t\tdomain: string | undefined,\n\t\tcloud: boolean | undefined,\n\t): ConfiguredIntegrationDescriptor[] | undefined {\n\t\tconst candidates = this.configured.get(id)?.filter(c => this.domainsMatch(id, c.domain, domain));\n\t\tif (cloud == null || candidates == null) return candidates;\n\n\t\tconst scoped = candidates.filter(c => c.cloud === cloud);\n\t\treturn scoped.length ? scoped : candidates;\n\t}\n\n\t/**\n\t * Marks the given connection as the primary/default for the provider and clears the flag on its\n\t * siblings. Persists immediately instead of relying on a session re-store to carry the primary flag.\n\t */\n\tasync setPrimaryConnection(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\tconst descriptors = this.configured.get(id);\n\t\tif (descriptors == null || descriptors.length === 0) return;\n\t\tif (!descriptors.some(d => d.id === connectionId)) return;\n\n\t\tconst target =\n\t\t\tdescriptors.find(d => d.id === connectionId && d.cloud) ?? descriptors.find(d => d.id === connectionId);\n\t\tif (target == null) return;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? target.domain : undefined;\n\t\t// A connection id can have both a local (PAT) and cloud descriptor. Mark the primary on a single\n\t\t// canonical variant (prefer cloud, since multi-account primaries are cloud-driven) within the\n\t\t// provider/host scope, so other self-managed hosts keep their own default connection.\n\t\tconst primaryIsCloud = descriptors.some(\n\t\t\td => d.id === connectionId && d.cloud && this.isInPrimaryScope(id, d, domain),\n\t\t);\n\n\t\tlet changed = false;\n\t\tconst updated = descriptors.map(d => {\n\t\t\tif (!this.isInPrimaryScope(id, d, domain)) return d;\n\n\t\t\tconst primary = d.id === connectionId && (d.cloud ?? false) === primaryIsCloud;\n\t\t\tif ((d.primary ?? false) === primary) return d;\n\n\t\t\tchanged = true;\n\t\t\treturn { ...d, primary: primary };\n\t\t});\n\t\tif (!changed) return;\n\n\t\tthis.configured.set(id, updated);\n\t\tthis.fireChange(id);\n\t\tawait this.storeConfigured();\n\t}\n\n\t/**\n\t * Removes a single connection (its secret + configured descriptor). If it was the primary and other\n\t * connections remain, a secondary is promoted (see {@link removeConfigured}). Unlike\n\t * {@link deleteAllStoredSessions}, this only affects the targeted connection. Pass `cloud` to scope\n\t * the removal to the cloud/local variant when a local PAT and a cloud session share a connection id.\n\t */\n\tasync deleteConnection(id: IntegrationIds, connectionId: string, cloud?: boolean): Promise<void> {\n\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t}\n\n\tprivate _addedIds = new Set<IntegrationIds>();\n\tprivate _removedIds = new Set<IntegrationIds>();\n\tprivate _fireChangeDebounced?: () => void;\n\n\tprivate normalizeConfiguredDomain(id: IntegrationIds, domain: string | undefined): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return domain;\n\n\t\treturn hostFromDomain(domain) ?? domain;\n\t}\n\n\tprivate domainsMatch(id: IntegrationIds, first: string | undefined, second: string | undefined): boolean {\n\t\tif (first === second) return true;\n\t\tif (!isSelfManagedHostIntegrationId(id)) return false;\n\t\tif (first == null || second == null) return false;\n\n\t\treturn areDomainsOnSameHost(first, second);\n\t}\n\n\tprivate fireChange(added?: IntegrationIds, removed?: IntegrationIds) {\n\t\tthis._fireChangeDebounced ??= debounce(() => {\n\t\t\tconst added = [...this._addedIds];\n\t\t\tthis._addedIds.clear();\n\t\t\tconst removed = [...this._removedIds];\n\t\t\tthis._removedIds.clear();\n\n\t\t\tthis._onDidChange.fire({ added: added, removed: removed });\n\t\t}, 250);\n\n\t\tif (added != null) {\n\t\t\tthis._addedIds.add(added);\n\t\t}\n\t\tif (removed != null) {\n\t\t\tthis._removedIds.add(removed);\n\t\t}\n\t\tthis._fireChangeDebounced();\n\t}\n}\n\nfunction convertStoredSessionToSession(\n\tstoredSession: StoredSession | undefined,\n\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\tcloudIfMissing: boolean,\n): ProviderAuthenticationSession | undefined {\n\tif (storedSession == null) return undefined;\n\n\treturn {\n\t\tid: storedSession.id,\n\t\taccessToken: storedSession.accessToken,\n\t\taccount: {\n\t\t\tid: storedSession.account?.id ?? '',\n\t\t\tlabel: storedSession.account?.label ?? '',\n\t\t},\n\t\tscopes: storedSession.scopes,\n\t\tcloud: storedSession.cloud ?? cloudIfMissing,\n\t\texpiresAt: storedSession.expiresAt ? new Date(storedSession.expiresAt) : undefined,\n\t\tdomain: storedSession.domain ?? descriptor.domain,\n\t\tbaseUrl: storedSession.baseUrl,\n\t\tprotocol: storedSession.protocol,\n\t\ttype: storedSession.type,\n\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello); without\n\t\t// this a rehydrated session silently loses the key and every read no-ops.\n\t\tappKey: storedSession.appKey,\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"configuredIntegrationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/configuredIntegrationService.ts"],"sourcesContent":["import { debounce } from '@gitlens/utils/debounce.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 { flatten } from '@gitlens/utils/iterable.js';\nimport type {\n\tIntegrationIds,\n\tStoredConfiguredIntegrationDescriptor,\n\tStoredIntegrationConfigurations,\n} from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { providersMetadata } from '../providers/models.js';\nimport {\n\tareDomainsOnSameHost,\n\tbaseUrlFromDomain,\n\thostFromDomain,\n\tsameConfiguredBaseUrl,\n} from '../utils/domain.utils.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider.js';\nimport type {\n\tCloudIntegrationAuthType,\n\tConfiguredIntegrationDescriptor,\n\tProviderAuthenticationSession,\n} from './models.js';\n\ninterface StoredSession {\n\tid: string;\n\taccessToken: string;\n\taccount?: { label?: string; displayName?: string; id: string };\n\tscopes: string[];\n\tcloud?: boolean;\n\ttype: CloudIntegrationAuthType | undefined;\n\texpiresAt?: string;\n\tdomain?: string;\n\t/** See {@link ProviderAuthenticationSession.baseUrl}: the configured address, path included. */\n\tbaseUrl?: string;\n\tprotocol?: string;\n\t/** The provider app key paired with the token for providers whose client needs one (e.g. Trello). */\n\tappKey?: string;\n}\n\nexport interface ConfiguredIntegrationsChangeEvent {\n\treadonly added: readonly IntegrationIds[];\n\treadonly removed: readonly IntegrationIds[];\n}\n\nexport class ConfiguredIntegrationService implements Disposable {\n\tprivate readonly _onDidChange = new Emitter<ConfiguredIntegrationsChangeEvent>();\n\tprivate storeConfiguredQueue: Promise<void> = Promise.resolve();\n\tget onDidChange(): Event<ConfiguredIntegrationsChangeEvent> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tconstructor(private readonly ctx: IntegrationServiceContext) {}\n\n\tdispose(): void {\n\t\tthis._onDidChange.dispose();\n\t}\n\n\tprivate _configured?: Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>;\n\tprivate get configured(): Map<IntegrationIds, ConfiguredIntegrationDescriptor[]> {\n\t\tif (this._configured == null) {\n\t\t\tthis._configured = new Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>();\n\n\t\t\tconst storedConfigured = this.ctx.storage.get('integrations:configured');\n\t\t\tfor (const [id, configured] of Object.entries(storedConfigured ?? {}) as [\n\t\t\t\tIntegrationIds,\n\t\t\t\tStoredConfiguredIntegrationDescriptor[],\n\t\t\t][]) {\n\t\t\t\tif (configured == null) continue;\n\n\t\t\t\tconst descriptors = configured.map(d => ({\n\t\t\t\t\t...d,\n\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? getStoredBaseUrl(d.baseUrl, d.domain) : d.baseUrl,\n\t\t\t\t\tdomain: this.normalizeConfiguredDomain(id, d.domain),\n\t\t\t\t\t// Backfill a stable connection id for pre-multi-account stored data: the domain for\n\t\t\t\t\t// self-managed hosts, or the provider's canonical domain for cloud (which is the legacy\n\t\t\t\t\t// secret-key session id), so existing secrets keep resolving with zero migration. Fall back\n\t\t\t\t\t// to the integration id (never empty) when no domain is known — self-managed providers carry\n\t\t\t\t\t// `domain: ''` in metadata, and an empty id would collide across connections and produce\n\t\t\t\t\t// ambiguous secret keys (`...|`).\n\t\t\t\t\tid: d.id || d.domain || providersMetadata[id]?.domain || id,\n\t\t\t\t\texpiresAt: d.expiresAt ? new Date(d.expiresAt) : undefined,\n\t\t\t\t}));\n\t\t\t\tthis._configured.set(id, descriptors);\n\t\t\t}\n\t\t}\n\n\t\treturn this._configured;\n\t}\n\n\t// Synchronous: reads the in-memory configured map (lazily hydrated from storage). No async work —\n\t// the old async variant only existed to weave in the built-in VS Code GitHub session, which is gone.\n\tgetConfigured(\n\t\tid?: IntegrationIds,\n\t\toptions?: { cloud?: boolean; domain?: string },\n\t): ConfiguredIntegrationDescriptor[] {\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\n\t\tconst configured =\n\t\t\tid != null\n\t\t\t\t? this.configured.get(id)\n\t\t\t\t: [...flatten<ConfiguredIntegrationDescriptor>(this.configured.values())];\n\t\tif (!configured?.length) return descriptors;\n\n\t\tif (options?.domain != null || options?.cloud != null) {\n\t\t\tfor (const descriptor of configured) {\n\t\t\t\tif (\n\t\t\t\t\t(options?.domain != null &&\n\t\t\t\t\t\t!this.domainsMatch(id ?? descriptor.integrationId, descriptor.domain, options.domain)) ||\n\t\t\t\t\t(options?.cloud === true && !descriptor.cloud) ||\n\t\t\t\t\t(options?.cloud === false && descriptor.cloud)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdescriptors.push(descriptor);\n\t\t\t}\n\t\t} else {\n\t\t\tdescriptors.push(...configured);\n\t\t}\n\n\t\treturn descriptors;\n\t}\n\n\tprivate async storeConfigured(): Promise<void> {\n\t\t// We need to convert the map to a record to store\n\t\tconst configured: StoredIntegrationConfigurations = {} as unknown as StoredIntegrationConfigurations;\n\t\tfor (const [id, descriptors] of this.configured) {\n\t\t\tconfigured[id] = descriptors.map(d => ({\n\t\t\t\t...d,\n\t\t\t\texpiresAt: d.expiresAt\n\t\t\t\t\t? d.expiresAt instanceof Date\n\t\t\t\t\t\t? d.expiresAt.toISOString()\n\t\t\t\t\t\t: d.expiresAt\n\t\t\t\t\t: undefined,\n\t\t\t}));\n\t\t}\n\n\t\tconst pending = this.storeConfiguredQueue.then(() =>\n\t\t\tthis.ctx.storage.store('integrations:configured', configured),\n\t\t);\n\t\tthis.storeConfiguredQueue = pending.catch(() => {});\n\t\tawait pending;\n\t}\n\n\tprivate async addOrUpdateConfigured(descriptor: ConfiguredIntegrationDescriptor): Promise<void> {\n\t\tdescriptor = {\n\t\t\t...descriptor,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(descriptor.integrationId)\n\t\t\t\t? getStoredBaseUrl(descriptor.baseUrl, descriptor.domain)\n\t\t\t\t: descriptor.baseUrl,\n\t\t\tdomain: this.normalizeConfiguredDomain(descriptor.integrationId, descriptor.domain),\n\t\t};\n\t\tconst descriptors = this.configured.get(descriptor.integrationId) ?? [];\n\t\t// Key connections by their stable id (+ cloud, to preserve legacy local/cloud coexistence) so\n\t\t// multiple accounts on the same provider+domain no longer overwrite each other.\n\t\tconst existing = descriptors.find(d => d.id === descriptor.id && d.cloud === descriptor.cloud);\n\n\t\t// The first connection for a provider/domain is primary by default; an explicit flag (or the\n\t\t// existing value) always wins. Self-managed providers can span multiple hosts under the same\n\t\t// integration id, so each host needs its own primary.\n\t\tconst primaryScope = this.getPrimaryScope(descriptor.integrationId, descriptors, descriptor.domain);\n\t\tconst primary = descriptor.primary ?? existing?.primary ?? !primaryScope.some(d => d.primary);\n\t\t// Preserve a previously-resolved type/accountName when the incoming write doesn't carry them (e.g.\n\t\t// the model re-stores the primary session with an empty account, after reconcile enriched it).\n\t\tconst type = descriptor.type ?? existing?.type;\n\t\tconst accountName = descriptor.accountName ?? existing?.accountName;\n\t\tconst normalized: ConfiguredIntegrationDescriptor = {\n\t\t\t...descriptor,\n\t\t\tprimary: primary,\n\t\t\ttype: type,\n\t\t\taccountName: accountName,\n\t\t};\n\n\t\tlet changed: boolean;\n\t\tif (existing != null) {\n\t\t\t// `baseUrl` is compared alongside `domain`, not folded into it: `domain` is normalized to a bare\n\t\t\t// host, so a connection re-pointed to another context path on the SAME host is identical by domain\n\t\t\t// and would otherwise be treated as unchanged — leaving the stored descriptor, and every warm\n\t\t\t// integration listening for the change, on the old address.\n\t\t\tif (\n\t\t\t\texisting.domain === normalized.domain &&\n\t\t\t\tsameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) &&\n\t\t\t\texisting.expiresAt === normalized.expiresAt &&\n\t\t\t\texisting.scopes === normalized.scopes &&\n\t\t\t\t(existing.primary ?? false) === (normalized.primary ?? false) &&\n\t\t\t\texisting.type === normalized.type &&\n\t\t\t\texisting.accountName === normalized.accountName\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only fire the change event on address/scopes/primary/type/accountName changes (ignore expiresAt churn)\n\t\t\tchanged =\n\t\t\t\texisting.domain !== normalized.domain ||\n\t\t\t\t!sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) ||\n\t\t\t\texisting.scopes !== normalized.scopes ||\n\t\t\t\t(existing.primary ?? false) !== (normalized.primary ?? false) ||\n\t\t\t\texisting.type !== normalized.type ||\n\t\t\t\texisting.accountName !== normalized.accountName;\n\n\t\t\t// remove the existing descriptor from the array\n\t\t\tdescriptors.splice(descriptors.indexOf(existing), 1);\n\t\t} else {\n\t\t\tchanged = true;\n\t\t}\n\n\t\tdescriptors.push(normalized);\n\t\tthis.configured.set(descriptor.integrationId, descriptors);\n\n\t\tif (changed) {\n\t\t\tthis.fireChange(descriptor.integrationId);\n\t\t}\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate async removeConfigured(\n\t\tid: IntegrationIds,\n\t\toptions: { connectionId: string; cloud: boolean | undefined },\n\t): Promise<void> {\n\t\tconst existing = this.configured.get(id);\n\t\tif (existing == null || existing.length === 0) return;\n\n\t\tconst removedPrimaryDomains = new Set<string | undefined>();\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\t\tfor (const d of existing) {\n\t\t\tif (\n\t\t\t\td.id === options.connectionId &&\n\t\t\t\t(options?.cloud == null ||\n\t\t\t\t\t(options?.cloud === true && d.cloud === true) ||\n\t\t\t\t\t(options?.cloud === false && d.cloud === false))\n\t\t\t) {\n\t\t\t\tif (d.primary) {\n\t\t\t\t\tremovedPrimaryDomains.add(d.domain);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tdescriptors.push(d);\n\t\t}\n\n\t\tif (descriptors.length === existing.length) return; // nothing matched\n\n\t\t// Promote a secondary to primary when the removed connection was the primary and others remain\n\t\t// in the same primary scope, so the provider/host stays in a well-defined connected state.\n\t\tfor (const domain of removedPrimaryDomains) {\n\t\t\tif (descriptors.some(d => this.isInPrimaryScope(id, d, domain) && d.primary)) continue;\n\n\t\t\tconst index = descriptors.findIndex(d => this.isInPrimaryScope(id, d, domain));\n\t\t\tif (index !== -1) {\n\t\t\t\tdescriptors[index] = { ...descriptors[index], primary: true };\n\t\t\t}\n\t\t}\n\n\t\tthis.configured.set(id, descriptors);\n\t\tthis.fireChange(undefined, id);\n\t\tawait this.storeConfigured();\n\t}\n\n\tasync storeSession(id: IntegrationIds, session: ProviderAuthenticationSession): Promise<void> {\n\t\tawait this.writeSecret(id, session);\n\t}\n\n\tasync getStoredSession(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\tconst sessionId = this.resolveConnectionId(id, descriptor);\n\t\tlet session = descriptor.cloud === true ? undefined : await this.readSecret(id, sessionId, false);\n\n\t\tlet cloudIfMissing = false;\n\t\tif (session != null) {\n\t\t\t// Check the `expiresAt` field\n\t\t\t// If it has an expiresAt property and the key is the old type, then it's a cloud session,\n\t\t\t// so delete it from the local key and\n\t\t\t// store with the \"cloud\" type key, and then use that one.\n\t\t\t// Otherwise it's a local session under the local key, so just return it.\n\t\t\tif (session.expiresAt != null) {\n\t\t\t\tcloudIfMissing = true;\n\t\t\t\tawait Promise.allSettled([this.deleteSecrets(id, session.id), this.writeSecret(id, session)]);\n\t\t\t}\n\t\t}\n\n\t\t// If no local session we try to restore a session with the cloud key\n\t\tif (session == null && descriptor.cloud !== false) {\n\t\t\tcloudIfMissing = true;\n\t\t\tsession = await this.readSecret(id, sessionId, true);\n\t\t}\n\n\t\treturn convertStoredSessionToSession(session, descriptor, cloudIfMissing);\n\t}\n\n\tasync deleteStoredSessions(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tawait this.deleteSecrets(id, this.resolveConnectionId(id, descriptor), cloud, options);\n\t}\n\n\tasync deleteAllStoredSessions(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\tawait this.deleteAllSecrets(id, cloud, domain);\n\t}\n\n\t/**\n\t * Reclaims all stored configuration + secrets for the given (typically retired) integration ids.\n\t * Used to clean up state left behind when an id is removed from the model — e.g. the local\n\t * self-managed `github-enterprise`/`gitlab-self-hosted` ids dropped when integrations went cloud-only.\n\t * Domains are read from the stored config (secrets are keyed by domain); ids with no stored config\n\t * are a no-op.\n\t */\n\tasync purgeStoredConfiguration(ids: readonly string[]): Promise<void> {\n\t\tconst stored =\n\t\t\tthis.ctx.storage.get<Record<string, StoredConfiguredIntegrationDescriptor[] | undefined>>(\n\t\t\t\t'integrations:configured',\n\t\t\t);\n\t\tif (stored == null) return;\n\n\t\tlet changed = false;\n\t\tfor (const id of ids) {\n\t\t\tif (!(id in stored)) continue;\n\n\t\t\tfor (const descriptor of stored[id] ?? []) {\n\t\t\t\t// Mirror the hydration backfill so the secret keys we delete match what was written: fall back\n\t\t\t\t// through domain, the provider's canonical domain, then the integration id (never empty). A\n\t\t\t\t// legacy cloud descriptor stored neither id nor domain, so an empty connection id here would\n\t\t\t\t// miss the canonical-domain secret (e.g. `integration.auth.cloud:github|github.com`) and orphan it.\n\t\t\t\tconst connectionId =\n\t\t\t\t\tdescriptor.id || descriptor.domain || providersMetadata[id as IntegrationIds]?.domain || id;\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tif (descriptor.domain) {\n\t\t\t\t\tawait this.ctx.storage.deleteWorkspace(`connected:${id}:${descriptor.domain}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchanged = true;\n\t\t}\n\t\tif (!changed) return;\n\n\t\tconst remaining = Object.fromEntries(Object.entries(stored).filter(([key]) => !ids.includes(key)));\n\t\tawait this.ctx.storage.store('integrations:configured', remaining as StoredIntegrationConfigurations);\n\t}\n\n\tasync deleteSecrets(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tif (cloud == null || cloud === false) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (cloud == null || cloud === true) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (options?.preserveConfigured) return;\n\n\t\tawait this.removeConfigured(id, { connectionId: connectionId, cloud: cloud });\n\t}\n\n\tasync deleteAllSecrets(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\t// Delete every connection's secret (multi-account): secrets are keyed per connection id, so a\n\t\t// single canonical-domain delete would orphan secondary tokens. When a domain is given (self-managed\n\t\t// disconnect of one host), scope to that host so other hosts under the same provider id survive.\n\t\tconst descriptors = this.configured.get(id);\n\t\tconst connectionIds = [\n\t\t\t...new Set(\n\t\t\t\t(domain != null ? descriptors?.filter(c => this.domainsMatch(id, c.domain, domain)) : descriptors)?.map(\n\t\t\t\t\tc => c.id,\n\t\t\t\t) ?? [],\n\t\t\t),\n\t\t];\n\t\tif (connectionIds.length) {\n\t\t\tfor (const connectionId of connectionIds) {\n\t\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// A domain-scoped clear that matched nothing means this host has no stored connections, so it must be\n\t\t// a no-op. The canonical-domain fallback below would target a bogus key (self-managed canonical domain\n\t\t// is empty), so skip it.\n\t\tif (domain != null) return;\n\n\t\t// No configured connections (e.g. an orphaned secret with no descriptor): fall back to the\n\t\t// provider's canonical domain, which is the legacy session id for cloud providers. Mirror the\n\t\t// hydration backfill's `|| id` guard so a self-managed provider (empty canonical domain) doesn't\n\t\t// target an ambiguous empty connection id.\n\t\tawait this.deleteSecrets(id, providersMetadata[id]?.domain || id, cloud);\n\t}\n\n\tasync writeSecret(id: IntegrationIds, session: ProviderAuthenticationSession | StoredSession): Promise<void> {\n\t\tawait this.ctx.storage.storeSecret(\n\t\t\tthis.getSecretKey(id, session.id, session.cloud ?? false),\n\t\t\tJSON.stringify(session),\n\t\t);\n\n\t\tawait this.addOrUpdateConfigured({\n\t\t\tid: session.id,\n\t\t\tintegrationId: id,\n\t\t\tdomain: isSelfManagedHostIntegrationId(id) ? session.domain : undefined,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? session.baseUrl : undefined,\n\t\t\texpiresAt: session.expiresAt,\n\t\t\tscopes: session.scopes.join(','),\n\t\t\tcloud: session.cloud ?? false,\n\t\t\ttype: session.type,\n\t\t\taccountName: session.account?.label || undefined,\n\t\t});\n\t}\n\n\tasync readSecret(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): Promise<StoredSession | undefined> {\n\t\tlet storedSession: StoredSession | undefined;\n\t\ttry {\n\t\t\tconst sessionJSON = await this.ctx.storage.getSecret(this.getSecretKey(id, sessionId, cloud));\n\t\t\tif (sessionJSON) {\n\t\t\t\tstoredSession = JSON.parse(sessionJSON);\n\t\t\t\tif (storedSession != null) {\n\t\t\t\t\tconst configured = this.configured.get(id);\n\t\t\t\t\tconst connectionId = storedSession.id ?? sessionId;\n\t\t\t\t\tconst sessionCloud = storedSession.cloud ?? cloud;\n\t\t\t\t\tconst domain = isSelfManagedHostIntegrationId(id)\n\t\t\t\t\t\t? (storedSession.domain ?? storedSession.id)\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (\n\t\t\t\t\t\tconfigured == null ||\n\t\t\t\t\t\tconfigured.length === 0 ||\n\t\t\t\t\t\t!configured.some(\n\t\t\t\t\t\t\tc => c.id === connectionId && c.integrationId === id && c.cloud === sessionCloud,\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tawait this.addOrUpdateConfigured({\n\t\t\t\t\t\t\tid: connectionId,\n\t\t\t\t\t\t\tintegrationId: id,\n\t\t\t\t\t\t\tdomain: domain,\n\t\t\t\t\t\t\t// Rebuilt from the secret, so it must carry the address the session does; omitting it\n\t\t\t\t\t\t\t// leaves the descriptor claiming no context path and makes reconcile read every\n\t\t\t\t\t\t\t// routine check-in as a re-pointed connection.\n\t\t\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? storedSession.baseUrl : undefined,\n\t\t\t\t\t\t\texpiresAt: storedSession.expiresAt,\n\t\t\t\t\t\t\tscopes: storedSession.scopes.join(','),\n\t\t\t\t\t\t\tcloud: sessionCloud,\n\t\t\t\t\t\t\ttype: storedSession.type,\n\t\t\t\t\t\t\taccountName: storedSession.account?.label || undefined,\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} catch (_ex) {\n\t\t\ttry {\n\t\t\t\tawait this.deleteSecrets(id, sessionId, cloud);\n\t\t\t} catch {}\n\t\t}\n\t\treturn storedSession;\n\t}\n\n\tprivate getSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): `integration.auth:${IntegrationIds}|${string}` | `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn cloud ? this.getCloudSecretKey(id, sessionId) : this.getLocalSecretKey(id, sessionId);\n\t}\n\n\tprivate getLocalSecretKey(id: IntegrationIds, sessionId: string): `integration.auth:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth:${id}|${sessionId}`;\n\t}\n\n\tprivate getCloudSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t): `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth.cloud:${id}|${sessionId}`;\n\t}\n\n\tprivate getPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptors: ConfiguredIntegrationDescriptor[],\n\t\tdomain: string | undefined,\n\t): ConfiguredIntegrationDescriptor[] {\n\t\treturn isSelfManagedHostIntegrationId(id)\n\t\t\t? descriptors.filter(d => this.isInPrimaryScope(id, d, domain))\n\t\t\t: descriptors;\n\t}\n\n\tprivate isInPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptor: ConfiguredIntegrationDescriptor,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, descriptor.domain, domain);\n\t}\n\n\t/**\n\t * Resolves the connection id used as the secret-key session id for an auth descriptor.\n\t * - If the descriptor targets a specific connection (`connectionId`), that wins.\n\t * - Otherwise returns the primary connection's id for the provider (matching the descriptor's domain\n\t * for self-managed hosts; any connection for cloud hosts, where the stored domain is undefined).\n\t * - Falls back to the descriptor domain when nothing is configured yet, preserving legacy\n\t * (pre-multi-account) reads with zero secret migration.\n\t */\n\tresolveConnectionId(id: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): string {\n\t\t// An empty connectionId is not a real target — treat it as \"no target\" so it resolves the primary\n\t\t// rather than keying a secret under an empty id.\n\t\tif (descriptor.connectionId) return descriptor.connectionId;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? descriptor.domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, domain, descriptor.cloud);\n\t\treturn (candidates?.find(c => c.primary) ?? candidates?.[0])?.id ?? descriptor.domain;\n\t}\n\n\t/**\n\t * Returns the id of the configured connection an unscoped descriptor resolves to (the primary for the\n\t * descriptor's domain, matching {@link resolveConnectionId}'s selection), or `undefined` when nothing is\n\t * configured for it. Unlike {@link resolveConnectionId} this only returns a genuine per-connection cloud\n\t * token id (never a local/PAT or domain-derived legacy id), so callers can safely use it to scope a cloud\n\t * token fetch to one host of a multi-host self-managed provider, or fall through to the provider-global\n\t * primary endpoint on `undefined`.\n\t */\n\t/**\n\t * The address already stored for a self-managed connection — see {@link ConfiguredIntegrationDescriptor.baseUrl}.\n\t *\n\t * Exists so a session rebuilt from a backend response that omits the domain can keep the address the\n\t * connection was configured with, rather than silently falling back to the bare host and losing a context\n\t * path the user never changed. Looked up by connection id first, then by host for an unscoped fetch.\n\t */\n\t/**\n\t * Whether `connectionId` names a cloud connection configured for `domain`'s host.\n\t *\n\t * The caller-facing question is \"is this id evidence that a token belongs to this host\". A connection id\n\t * can arrive from a caller rather than from our own configuration, so it is only evidence once it matches\n\t * a descriptor we stored for that host — see the host guard in `CloudIntegrationAuthenticationProvider`.\n\t */\n\tisConnectionConfiguredForHost(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string | undefined,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\tif (connectionId == null) return false;\n\n\t\tconst connection = this.getConfigured(id, { cloud: true }).find(c => c.id === connectionId);\n\t\tif (connection == null) return false;\n\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, connection.domain, domain);\n\t}\n\n\tgetConfiguredBaseUrl(id: IntegrationIds, connectionId: string | undefined, domain?: string): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return undefined;\n\n\t\tconst configured = this.getConfigured(id, { cloud: true });\n\t\tconst byId = connectionId != null ? configured.find(c => c.id === connectionId) : undefined;\n\t\tif (byId != null) return byId.baseUrl;\n\n\t\treturn configured.find(c => this.domainsMatch(id, c.domain, domain))?.baseUrl;\n\t}\n\n\tgetConfiguredConnectionId(id: IntegrationIds, domain: string | undefined, cloud?: boolean): string | undefined {\n\t\tconst scoped = isSelfManagedHostIntegrationId(id) ? domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, scoped, cloud);\n\t\tconst connection = candidates?.find(c => c.primary) ?? candidates?.[0];\n\t\tif (connection == null) return undefined;\n\n\t\t// Only return a real per-connection cloud token id. Reject a local (PAT) descriptor (surfaced by\n\t\t// scopeConnectionCandidates' non-cloud fallback) and a domain-derived legacy id (the descriptor's own\n\t\t// domain or the provider's canonical domain — see ConfiguredIntegrationDescriptor.id), either of which\n\t\t// would target `v1/provider-tokens/tokens/{domain}` (rejected as a non-uuid). Callers then fall through\n\t\t// to the provider-scoped primary endpoint.\n\t\tif (!connection.cloud) return undefined;\n\n\t\tconst canonicalDomain = providersMetadata[id]?.domain;\n\t\tif (connection.id === connection.domain || (canonicalDomain && connection.id === canonicalDomain)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn connection.id;\n\t}\n\n\t/**\n\t * Selects configured descriptors for a provider matching `domain`, further narrowed to the `cloud`\n\t * variant when specified. A connection id can have both a local (PAT) and cloud descriptor whose ids\n\t * differ (e.g. a legacy local descriptor keyed by domain alongside a real cloud token id); scoping by\n\t * `cloud` keeps a cloud-scoped read/delete from resolving the local id (and vice versa). Falls back to\n\t * the domain-only set when no variant matches, so legacy single-variant resolution is unchanged.\n\t */\n\tprivate scopeConnectionCandidates(\n\t\tid: IntegrationIds,\n\t\tdomain: string | undefined,\n\t\tcloud: boolean | undefined,\n\t): ConfiguredIntegrationDescriptor[] | undefined {\n\t\tconst candidates = this.configured.get(id)?.filter(c => this.domainsMatch(id, c.domain, domain));\n\t\tif (cloud == null || candidates == null) return candidates;\n\n\t\tconst scoped = candidates.filter(c => c.cloud === cloud);\n\t\treturn scoped.length ? scoped : candidates;\n\t}\n\n\t/**\n\t * Marks the given connection as the primary/default for the provider and clears the flag on its\n\t * siblings. Persists immediately instead of relying on a session re-store to carry the primary flag.\n\t */\n\tasync setPrimaryConnection(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\tconst descriptors = this.configured.get(id);\n\t\tif (descriptors == null || descriptors.length === 0) return;\n\t\tif (!descriptors.some(d => d.id === connectionId)) return;\n\n\t\tconst target =\n\t\t\tdescriptors.find(d => d.id === connectionId && d.cloud) ?? descriptors.find(d => d.id === connectionId);\n\t\tif (target == null) return;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? target.domain : undefined;\n\t\t// A connection id can have both a local (PAT) and cloud descriptor. Mark the primary on a single\n\t\t// canonical variant (prefer cloud, since multi-account primaries are cloud-driven) within the\n\t\t// provider/host scope, so other self-managed hosts keep their own default connection.\n\t\tconst primaryIsCloud = descriptors.some(\n\t\t\td => d.id === connectionId && d.cloud && this.isInPrimaryScope(id, d, domain),\n\t\t);\n\n\t\tlet changed = false;\n\t\tconst updated = descriptors.map(d => {\n\t\t\tif (!this.isInPrimaryScope(id, d, domain)) return d;\n\n\t\t\tconst primary = d.id === connectionId && (d.cloud ?? false) === primaryIsCloud;\n\t\t\tif ((d.primary ?? false) === primary) return d;\n\n\t\t\tchanged = true;\n\t\t\treturn { ...d, primary: primary };\n\t\t});\n\t\tif (!changed) return;\n\n\t\tthis.configured.set(id, updated);\n\t\tthis.fireChange(id);\n\t\tawait this.storeConfigured();\n\t}\n\n\t/**\n\t * Removes a single connection (its secret + configured descriptor). If it was the primary and other\n\t * connections remain, a secondary is promoted (see {@link removeConfigured}). Unlike\n\t * {@link deleteAllStoredSessions}, this only affects the targeted connection. Pass `cloud` to scope\n\t * the removal to the cloud/local variant when a local PAT and a cloud session share a connection id.\n\t */\n\tasync deleteConnection(id: IntegrationIds, connectionId: string, cloud?: boolean): Promise<void> {\n\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t}\n\n\tprivate _addedIds = new Set<IntegrationIds>();\n\tprivate _removedIds = new Set<IntegrationIds>();\n\tprivate _fireChangeDebounced?: () => void;\n\n\tprivate normalizeConfiguredDomain(id: IntegrationIds, domain: string | undefined): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return domain;\n\n\t\treturn hostFromDomain(domain) ?? domain;\n\t}\n\n\tprivate domainsMatch(id: IntegrationIds, first: string | undefined, second: string | undefined): boolean {\n\t\tif (first === second) return true;\n\t\tif (!isSelfManagedHostIntegrationId(id)) return false;\n\t\tif (first == null || second == null) return false;\n\n\t\treturn areDomainsOnSameHost(first, second);\n\t}\n\n\tprivate fireChange(added?: IntegrationIds, removed?: IntegrationIds) {\n\t\tthis._fireChangeDebounced ??= debounce(() => {\n\t\t\tconst added = [...this._addedIds];\n\t\t\tthis._addedIds.clear();\n\t\t\tconst removed = [...this._removedIds];\n\t\t\tthis._removedIds.clear();\n\n\t\t\tthis._onDidChange.fire({ added: added, removed: removed });\n\t\t}, 250);\n\n\t\tif (added != null) {\n\t\t\tthis._addedIds.add(added);\n\t\t}\n\t\tif (removed != null) {\n\t\t\tthis._removedIds.add(removed);\n\t\t}\n\t\tthis._fireChangeDebounced();\n\t}\n}\n\nfunction convertStoredSessionToSession(\n\tstoredSession: StoredSession | undefined,\n\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\tcloudIfMissing: boolean,\n): ProviderAuthenticationSession | undefined {\n\tif (storedSession == null) return undefined;\n\n\treturn {\n\t\tid: storedSession.id,\n\t\taccessToken: storedSession.accessToken,\n\t\taccount: {\n\t\t\tid: storedSession.account?.id ?? '',\n\t\t\tlabel: storedSession.account?.label ?? '',\n\t\t},\n\t\tscopes: storedSession.scopes,\n\t\tcloud: storedSession.cloud ?? cloudIfMissing,\n\t\texpiresAt: storedSession.expiresAt ? new Date(storedSession.expiresAt) : undefined,\n\t\tdomain: storedSession.domain ?? descriptor.domain,\n\t\tbaseUrl: getStoredBaseUrl(storedSession.baseUrl, storedSession.domain, storedSession.protocol),\n\t\tprotocol: storedSession.protocol,\n\t\ttype: storedSession.type,\n\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello); without\n\t\t// this a rehydrated session silently loses the key and every read no-ops.\n\t\tappKey: storedSession.appKey,\n\t};\n}\n\nfunction getStoredBaseUrl(\n\tbaseUrl: string | undefined,\n\tdomain: string | undefined,\n\tprotocol?: string,\n): string | undefined {\n\tif (baseUrl != null) return baseUrl;\n\n\tconst legacyUrl = baseUrlFromDomain(domain, protocol);\n\treturn legacyUrl != null && new URL(legacyUrl).pathname !== '/' ? legacyUrl : undefined;\n}\n"],"mappings":"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"}
@@ -2,8 +2,8 @@ import { trace } from "../../../utils/decorators/log.js";
2
2
  import __decorate from "../../../_virtual/@oxc-project+runtime/helpers/esm/decorate.js";
3
3
  import { Emitter } from "../../../utils/event.js";
4
4
  import { isSupportedCloudIntegrationId } from "./models.js";
5
- import { isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
6
5
  import { areDomainsOnSameHost } from "../utils/domain.utils.js";
6
+ import { isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
7
7
  //#region ../plus/integrations/src/authentication/integrationAuthenticationProvider.ts
8
8
  const maxSmallIntegerV8 = 2 ** 30 - 1;
9
9
  var IntegrationAuthenticationProviderBase = class {
@@ -1,6 +1,6 @@
1
1
  import { TokenWithInfo } from "./authentication/models.js";
2
2
  import { IntegrationIds } from "./constants.js";
3
- import { ProviderWarning } from "./results.js";
3
+ import { ProviderWarning, ProviderWarningCause, ProviderWarningScope } from "./results.js";
4
4
  import { CollectionMetadata, CollectionScopeFailure } from "@gitkraken/provider-apis";
5
5
  //#region ../plus/integrations/src/collectionMetadata.d.ts
6
6
  /**
@@ -25,9 +25,54 @@ export declare function throwIfCallerContractError(ex: unknown): void;
25
25
  * Maps a caught GitLens request error to the SDK collection failure vocabulary used inside provider fan-outs.
26
26
  */
27
27
  export declare function toCollectionFailureKind(ex: unknown): CollectionScopeFailure['kind'];
28
- /** Builds a structured SDK scope failure from a caught GitLens request error. */
29
- export declare function toCollectionScopeFailure(scope: CollectionScopeFailure['scope'], ex: unknown): CollectionScopeFailure;
28
+ /**
29
+ * What an authentication refusal's response said, kept on the failure so the provider can name its cause once the
30
+ * credential behind it is confirmed (see `IntegrationBase.confirmScopedAuthFailures`). Only these facts are kept,
31
+ * never the response: its headers carry the provider's session cookies.
32
+ */
33
+ export interface ProviderRefusal {
34
+ status: number;
35
+ /** The provider's own explanation: Azure DevOps' `X-TFS-ServiceError`, or the error body's message. */
36
+ detail?: string;
37
+ /** The provider's error type, when the body names one (Azure DevOps `typeKey`). */
38
+ typeKey?: string;
39
+ }
40
+ /** A scope failure as recorded here: the SDK's shape, plus what the refusal said and, once it is known, why. */
41
+ export interface ProviderScopeFailure extends CollectionScopeFailure {
42
+ refusal?: ProviderRefusal;
43
+ cause?: ProviderWarningCause;
44
+ /** The provider pinned the refusal on the credential itself, so it is published for the connection, unscoped. */
45
+ credentialRefused?: true;
46
+ }
47
+ /**
48
+ * Builds a structured SDK scope failure from a caught GitLens request error.
49
+ *
50
+ * An authentication refusal is described in the provider's own words when it gives any, and otherwise by the
51
+ * error the provider's response raised, sanitized like any warning's (a page becomes its status), rather than by
52
+ * `AuthenticationError`'s generic "credentials are either invalid or expired": a scope refusing a sound credential
53
+ * is the case this failure exists to tell apart, and that sentence says the opposite.
54
+ */
55
+ export declare function toCollectionScopeFailure(scope: CollectionScopeFailure['scope'], ex: unknown): ProviderScopeFailure;
30
56
  export declare function toCollectionFailureError(failure: CollectionScopeFailure, tokenWithInfo: TokenWithInfo): Error;
57
+ /**
58
+ * Whether `metadata` reports authentication failures only for scopes below the provider, with none for the
59
+ * connection itself. A credential revoked since a provider cached its discovery produces exactly this shape,
60
+ * the same one a scope's own refusal does; `IntegrationBase.confirmScopedAuthFailures` tells them apart.
61
+ *
62
+ * Classified exactly as {@link assessCollectionMetadata} classifies the warnings, so it agrees with what a
63
+ * consumer would be shown.
64
+ */
65
+ export declare function hasOnlyScopedAuthFailures(metadata: CollectionMetadata | undefined): boolean;
66
+ /**
67
+ * Marks each scoped authentication failure in `metadata` whose refusal the provider pins on the credential, so it is
68
+ * published for the connection rather than for its scope. Returns whether any was.
69
+ */
70
+ export declare function markCredentialRefusals(metadata: CollectionMetadata | undefined, isCredentialRefusal: (refusal: ProviderRefusal) => boolean): boolean;
71
+ /**
72
+ * Names the cause of each scoped authentication failure in `metadata` that kept its refusal. Only for a read whose
73
+ * credential was just confirmed: before that, the refusals this can name look exactly like a dead credential.
74
+ */
75
+ export declare function attributeScopedAuthFailures(metadata: CollectionMetadata | undefined, describe: (refusal: ProviderRefusal, scope: ProviderWarningScope) => ProviderWarningCause | undefined): void;
31
76
  /**
32
77
  * Whether SDK metadata describes a read that may be missing results.
33
78
  *