@gitkraken/core-gitlens 0.10.2 → 0.10.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"azureDevOpsServer.js","names":[],"sources":["../../../../../plus/integrations/src/providers/azureDevOpsServer.ts"],"sourcesContent":["import type { CollectionScopeFailure } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { IssueSearchCriteria } from '@gitlens/git/models/issue.js';\nimport type { PullRequest, PullRequestSearchCriteria, PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport { CancellationError } from '@gitlens/utils/cancellation.js';\nimport { uuid } from '@gitlens/utils/crypto.js';\nimport type { Emitter } from '@gitlens/utils/event.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession, TokenWithInfo } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { GitSelfManagedHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { IntegrationReadUnavailableError, RequestNotFoundError } from '../errors.js';\nimport type { IntegrationConnectionChangeEvent } from '../integrationService.js';\nimport type {\n\tIntegrationKey,\n\tProviderIssueSearchPage,\n\tProviderPullRequestCount,\n\tProviderPullRequestSearchPage,\n\tProviderSearchCount,\n} from '../models/integration.js';\nimport type { AzureOrganizationDescriptor, AzureProjectDescriptor } from './azure/models.js';\nimport type { AzurePullRequestSearchPosition } from './azure/search.js';\nimport {\n\tcompareAzurePullRequestSearchPositions,\n\tgetInvalidAzureSearchDate,\n\tisAzureWorkItemSearchFirstPage,\n\tparseAzurePullRequestSearchCursor,\n\ttoAzurePullRequestSearchCursorKey,\n\ttoAzurePullRequestSearchFilter,\n\ttoAzurePullRequestSearchPosition,\n\ttoAzureSearchPageSize,\n} from './azure/search.js';\nimport type { AzureReviewerRead } from './azureDevOps.js';\nimport {\n\tAzureDevOpsIntegrationBase,\n\tgetAzureRepositoryApiBaseUrl,\n\tgetAzureRepositoryIdentity,\n\tmarkMyGroupReviews,\n\tsameAzureName,\n\ttoAzureReviewerReads,\n\tuniqueAzureNames,\n} from './azureDevOps.js';\nimport type {\n\tProviderApiCollectionResult,\n\tProviderAzureResource,\n\tProviderPullRequest,\n\tProviderRepoInput,\n\tProviderReposInput,\n} from './models.js';\nimport {\n\tfromProviderPullRequest,\n\tgetProviderPullRequestIdentity,\n\tisNativePullRequest,\n\tprovidersMetadata,\n\tPullRequestFilter,\n\ttoProviderPullRequestStates,\n} from './models.js';\nimport type { ProvidersApi } from './providersApi.js';\n\n/**\n * Whether `value` can stand as one route segment. Encoding keeps `.` and `..` as they are, and a URL resolves them\n * as directory steps, so a name of either would address a different route than the one named.\n */\nfunction isAzureRouteSegment(value: string): boolean {\n\treturn value.length > 0 && value !== '.' && value !== '..';\n}\n\n/**\n * Runs one scope of a count probe, answering a refusal of THAT scope (see `searchUnavailable`) as the error itself,\n * which the facade reports and drops without failing the scopes around it. Anything else — a rejected credential,\n * a failed request — still rejects, so the batch reports it.\n */\nasync function refusalAsCount<T>(count: () => Promise<T>): Promise<T | IntegrationReadUnavailableError> {\n\ttry {\n\t\treturn await count();\n\t} catch (ex) {\n\t\tif (ex instanceof IntegrationReadUnavailableError) return ex;\n\n\t\tthrow ex;\n\t}\n}\n\nfunction findAzureCollection(\n\tcollections: readonly AzureOrganizationDescriptor[],\n\tname: string,\n): AzureOrganizationDescriptor | undefined {\n\treturn collections.find(c => sameAzureName(c.name, name) || c.id === name);\n}\n\n/**\n * The most pull requests one facet of a filtered pull-request search drains. Azure has no server-side text, date or\n * draft filter and no ordering, so a facet is read in full before any of them apply; past this bound the facet's\n * unread rows could sort anywhere, and the search reports itself truncated instead of guessing.\n */\nconst azurePullRequestSearchFacetLimit = 1000;\n\n/**\n * The page size a facet drain reads. The SDK's own default for both pull request reads, and the size it sizes\n * `$skip` by, so the budget above counts what was actually read.\n */\nconst azurePullRequestSearchPageSize = 100;\n\n/**\n * How many scopes of one work-item count batch query at once. The facade already runs several batches concurrently,\n * so this multiplies with it: kept small, so a large count preview can't flood a self-hosted server.\n */\nconst azureIssueCountConcurrency = 2;\n\n/**\n * How long a drained pull-request search keeps serving its continuation pages after the last one was read, and the\n * most it lives however actively it is paged. A pagination that keeps reading stays on one consistent drain; one\n * left idle, or paged for longer than that, is read again.\n */\nconst pullRequestSearchIdleTtl = 5 * 60 * 1000;\nconst pullRequestSearchMaxTtl = 30 * 60 * 1000;\n\n/**\n * How long a count reuses the drain a search of its query just read. Long enough for a count shown next to the\n * search it previews to agree with it, short enough that a count polled next to an unapplied filter stays live: the\n * window runs from the drain, not from the last count, so polling never extends it.\n */\nconst pullRequestCountReuseTtl = 60 * 1000;\n\n/**\n * Kept unconverted, so each page converts its rows with the projection its own caller asked for: the drain is shared\n * by every page of a query, whatever `summary` each page passes.\n */\ninterface AzurePullRequestSearchRow {\n\tpr: ProviderPullRequest;\n\tproject: AzureProjectDescriptor;\n\tcurrentAccount: { id: string } | undefined;\n\tposition: AzurePullRequestSearchPosition;\n}\n\ninterface AzurePullRequestSearchDrain {\n\trows: AzurePullRequestSearchRow[];\n\ttruncated: boolean;\n\t/** Collections whose groups couldn't be read, so what only those groups review is missing. */\n\tfailures?: CollectionScopeFailure[];\n}\n\n/**\n * Everything that decides a pull-request search's result set and order, as the value its cursor is fingerprinted\n * from. `pageSize` is left out on purpose: it only decides where a page ends, which the keyset cursor already\n * carries.\n */\nfunction toPullRequestSearchQuery(\n\toptions: { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria },\n\tsort: PullRequestSorting,\n): unknown {\n\tconst criteria = options.criteria;\n\treturn {\n\t\trepos: options.repos?.map(r => [r.namespace, r.project ?? null, r.name]),\n\t\torg: options.org ?? null,\n\t\trelationships: [...new Set(criteria?.relationships ?? [])].sort(),\n\t\tstates: [...new Set(criteria?.states ?? [])].sort(),\n\t\ttext: criteria?.text?.trim() ?? null,\n\t\tdraft: criteria?.draft ?? null,\n\t\tupdatedAfter: criteria?.updatedAfter ?? null,\n\t\tcreatedAfter: criteria?.createdAfter ?? null,\n\t\tsort: sort,\n\t};\n}\n\nconst serverMetadata = providersMetadata[GitSelfManagedHostIntegrationId.AzureDevOpsServer];\nconst serverAuthProvider = Object.freeze({ id: serverMetadata.id, scopes: serverMetadata.scopes });\n\nexport class AzureDevOpsServerIntegration extends AzureDevOpsIntegrationBase<GitSelfManagedHostIntegrationId.AzureDevOpsServer> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = serverAuthProvider;\n\treadonly id = GitSelfManagedHostIntegrationId.AzureDevOpsServer;\n\tprotected readonly key: IntegrationKey<GitSelfManagedHostIntegrationId.AzureDevOpsServer>;\n\treadonly name: string = 'Azure DevOps Server';\n\n\tconstructor(\n\t\tctx: IntegrationServiceContext,\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tgetProvidersApi: () => Promise<ProvidersApi>,\n\t\tdidChangeConnection: Emitter<IntegrationConnectionChangeEvent>,\n\t\treadonly domain: string,\n\t) {\n\t\tsuper(ctx, authenticationService, getProvidersApi, didChangeConnection);\n\t\tthis.key = `${this.id}:${this.domain}`;\n\t}\n\n\tprotected override apiBaseUrlFor(session: ProviderAuthenticationSession): string {\n\t\treturn this.getSelfManagedApiBaseUrl(session);\n\t}\n\n\t/**\n\t * The credential AND the installation it is used against: one integration instance serves every installation on\n\t * its host (context paths differ, the host doesn't), and the same token connected to two of them must not reuse\n\t * one installation's collections or projects on the other.\n\t */\n\tprotected override discoveryKey(session: ProviderAuthenticationSession): string {\n\t\treturn `${session.accessToken}:${this.apiBaseUrlFor(session)}`;\n\t}\n\n\tprotected override getRepositoriesApiBaseUrl(\n\t\tsession: ProviderAuthenticationSession,\n\t\trepos: ProviderReposInput,\n\t): string {\n\t\t// Requests append each repository's organization (its collection), which an address naming that collection\n\t\t// already ends with. Only repositories of a single organization can share one base; mixed ones keep the\n\t\t// address, so another collection's repository stays below it (see `resolveRepository`).\n\t\tconst owners = new Set(repos.map(r => (typeof r === 'object' ? r.namespace : undefined)));\n\t\tconst [owner] = owners;\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\t\treturn owners.size === 1 && owner != null\n\t\t\t? getAzureRepositoryApiBaseUrl(baseUrl, { owner: owner, virtualDirectory: undefined })\n\t\t\t: baseUrl;\n\t}\n\n\tprotected override getApiOptions(\n\t\tsession: ProviderAuthenticationSession,\n\t\tdoNotConvertToPat: boolean = false,\n\t): {\n\t\ttokenWithInfo: TokenWithInfo<GitSelfManagedHostIntegrationId.AzureDevOpsServer>;\n\t\toptions: { isPAT: boolean; baseUrl?: string };\n\t} {\n\t\tconst { options, ...rest } = super.getApiOptions(session, doNotConvertToPat);\n\t\treturn {\n\t\t\t...rest,\n\t\t\toptions: { ...options, baseUrl: this.apiBaseUrlFor(session) },\n\t\t};\n\t}\n\n\tprotected override getCollectionApiOptions(\n\t\tsession: ProviderAuthenticationSession,\n\t\tcollection: string,\n\t): { isPAT: boolean; baseUrl?: string } {\n\t\treturn {\n\t\t\t...super.getCollectionApiOptions(session, collection),\n\t\t\tbaseUrl: this.collectionApiBaseUrl(session, collection),\n\t\t};\n\t}\n\n\t/** The base a request below `collection` appends to: the installation, whichever the address already ends in. */\n\tprotected override collectionApiBaseUrl(session: ProviderAuthenticationSession, collection: string): string {\n\t\treturn getAzureRepositoryApiBaseUrl(this.apiBaseUrlFor(session), {\n\t\t\towner: collection,\n\t\t\tvirtualDirectory: undefined,\n\t\t});\n\t}\n\n\t/**\n\t * Only the server level lists the collections: below one, Azure DevOps Server answers `projectCollections` with a\n\t * 404 page. On that 404 an address that names a collection reads the collection's own connection data, whose\n\t * `webApplicationRelativeDirectory` spells it, and reports it as the one collection visible there.\n\t */\n\tprotected override async requestResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\tuserId: string,\n\t): Promise<ProviderAzureResource[] | undefined> {\n\t\ttry {\n\t\t\treturn await super.requestResourcesForUser(session, userId);\n\t\t} catch (ex) {\n\t\t\tif (!(ex instanceof RequestNotFoundError)) throw ex;\n\n\t\t\tconst azure = await this.authenticationService.apis.azure;\n\t\t\tconst addressed = await azure?.getAddressedCollection(\n\t\t\t\tthis,\n\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\tthis.apiBaseUrlFor(session),\n\t\t\t);\n\t\t\tif (addressed == null) throw ex;\n\n\t\t\treturn [addressed];\n\t\t}\n\t}\n\n\t/**\n\t * The filtered work-item search: one WIQL query per page over ONE collection, then one detail read for the\n\t * page's slice. See {@link resolveWorkItemSearchScope} for why a search never spans several collections.\n\t */\n\tprotected override async searchProviderIssuesPage(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: {\n\t\t\trepos?: ProviderRepoInput[];\n\t\t\torg?: string;\n\t\t\tcriteria?: IssueSearchCriteria;\n\t\t\tcursor?: string;\n\t\t\tpageSize?: number;\n\t\t},\n\t\tcancellation?: AbortSignal,\n\t): Promise<ProviderIssueSearchPage | undefined> {\n\t\tconst azure = await this.authenticationService.apis.azure;\n\t\tif (azure == null) return undefined;\n\n\t\tthis.assertSearchDates(options.criteria);\n\t\t// A first page re-reads the collection's projects, so a project created since discovery is known before its\n\t\t// work items can turn up in a collection-wide query; a continuation keeps the set its first page resolved.\n\t\t// The facade's page-1 marker (`broadenIssues` retries a failed first page with it) is a first page too.\n\t\tconst scope = await this.resolveWorkItemSearchScope(\n\t\t\tsession,\n\t\t\toptions.repos,\n\t\t\toptions.org,\n\t\t\tisAzureWorkItemSearchFirstPage(options.cursor),\n\t\t);\n\t\treturn azure.searchWorkItemsPage(\n\t\t\tthis,\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tscope.collection,\n\t\t\t{\n\t\t\t\tbaseUrl: this.collectionApiBaseUrl(session, scope.collection),\n\t\t\t\tcriteria: options.criteria,\n\t\t\t\tprojectNames: scope.projectNames,\n\t\t\t\tresolveProject: name => scope.projects.find(p => sameAzureName(p.name, name)),\n\t\t\t\tcursor: options.cursor,\n\t\t\t\tpageSize: options.pageSize,\n\t\t\t},\n\t\t\tcancellation,\n\t\t);\n\t}\n\n\t/**\n\t * Counts each scope with the WIQL its search would page through, so a count never previews constraints the read\n\t * doesn't apply. A scope this provider refuses (see {@link resolveWorkItemSearchScope}) is answered as that\n\t * refusal, so the facade drops only it; a request failure still fails the batch, which the facade isolates from\n\t * the other batches.\n\t */\n\tprotected override async countProviderIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tscopes: readonly { repos?: ProviderRepoInput[]; org?: string; criteria?: IssueSearchCriteria }[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<ProviderSearchCount[] | undefined> {\n\t\tconst azure = await this.authenticationService.apis.azure;\n\t\tif (azure == null) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\treturn mapBounded(scopes, azureIssueCountConcurrency, s =>\n\t\t\trefusalAsCount(async () => {\n\t\t\t\tthis.assertSearchDates(s.criteria);\n\t\t\t\t// Re-reads the projects like a first search page does, so a count and the page it previews resolve\n\t\t\t\t// the same project set — a newer project can't be counted by one and refused or missed by the other.\n\t\t\t\tconst scope = await this.resolveWorkItemSearchScope(session, s.repos, s.org, true);\n\t\t\t\treturn azure.countWorkItems(\n\t\t\t\t\tthis,\n\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\tscope.collection,\n\t\t\t\t\t{\n\t\t\t\t\t\tbaseUrl: this.collectionApiBaseUrl(session, scope.collection),\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t\tprojectNames: scope.projectNames,\n\t\t\t\t\t},\n\t\t\t\t\tcancellation,\n\t\t\t\t);\n\t\t\t}),\n\t\t);\n\t}\n\n\t/**\n\t * The ONE collection a work-item search runs in, and the projects that bound it.\n\t *\n\t * A WIQL query is collection-scoped, and ordering a union of several would need every match's details rather\n\t * than one page's, so a search never spans collections. `org` names the collection; without it the repositories'\n\t * collection is used, and without either the only collection the account can see. Anything else — repositories\n\t * in two collections, or several visible collections and nothing to choose one — is refused rather than\n\t * narrowed to whichever came first.\n\t *\n\t * Repositories bound the query by their PROJECT, since Azure work items belong to projects rather than to\n\t * repositories, and every project must be one the collection actually has: WIQL answers an unknown project name\n\t * with an empty result, which would read as \"nothing matched\".\n\t */\n\tprivate async resolveWorkItemSearchScope(\n\t\tsession: ProviderAuthenticationSession,\n\t\trepos: ProviderRepoInput[] | undefined,\n\t\torg: string | undefined,\n\t\trefreshProjects: boolean = false,\n\t): Promise<{ collection: string; projects: AzureProjectDescriptor[]; projectNames?: string[] }> {\n\t\tconst repoCollections = uniqueAzureNames(repos?.map(r => r.namespace) ?? []);\n\t\tif (repoCollections.length > 1) {\n\t\t\tthrow this.searchUnavailable(\n\t\t\t\t'the repositories span several collections; search each collection separately',\n\t\t\t);\n\t\t}\n\t\tif (org && repoCollections.length === 1 && !sameAzureName(org, repoCollections[0])) {\n\t\t\tthrow this.searchUnavailable(`the repositories are not in the '${org}' collection`);\n\t\t}\n\n\t\tconst collections = await this.getSearchCollections(session);\n\t\tconst name = org || repoCollections[0] || (collections.length === 1 ? collections[0].name : undefined);\n\t\tif (name == null) {\n\t\t\tthrow this.searchUnavailable(\n\t\t\t\t`the account can see ${collections.length} collections; pass \\`org\\` to choose the one to search`,\n\t\t\t);\n\t\t}\n\n\t\tconst collection = findAzureCollection(collections, name);\n\t\tif (collection == null) throw this.searchUnavailable(`the '${name}' collection is not visible`);\n\n\t\tconst projects = await this.getSearchProjects(session, collection, refreshProjects);\n\t\tif (!repos?.length) return { collection: collection.name, projects: projects };\n\n\t\tconst projectNames = uniqueAzureNames(\n\t\t\trepos.map(r => {\n\t\t\t\tconst projectName = getAzureRepositoryIdentity({\n\t\t\t\t\towner: r.namespace,\n\t\t\t\t\tname: r.name,\n\t\t\t\t\tproject: r.project,\n\t\t\t\t}).projectName;\n\t\t\t\tif (projectName == null) {\n\t\t\t\t\tthrow this.searchUnavailable(`repository '${r.namespace}/${r.name}' names no project`);\n\t\t\t\t}\n\n\t\t\t\tconst project = projects.find(p => sameAzureName(p.name, projectName));\n\t\t\t\tif (project == null) {\n\t\t\t\t\tthrow this.searchUnavailable(`project '${projectName}' is not visible in '${collection.name}'`);\n\t\t\t\t}\n\n\t\t\t\treturn project.name;\n\t\t\t}),\n\t\t);\n\t\treturn { collection: collection.name, projects: projects, projectNames: projectNames };\n\t}\n\n\t/**\n\t * The filtered pull-request search. Azure's pull request query can only narrow by creator, reviewer, status and\n\t * repository, so every facet is drained (see {@link drainPullRequestSearch}) and the criteria it can't express\n\t * are applied to the complete facets; the union is then ordered and served from a keyset cursor.\n\t */\n\tprotected override async searchProviderPullRequestsPage(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: {\n\t\t\trepos?: ProviderRepoInput[];\n\t\t\torg?: string;\n\t\t\tcriteria?: PullRequestSearchCriteria;\n\t\t\tcursor?: string;\n\t\t\tpageSize?: number;\n\t\t\tsummary?: boolean;\n\t\t},\n\t\tcancellation?: AbortSignal,\n\t): Promise<ProviderPullRequestSearchPage | undefined> {\n\t\tthis.assertSearchDates(options.criteria);\n\t\tconst sort = options.criteria?.sort ?? defaultPullRequestSort;\n\t\tconst query = toPullRequestSearchQuery(options, sort);\n\t\tconst key = toAzurePullRequestSearchCursorKey(query);\n\t\tconst cursor = parseAzurePullRequestSearchCursor(options.cursor, key);\n\t\t// A first page drains fresh, into a drain of its own, and its continuations page through that one: the cache\n\t\t// keeps it alive for as long as the pagination keeps reading it (an access TTL), so rows can't move between\n\t\t// pages, and a later first page of the same query never replaces it. A continuation whose drain is gone is\n\t\t// refused, like an expired work-item snapshot: resuming against a re-drain could skip or repeat a pull request\n\t\t// whose sort date changed. The drain is keyed by credential and installation too, so a cursor handed to\n\t\t// another connection finds nothing and is refused the same way.\n\t\tconst drainId = cursor?.drain ?? uuid();\n\t\tconst drainKey = this.pullRequestSearchCacheKey(session, query, drainId);\n\t\t// A continuation only ever READS its drain: creating one under its id would be the re-drain refused above.\n\t\tconst drainFound =\n\t\t\tcursor != null\n\t\t\t\t? this._pullRequestSearches.get(drainKey)\n\t\t\t\t: this.getPullRequestSearch(this._pullRequestSearches, drainKey, session, options, sort, cancellation);\n\t\tif (drainFound == null) {\n\t\t\tthrow new Error(\n\t\t\t\t'Pull request search results expired or belong to another connection; restart the read without a cursor',\n\t\t\t);\n\t\t}\n\n\t\tconst drain = await drainFound;\n\t\tif (cursor == null) {\n\t\t\t// Also the query's latest drain, which a count of the same query reads rather than draining again. Stored\n\t\t\t// through `getOrCreate`, which applies the cache's capacity and expiry; `set` applies neither.\n\t\t\tconst latestKey = this.pullRequestSearchCacheKey(session, query);\n\t\t\tthis._latestPullRequestSearches.delete(latestKey);\n\t\t\tvoid this._latestPullRequestSearches.getOrCreate(latestKey, () => Promise.resolve(drain));\n\t\t}\n\n\t\tconst pageSize = toAzureSearchPageSize(options.pageSize, 50, 100);\n\t\tconst start =\n\t\t\tcursor == null\n\t\t\t\t? 0\n\t\t\t\t: drain.rows.findIndex(r => compareAzurePullRequestSearchPositions(r.position, cursor.after, sort) > 0);\n\t\tconst slice = start < 0 ? [] : drain.rows.slice(start, start + pageSize);\n\t\tconst page = cursor?.page ?? 1;\n\t\tconst last = slice.at(-1);\n\t\tconst hasMore = last != null && start + slice.length < drain.rows.length;\n\t\treturn {\n\t\t\tvalues: slice.map(r =>\n\t\t\t\tfromProviderPullRequest(r.pr, this, {\n\t\t\t\t\tproject: r.project,\n\t\t\t\t\tcurrentAccount: r.currentAccount,\n\t\t\t\t\tprojection: options.summary ? 'search-summary' : 'search',\n\t\t\t\t}),\n\t\t\t),\n\t\t\tcursor: hasMore\n\t\t\t\t? JSON.stringify({ key: key, drain: drainId, page: page + 1, after: last.position })\n\t\t\t\t: undefined,\n\t\t\thasMore: hasMore,\n\t\t\tpage: page,\n\t\t\ttruncated: drain.truncated,\n\t\t\ttotalCount: drain.truncated ? undefined : drain.rows.length,\n\t\t\tmetadata: drain.failures != null ? { completeness: 'partial', failures: drain.failures } : undefined,\n\t\t};\n\t}\n\n\t/**\n\t * Counts each scope from the latest drain of its query, the one a search's first page just read, or drains it; see\n\t * {@link searchProviderPullRequestsPage}.\n\t */\n\tprotected override async countProviderPullRequests(\n\t\tsession: ProviderAuthenticationSession,\n\t\tscopes: readonly { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria }[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<(ProviderPullRequestCount | Error)[] | undefined> {\n\t\t// One scope at a time: each is a drain that already fans out over its facets, and the facade runs several\n\t\t// batches of scopes at once, so a concurrent batch would multiply the requests reaching the server.\n\t\treturn mapBounded(scopes, 1, s =>\n\t\t\trefusalAsCount(async () => {\n\t\t\t\tthis.assertSearchDates(s.criteria);\n\t\t\t\tconst sort = s.criteria?.sort ?? defaultPullRequestSort;\n\t\t\t\tconst drain = await this.getPullRequestSearch(\n\t\t\t\t\tthis._latestPullRequestSearches,\n\t\t\t\t\tthis.pullRequestSearchCacheKey(session, toPullRequestSearchQuery(s, sort)),\n\t\t\t\t\tsession,\n\t\t\t\t\ts,\n\t\t\t\t\tsort,\n\t\t\t\t\tcancellation,\n\t\t\t\t);\n\t\t\t\t// Past the drain budget the true total is unknown, so it is reported as uncounted rather than understated.\n\t\t\t\treturn { count: drain.truncated ? undefined : drain.rows.length };\n\t\t\t}),\n\t\t);\n\t}\n\n\t/**\n\t * The drain each pagination pages through, one per first page (see {@link searchProviderPullRequestsPage}), so\n\t * the 50 most recently read paginations are kept for as long as they keep reading.\n\t */\n\tprivate readonly _pullRequestSearches = new PromiseCache<string, AzurePullRequestSearchDrain>({\n\t\tcapacity: 50,\n\t\taccessTTL: pullRequestSearchIdleTtl,\n\t\tcreateTTL: pullRequestSearchMaxTtl,\n\t});\n\t/**\n\t * Each query's latest drain, which its count reads for {@link pullRequestCountReuseTtl}. Apart from the\n\t * paginations' drains, so counting and starting searches never evicts a pagination still being read.\n\t */\n\tprivate readonly _latestPullRequestSearches = new PromiseCache<string, AzurePullRequestSearchDrain>({\n\t\tcapacity: 20,\n\t\tcreateTTL: pullRequestCountReuseTtl,\n\t});\n\n\t/**\n\t * Keyed by the query itself, not its cursor fingerprint, so two queries whose fingerprints collide can never share\n\t * a drain; by credential and installation, so two accounts or installations never read each other's results; and\n\t * by `drainId` for a pagination's own drain, left out for the query's latest one.\n\t */\n\tprivate pullRequestSearchCacheKey(\n\t\tsession: ProviderAuthenticationSession,\n\t\tquery: unknown,\n\t\tdrainId?: string,\n\t): string {\n\t\treturn JSON.stringify([session.accessToken, this.apiBaseUrlFor(session), query, drainId ?? null]);\n\t}\n\n\tprivate getPullRequestSearch(\n\t\tcache: PromiseCache<string, AzurePullRequestSearchDrain>,\n\t\tcacheKey: string,\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria },\n\t\tsort: PullRequestSorting,\n\t\tcancellation: AbortSignal | undefined,\n\t): Promise<AzurePullRequestSearchDrain> {\n\t\treturn cache.getOrCreate(\n\t\t\tcacheKey,\n\t\t\t(_cacheable, signal) => this.drainPullRequestSearch(session, options, sort, signal),\n\t\t\t{ cancellation: cancellation },\n\t\t);\n\t}\n\n\t/**\n\t * Every pull request matching the search, filtered, deduplicated and ordered.\n\t *\n\t * One facet per (repository or project) × relationship: `Author` reads by creator, while `Assignee` and\n\t * `ReviewRequested` both read by reviewer, since Azure has no assignee distinct from its reviewers: every open pull\n\t * request, kept when it names the user or one of the user's groups, and closed or merged ones through Azure's filter\n\t * for the user (see `toAzureReviewerReads`). Each facet is drained to {@link azurePullRequestSearchFacetLimit}; a\n\t * facet that still has more marks the whole result truncated, because its unread rows could sort anywhere in the\n\t * union. A facet that fails fails the search: a union missing one facet would be served in an order it doesn't\n\t * have. A collection whose groups can't be read is the exception: its facets still read what names the user, and\n\t * the result is reported incomplete with that collection's failure.\n\t */\n\tprivate async drainPullRequestSearch(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria },\n\t\tsort: PullRequestSorting,\n\t\tcancellation: AbortSignal | undefined,\n\t): Promise<AzurePullRequestSearchDrain> {\n\t\tconst criteria = options.criteria;\n\t\t// Built before any request, so criteria it can't apply fail the search before the facets are drained.\n\t\tconst matches = toAzurePullRequestSearchFilter(criteria);\n\t\tconst relationships = new Set(criteria?.relationships ?? []);\n\t\tconst wantAuthored = relationships.has(PullRequestFilter.Author);\n\t\tconst wantReviewed =\n\t\t\trelationships.has(PullRequestFilter.Assignee) || relationships.has(PullRequestFilter.ReviewRequested);\n\n\t\t// A drain runs for a first page (or once its predecessor expired), so it re-reads the projects it fans out\n\t\t// over: an org search trusting the connection-long discovery cache would silently skip a newer project.\n\t\tconst facets = await this.resolvePullRequestSearchFacets(session, options.repos, options.org);\n\n\t\t// The current user's id is resolved PER COLLECTION: Azure DevOps Server gives one person a different\n\t\t// identity id in each collection than at the server level, and a creator/reviewer filter only matches the\n\t\t// collection's. The server-level id matches nothing there and would serve an empty result as if nobody had\n\t\t// authored anything.\n\t\tconst userIds = new Map<string, string>();\n\t\tif (relationships.size > 0) {\n\t\t\tfor (const collection of uniqueAzureNames(facets.map(f => f.collection))) {\n\t\t\t\tconst userId = await this.getFilterUserId(session, collection);\n\t\t\t\tif (userId == null) {\n\t\t\t\t\tthrow this.searchUnavailable(`the current user could not be resolved in '${collection}'`);\n\t\t\t\t}\n\n\t\t\t\tuserIds.set(collection, userId);\n\t\t\t}\n\t\t}\n\n\t\t// Every group the user is in, per collection, which the reviewer facets keep pull requests for too (see\n\t\t// `toAzureReviewerReads`). A collection whose groups can't be read (e.g. a token without the Identity scope)\n\t\t// keeps what names the user and is reported as a failure of that collection, so the search is incomplete\n\t\t// rather than refused, and a credential its other reads accept isn't treated as a dead one.\n\t\tconst failures: CollectionScopeFailure[] = [];\n\t\tconst groupsByCollection = new Map(\n\t\t\twantReviewed\n\t\t\t\t? await Promise.all(\n\t\t\t\t\t\tArray.from(userIds, async ([collection, userId]) => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn [\n\t\t\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\t\t\tawait this.getReviewerGroupIds(session, collection, userId),\n\t\t\t\t\t\t\t\t] as const;\n\t\t\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\t\t\tconst resourceId = facets.find(f => f.collection === collection)!.project.resourceId;\n\t\t\t\t\t\t\t\tfailures.push(\n\t\t\t\t\t\t\t\t\ttoCollectionScopeFailure({ providerId: this.id, resourceId: resourceId }, ex),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn [collection, new Set<string>()] as const;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t: [],\n\t\t);\n\t\tconst groupIds = new Set([...groupsByCollection.values()].flatMap(ids => [...ids]));\n\n\t\tconst states = toProviderPullRequestStates(criteria?.states?.length ? criteria.states : 'open');\n\t\tconst filtersFor = (\n\t\t\tcollection: string,\n\t\t): { filter: AzureReviewerRead['filter'] & { authorLogin?: string }; keep?: AzureReviewerRead['keep'] }[] => {\n\t\t\tif (relationships.size === 0) return [{ filter: {} }];\n\n\t\t\tconst userId = userIds.get(collection)!;\n\t\t\treturn [\n\t\t\t\t...(wantAuthored ? [{ filter: { authorLogin: userId } }] : []),\n\t\t\t\t...(wantReviewed ? toAzureReviewerReads(userId, groupsByCollection.get(collection)!, states) : []),\n\t\t\t];\n\t\t};\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst { tokenWithInfo, options: apiOptions } = this.getApiOptions(session);\n\t\tconst drains = await mapBounded(\n\t\t\tfacets.flatMap(facet => filtersFor(facet.collection).map(f => ({ facet: facet, ...f }))),\n\t\t\tproviderFanOutConcurrency,\n\t\t\tasync ({ facet, filter, keep }) => {\n\t\t\t\tconst baseUrl = this.collectionApiBaseUrl(session, facet.collection);\n\t\t\t\tconst values: ProviderPullRequest[] = [];\n\t\t\t\tlet page = 1;\n\t\t\t\tfor (let read = 1; ; read++) {\n\t\t\t\t\tif (cancellation?.aborted) throw new CancellationError();\n\n\t\t\t\t\tconst input = {\n\t\t\t\t\t\t...apiOptions,\n\t\t\t\t\t\t// A reviewer facet narrows the states itself (see `toAzureReviewerReads`).\n\t\t\t\t\t\tstates: states,\n\t\t\t\t\t\t...filter,\n\t\t\t\t\t\tpage: page,\n\t\t\t\t\t\tpageSize: azurePullRequestSearchPageSize,\n\t\t\t\t\t\tbaseUrl: baseUrl,\n\t\t\t\t\t};\n\t\t\t\t\tconst result =\n\t\t\t\t\t\tfacet.repository != null\n\t\t\t\t\t\t\t? await api\n\t\t\t\t\t\t\t\t\t.getPullRequestsForRepo(\n\t\t\t\t\t\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tnamespace: facet.collection,\n\t\t\t\t\t\t\t\t\t\t\tname: facet.repository,\n\t\t\t\t\t\t\t\t\t\t\tproject: facet.project.name,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tinput,\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t.then(r => ({\n\t\t\t\t\t\t\t\t\t\tvalues: r.values,\n\t\t\t\t\t\t\t\t\t\tmore: r.paging?.more === true,\n\t\t\t\t\t\t\t\t\t\tnextPage: r.paging?.nextPage,\n\t\t\t\t\t\t\t\t\t}))\n\t\t\t\t\t\t\t: await api\n\t\t\t\t\t\t\t\t\t.getPullRequestsForAzureProject(\n\t\t\t\t\t\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\t\t\t\t\t\t{ namespace: facet.collection, project: facet.project.name },\n\t\t\t\t\t\t\t\t\t\tinput,\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t.then(r =>\n\t\t\t\t\t\t\t\t\t\tr != null\n\t\t\t\t\t\t\t\t\t\t\t? { values: r.data, more: r.hasMore, nextPage: r.nextPage ?? undefined }\n\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\tif (result == null) throw new Error('Azure DevOps returned no pull request page');\n\n\t\t\t\t\tvalues.push(...result.values);\n\t\t\t\t\tif (!result.more) return { facet: facet, keep: keep, values: values, truncated: false };\n\t\t\t\t\t// Follows the page the provider says comes next rather than assuming one: a continuation that\n\t\t\t\t\t// doesn't advance would re-read or skip rows, so it fails the facet — and with it the search —\n\t\t\t\t\t// instead of serving a union built from it.\n\t\t\t\t\tif (result.nextPage == null || result.nextPage <= page) {\n\t\t\t\t\t\tthrow new Error('Azure DevOps returned no advancing pull request continuation');\n\t\t\t\t\t}\n\t\t\t\t\t// The budget counts pages READ, not rows kept: several states are one `status=all` query the SDK\n\t\t\t\t\t// narrows client-side, so counting survivors would keep paging through rows it discards.\n\t\t\t\t\tif (read * azurePullRequestSearchPageSize >= azurePullRequestSearchFacetLimit) {\n\t\t\t\t\t\treturn { facet: facet, keep: keep, values: values, truncated: true };\n\t\t\t\t\t}\n\n\t\t\t\t\tpage = result.nextPage;\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\tconst rows = new Map<string, AzurePullRequestSearchRow>();\n\t\tfor (const { facet, keep, values } of drains) {\n\t\t\tfor (const pr of values) {\n\t\t\t\tif (keep != null && !keep(pr)) continue;\n\t\t\t\tif (!matches(pr)) continue;\n\n\t\t\t\tconst identity =\n\t\t\t\t\tgetProviderPullRequestIdentity(pr) ?? `project:${facet.collection}/${facet.project.name}:${pr.id}`;\n\t\t\t\tif (rows.has(identity)) continue;\n\n\t\t\t\tconst userId = userIds.get(facet.collection);\n\t\t\t\trows.set(identity, {\n\t\t\t\t\tpr: markMyGroupReviews(pr, groupIds),\n\t\t\t\t\tproject: facet.project,\n\t\t\t\t\tcurrentAccount: userId != null ? { id: userId } : undefined,\n\t\t\t\t\tposition: toAzurePullRequestSearchPosition(pr, sort, identity),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\trows: [...rows.values()].sort((a, b) =>\n\t\t\t\tcompareAzurePullRequestSearchPositions(a.position, b.position, sort),\n\t\t\t),\n\t\t\ttruncated: failures.length > 0 || drains.some(d => d.truncated),\n\t\t\tfailures: failures.length > 0 ? failures : undefined,\n\t\t};\n\t}\n\n\t/**\n\t * The repositories or projects a pull-request search reads, each with its collection and project descriptor.\n\t *\n\t * Repositories are read one by one; an `org` is its collection's projects; a search bounded only by the current\n\t * user reads every project the account can see, the same breadth as the account-wide pull request read.\n\t */\n\tprivate async resolvePullRequestSearchFacets(\n\t\tsession: ProviderAuthenticationSession,\n\t\trepos: ProviderRepoInput[] | undefined,\n\t\torg: string | undefined,\n\t): Promise<{ collection: string; project: AzureProjectDescriptor; repository?: string }[]> {\n\t\tconst collections = await this.getSearchCollections(session);\n\t\tconst collectionFor = (name: string): AzureOrganizationDescriptor => {\n\t\t\tconst collection = findAzureCollection(collections, name);\n\t\t\tif (collection == null) throw this.searchUnavailable(`the '${name}' collection is not visible`);\n\n\t\t\treturn collection;\n\t\t};\n\n\t\tif (repos?.length) {\n\t\t\tif (org && repos.some(r => !sameAzureName(r.namespace, org))) {\n\t\t\t\tthrow this.searchUnavailable(`the repositories are not all in the '${org}' collection`);\n\t\t\t}\n\n\t\t\t// One discovery per distinct collection, however many of its repositories the search names.\n\t\t\tconst projectsByCollection = new Map<string, Promise<AzureProjectDescriptor[]>>();\n\t\t\tconst projectsOf = (collection: AzureOrganizationDescriptor): Promise<AzureProjectDescriptor[]> => {\n\t\t\t\tlet projects = projectsByCollection.get(collection.id);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tprojects = this.getSearchProjects(session, collection, true);\n\t\t\t\t\tprojectsByCollection.set(collection.id, projects);\n\t\t\t\t}\n\t\t\t\treturn projects;\n\t\t\t};\n\n\t\t\treturn Promise.all(\n\t\t\t\trepos.map(async r => {\n\t\t\t\t\tconst collection = collectionFor(r.namespace);\n\t\t\t\t\tconst identity = getAzureRepositoryIdentity({\n\t\t\t\t\t\towner: r.namespace,\n\t\t\t\t\t\tname: r.name,\n\t\t\t\t\t\tproject: r.project,\n\t\t\t\t\t});\n\t\t\t\t\tif (identity.projectName == null) {\n\t\t\t\t\t\tthrow this.searchUnavailable(`repository '${r.namespace}/${r.name}' names no project`);\n\t\t\t\t\t}\n\t\t\t\t\t// The repository becomes a URL segment the SDK encodes but doesn't validate, and a segment of `..`\n\t\t\t\t\t// survives encoding and resolves to the project-wide route: refused rather than widened.\n\t\t\t\t\tif (!isAzureRouteSegment(identity.repositoryName)) {\n\t\t\t\t\t\tthrow this.searchUnavailable(`repository '${r.namespace}/${r.name}' is not a valid name`);\n\t\t\t\t\t}\n\n\t\t\t\t\tconst project = (await projectsOf(collection)).find(p =>\n\t\t\t\t\t\tsameAzureName(p.name, identity.projectName!),\n\t\t\t\t\t);\n\t\t\t\t\tif (project == null) {\n\t\t\t\t\t\tthrow this.searchUnavailable(\n\t\t\t\t\t\t\t`project '${identity.projectName}' is not visible in '${collection.name}'`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn { collection: collection.name, project: project, repository: identity.repositoryName };\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst scoped = org ? [collectionFor(org)] : collections;\n\t\tconst projects = await Promise.all(scoped.map(c => this.getSearchProjects(session, c, true)));\n\t\treturn projects.flat().map(p => ({ collection: p.resourceName, project: p }));\n\t}\n\n\tprivate readonly _collectionUserIds = new PromiseCache<string, string | undefined>({ capacity: 50 });\n\n\t/**\n\t * The current user's identity id in `collection`, read from the collection's own connection data, which is\n\t * what Azure's creator and reviewer filters compare against. Cached per credential and collection, since it\n\t * can't change for either.\n\t */\n\tprotected override getFilterUserId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tcollection: string,\n\t): Promise<string | undefined> {\n\t\tconst baseUrl = this.collectionApiBaseUrl(session, collection);\n\t\treturn this._collectionUserIds.getOrCreate(\n\t\t\tJSON.stringify([session.accessToken, baseUrl, collection.toLowerCase()]),\n\t\t\tasync cacheable => {\n\t\t\t\tconst azure = await this.authenticationService.apis.azure;\n\t\t\t\tconst user = await azure?.getCurrentUserOnServer(\n\t\t\t\t\tthis,\n\t\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\t\t`${baseUrl}/${encodeURIComponent(collection)}`,\n\t\t\t\t);\n\t\t\t\t// A miss is not cached: the lookup degrades to `undefined` on a transient failure, and caching that\n\t\t\t\t// would refuse every relationship search in the collection until the connection is dropped.\n\t\t\t\tif (user?.id == null) {\n\t\t\t\t\tcacheable.invalidate();\n\t\t\t\t}\n\n\t\t\t\treturn user?.id;\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Resolved per collection, which a raw row names as its repository owner: a collection's pull requests carry the\n\t * id {@link getFilterUserId} resolves there, never the server-level account's. A row that names no collection or\n\t * has no author to match gets no viewer, nor does one in a collection whose id can't be read: its authorship\n\t * stays unknown.\n\t */\n\toverride async getPullRequestViewers(\n\t\tpullRequests: readonly (ProviderPullRequest | PullRequest)[],\n\t\tconnectionId?: string,\n\t): Promise<({ id: string; username?: string } | undefined)[]> {\n\t\tconst session = await this.resolveReadSessionOrThrow(connectionId, undefined);\n\t\tif (session == null) return pullRequests.map(() => undefined);\n\n\t\tconst viewerFor = this.getOrganizationViewers(session, undefined);\n\t\treturn Promise.all(\n\t\t\tpullRequests.map(pr => {\n\t\t\t\t// Azure DevOps Server's rows are provider-apis' own; only GitHub's are native.\n\t\t\t\tconst collection = isNativePullRequest(pr) ? undefined : pr.repository?.owner?.login;\n\t\t\t\treturn collection && pr.author != null ? viewerFor(collection) : Promise.resolve(undefined);\n\t\t\t}),\n\t\t);\n\t}\n\n\t/**\n\t * Ignores `account`: it is the server-level one, which no collection's pull requests carry. A collection whose id\n\t * can't be read resolves `undefined`, since authorship is optional enrichment and one collection's failed lookup\n\t * must not fail the read or blank the others; it is asked once per read, as a miss isn't cached.\n\t */\n\tprotected override getOrganizationViewers(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_account: { id: string; username?: string } | undefined,\n\t): (collection: string) => Promise<{ id: string } | undefined> {\n\t\tconst viewers = new Map<string, Promise<{ id: string } | undefined>>();\n\t\treturn collection => {\n\t\t\tconst key = collection.toLowerCase();\n\t\t\tlet viewer = viewers.get(key);\n\t\t\tif (viewer == null) {\n\t\t\t\tviewer = this.getFilterUserId(session, collection).then(\n\t\t\t\t\tid => (id != null ? { id: id } : undefined),\n\t\t\t\t\t() => undefined,\n\t\t\t\t);\n\t\t\t\tviewers.set(key, viewer);\n\t\t\t}\n\t\t\treturn viewer;\n\t\t};\n\t}\n\n\t/** Every collection the account can see, or a refusal when discovery itself failed. */\n\tprivate async getSearchCollections(session: ProviderAuthenticationSession): Promise<AzureOrganizationDescriptor[]> {\n\t\tconst collections = await this.getProviderResourcesForUser(session);\n\t\tif (collections == null) throw this.searchUnavailable('the account’s collections could not be read');\n\n\t\treturn collections;\n\t}\n\n\t/**\n\t * A collection's projects, refused when discovery was incomplete: a search bounded by an incomplete project set\n\t * would silently omit the missing projects' matches. `refresh` re-reads them rather than trusting the discovery\n\t * cache, which lives for the whole connection and so doesn't know a project created since.\n\t */\n\tprivate async getSearchProjects(\n\t\tsession: ProviderAuthenticationSession,\n\t\tcollection: AzureOrganizationDescriptor,\n\t\trefresh: boolean = false,\n\t): Promise<AzureProjectDescriptor[]> {\n\t\tconst projects = refresh\n\t\t\t? await this.refreshSearchProjects(session, collection)\n\t\t\t: await this.getProviderProjectsForResources(session, [collection]);\n\t\tif (projects.metadata != null && projects.metadata.completeness !== 'complete') {\n\t\t\tthrow this.searchUnavailable(`the projects of the '${collection.name}' collection could not all be read`);\n\t\t}\n\n\t\treturn projects.values;\n\t}\n\n\tprivate readonly _projectRefreshes = new Map<\n\t\tstring,\n\t\tPromise<ProviderApiCollectionResult<AzureProjectDescriptor>>\n\t>();\n\n\t/**\n\t * Re-reads a collection's projects, sharing one read among the refreshes IN FLIGHT at once — a count batch or a\n\t * multi-repository drain refreshes the same collection once per scope, and each would otherwise re-read every\n\t * project page. Nothing outlives the read, so the next search still sees a project created after it.\n\t */\n\tprivate refreshSearchProjects(\n\t\tsession: ProviderAuthenticationSession,\n\t\tcollection: AzureOrganizationDescriptor,\n\t): Promise<ProviderApiCollectionResult<AzureProjectDescriptor>> {\n\t\tconst key = `${this.discoveryKey(session)}:${collection.id}`;\n\t\tlet pending = this._projectRefreshes.get(key);\n\t\tif (pending == null) {\n\t\t\tpending = this.getProviderProjectsForResources(session, [collection], true).finally(() => {\n\t\t\t\tthis._projectRefreshes.delete(key);\n\t\t\t});\n\t\t\tthis._projectRefreshes.set(key, pending);\n\t\t}\n\n\t\treturn pending;\n\t}\n\n\t/**\n\t * Refuses dates the search can't apply before anything is read. The facade only checks that dates are\n\t * supported, not their shape, and a refusal here stays with its own scope in a count batch.\n\t */\n\tprivate assertSearchDates(criteria: { updatedAfter?: string; createdAfter?: string } | undefined): void {\n\t\tconst invalid = getInvalidAzureSearchDate(criteria);\n\t\tif (invalid != null) throw this.searchUnavailable(invalid);\n\t}\n\n\tprivate searchUnavailable(reason: string): IntegrationReadUnavailableError {\n\t\treturn new IntegrationReadUnavailableError(this.name, `search refused: ${reason}.`);\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tsuper.providerOnDisconnect();\n\t\tthis._pullRequestSearches.clear();\n\t\tthis._latestPullRequestSearches.clear();\n\t\tthis._collectionUserIds.clear();\n\t}\n\n\tprotected override async _requestForCurrentUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst azure = await this.authenticationService.apis.azure;\n\t\tconst user = azure\n\t\t\t? await azure.getCurrentUserOnServer(this, toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session))\n\t\t\t: undefined;\n\t\treturn user\n\t\t\t? {\n\t\t\t\t\tprovider: this,\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tname: user.name ?? undefined,\n\t\t\t\t\temail: user.email ?? undefined,\n\t\t\t\t\tavatarUrl: user.avatarUrl ?? undefined,\n\t\t\t\t\tusername: user.username ?? undefined,\n\t\t\t\t}\n\t\t\t: undefined;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"azureDevOpsServer.js","names":[],"sources":["../../../../../plus/integrations/src/providers/azureDevOpsServer.ts"],"sourcesContent":["import type { CollectionScopeFailure } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { IssueSearchCriteria } from '@gitlens/git/models/issue.js';\nimport type { PullRequest, PullRequestSearchCriteria, PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport { CancellationError } from '@gitlens/utils/cancellation.js';\nimport { uuid } from '@gitlens/utils/crypto.js';\nimport type { Emitter } from '@gitlens/utils/event.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession, TokenWithInfo } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { GitSelfManagedHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { IntegrationReadUnavailableError, RequestNotFoundError } from '../errors.js';\nimport type { IntegrationConnectionChangeEvent } from '../integrationService.js';\nimport type {\n\tIntegrationKey,\n\tProviderIssueSearchPage,\n\tProviderPullRequestCount,\n\tProviderPullRequestSearchPage,\n\tProviderSearchCount,\n} from '../models/integration.js';\nimport type { AzureOrganizationDescriptor, AzureProjectDescriptor } from './azure/models.js';\nimport type { AzurePullRequestSearchPosition } from './azure/search.js';\nimport {\n\tcompareAzurePullRequestSearchPositions,\n\tgetInvalidAzureSearchDate,\n\tisAzureWorkItemSearchFirstPage,\n\tparseAzurePullRequestSearchCursor,\n\ttoAzurePullRequestSearchCursorKey,\n\ttoAzurePullRequestSearchFilter,\n\ttoAzurePullRequestSearchPosition,\n\ttoAzureSearchPageSize,\n} from './azure/search.js';\nimport type { AzureReviewerRead } from './azureDevOps.js';\nimport {\n\tAzureDevOpsIntegrationBase,\n\tgetAzureRepositoryApiBaseUrl,\n\tgetAzureRepositoryIdentity,\n\tmarkMyGroupReviews,\n\tsameAzureName,\n\ttoAzureReviewerReads,\n\tuniqueAzureNames,\n} from './azureDevOps.js';\nimport type {\n\tProviderApiCollectionResult,\n\tProviderAzureResource,\n\tProviderPullRequest,\n\tProviderRepoInput,\n\tProviderReposInput,\n} from './models.js';\nimport {\n\tfromProviderPullRequest,\n\tgetProviderPullRequestIdentity,\n\tisNativePullRequest,\n\tprovidersMetadata,\n\tPullRequestFilter,\n\ttoProviderPullRequestStates,\n} from './models.js';\nimport type { ProvidersApi } from './providersApi.js';\n\n/**\n * Whether `value` can stand as one route segment. Encoding keeps `.` and `..` as they are, and a URL resolves them\n * as directory steps, so a name of either would address a different route than the one named.\n */\nfunction isAzureRouteSegment(value: string): boolean {\n\treturn value.length > 0 && value !== '.' && value !== '..';\n}\n\n/**\n * Runs one scope of a count probe, answering a refusal of THAT scope (see `searchUnavailable`) as the error itself,\n * which the facade reports and drops without failing the scopes around it. Anything else — a rejected credential,\n * a failed request — still rejects, so the batch reports it.\n */\nasync function refusalAsCount<T>(count: () => Promise<T>): Promise<T | IntegrationReadUnavailableError> {\n\ttry {\n\t\treturn await count();\n\t} catch (ex) {\n\t\tif (ex instanceof IntegrationReadUnavailableError) return ex;\n\n\t\tthrow ex;\n\t}\n}\n\nfunction findAzureCollection(\n\tcollections: readonly AzureOrganizationDescriptor[],\n\tname: string,\n): AzureOrganizationDescriptor | undefined {\n\treturn collections.find(c => sameAzureName(c.name, name) || c.id === name);\n}\n\n/**\n * The most pull requests one facet of a filtered pull-request search drains. Azure has no server-side text, date or\n * draft filter and no ordering, so a facet is read in full before any of them apply; past this bound the facet's\n * unread rows could sort anywhere, and the search reports itself truncated instead of guessing.\n */\nconst azurePullRequestSearchFacetLimit = 1000;\n\n/**\n * The page size a facet drain reads. The SDK's own default for both pull request reads, and the size it sizes\n * `$skip` by, so the budget above counts what was actually read.\n */\nconst azurePullRequestSearchPageSize = 100;\n\n/**\n * How many scopes of one work-item count batch query at once. The facade already runs several batches concurrently,\n * so this multiplies with it: kept small, so a large count preview can't flood a self-hosted server.\n */\nconst azureIssueCountConcurrency = 2;\n\n/**\n * How long a drained pull-request search keeps serving its continuation pages after the last one was read, and the\n * most it lives however actively it is paged. A pagination that keeps reading stays on one consistent drain; one\n * left idle, or paged for longer than that, is read again.\n */\nconst pullRequestSearchIdleTtl = 5 * 60 * 1000;\nconst pullRequestSearchMaxTtl = 30 * 60 * 1000;\n\n/**\n * How long a count reuses the drain a search of its query just read. Long enough for a count shown next to the\n * search it previews to agree with it, short enough that a count polled next to an unapplied filter stays live: the\n * window runs from the drain, not from the last count, so polling never extends it.\n */\nconst pullRequestCountReuseTtl = 60 * 1000;\n\n/**\n * Kept unconverted, so each page converts its rows with the projection its own caller asked for: the drain is shared\n * by every page of a query, whatever `summary` each page passes.\n */\ninterface AzurePullRequestSearchRow {\n\tpr: ProviderPullRequest;\n\tproject: AzureProjectDescriptor;\n\tcurrentAccount: { id: string } | undefined;\n\tposition: AzurePullRequestSearchPosition;\n}\n\ninterface AzurePullRequestSearchDrain {\n\trows: AzurePullRequestSearchRow[];\n\ttruncated: boolean;\n\t/** Collections whose groups couldn't be read, so what only those groups review is missing. */\n\tfailures?: CollectionScopeFailure[];\n}\n\n/**\n * Everything that decides a pull-request search's result set and order, as the value its cursor is fingerprinted\n * from. `pageSize` is left out on purpose: it only decides where a page ends, which the keyset cursor already\n * carries.\n */\nfunction toPullRequestSearchQuery(\n\toptions: { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria },\n\tsort: PullRequestSorting,\n): unknown {\n\tconst criteria = options.criteria;\n\treturn {\n\t\trepos: options.repos?.map(r => [r.namespace, r.project ?? null, r.name]),\n\t\torg: options.org ?? null,\n\t\trelationships: [...new Set(criteria?.relationships ?? [])].sort(),\n\t\tstates: [...new Set(criteria?.states ?? [])].sort(),\n\t\ttext: criteria?.text?.trim() ?? null,\n\t\tdraft: criteria?.draft ?? null,\n\t\tupdatedAfter: criteria?.updatedAfter ?? null,\n\t\tcreatedAfter: criteria?.createdAfter ?? null,\n\t\tsort: sort,\n\t};\n}\n\nconst serverMetadata = providersMetadata[GitSelfManagedHostIntegrationId.AzureDevOpsServer];\nconst serverAuthProvider = Object.freeze({ id: serverMetadata.id, scopes: serverMetadata.scopes });\n\nexport class AzureDevOpsServerIntegration extends AzureDevOpsIntegrationBase<GitSelfManagedHostIntegrationId.AzureDevOpsServer> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = serverAuthProvider;\n\treadonly id = GitSelfManagedHostIntegrationId.AzureDevOpsServer;\n\tprotected readonly key: IntegrationKey<GitSelfManagedHostIntegrationId.AzureDevOpsServer>;\n\treadonly name: string = 'Azure DevOps Server';\n\n\tconstructor(\n\t\tctx: IntegrationServiceContext,\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tgetProvidersApi: () => Promise<ProvidersApi>,\n\t\tdidChangeConnection: Emitter<IntegrationConnectionChangeEvent>,\n\t\treadonly domain: string,\n\t) {\n\t\tsuper(ctx, authenticationService, getProvidersApi, didChangeConnection);\n\t\tthis.key = `${this.id}:${this.domain}`;\n\t}\n\n\tprotected override apiBaseUrlFor(session: ProviderAuthenticationSession): string {\n\t\treturn this.getSelfManagedApiBaseUrl(session);\n\t}\n\n\t/**\n\t * The credential AND the installation it is used against: one integration instance serves every installation on\n\t * its host (context paths differ, the host doesn't), and the same token connected to two of them must not reuse\n\t * one installation's collections or projects on the other.\n\t */\n\tprotected override discoveryKey(session: ProviderAuthenticationSession): string {\n\t\treturn `${session.accessToken}:${this.apiBaseUrlFor(session)}`;\n\t}\n\n\tprotected override getRepositoriesApiBaseUrl(\n\t\tsession: ProviderAuthenticationSession,\n\t\trepos: ProviderReposInput,\n\t): string {\n\t\t// Requests append each repository's organization (its collection), which an address naming that collection\n\t\t// already ends with. Only repositories of a single organization can share one base; mixed ones keep the\n\t\t// address, so another collection's repository stays below it (see `resolveRepository`).\n\t\tconst owners = new Set(repos.map(r => (typeof r === 'object' ? r.namespace : undefined)));\n\t\tconst [owner] = owners;\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\t\treturn owners.size === 1 && owner != null\n\t\t\t? getAzureRepositoryApiBaseUrl(baseUrl, { owner: owner, virtualDirectory: undefined })\n\t\t\t: baseUrl;\n\t}\n\n\tprotected override getApiOptions(\n\t\tsession: ProviderAuthenticationSession,\n\t\tdoNotConvertToPat: boolean = false,\n\t): {\n\t\ttokenWithInfo: TokenWithInfo<GitSelfManagedHostIntegrationId.AzureDevOpsServer>;\n\t\toptions: { isPAT: boolean; baseUrl?: string };\n\t} {\n\t\tconst { options, ...rest } = super.getApiOptions(session, doNotConvertToPat);\n\t\treturn {\n\t\t\t...rest,\n\t\t\toptions: { ...options, baseUrl: this.apiBaseUrlFor(session) },\n\t\t};\n\t}\n\n\tprotected override getCollectionApiOptions(\n\t\tsession: ProviderAuthenticationSession,\n\t\tcollection: string,\n\t): { isPAT: boolean; baseUrl?: string } {\n\t\treturn {\n\t\t\t...super.getCollectionApiOptions(session, collection),\n\t\t\tbaseUrl: this.collectionApiBaseUrl(session, collection),\n\t\t};\n\t}\n\n\t/** The base a request below `collection` appends to: the installation, whichever the address already ends in. */\n\tprotected override collectionApiBaseUrl(session: ProviderAuthenticationSession, collection: string): string {\n\t\treturn getAzureRepositoryApiBaseUrl(this.apiBaseUrlFor(session), {\n\t\t\towner: collection,\n\t\t\tvirtualDirectory: undefined,\n\t\t});\n\t}\n\n\t/**\n\t * Only the server level lists the collections: below one, Azure DevOps Server answers `projectCollections` with a\n\t * 404 page. On that 404 an address that names a collection reads the collection's own connection data, whose\n\t * `webApplicationRelativeDirectory` spells it, and reports it as the one collection visible there.\n\t */\n\tprotected override async requestResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\tuserId: string,\n\t): Promise<ProviderAzureResource[] | undefined> {\n\t\ttry {\n\t\t\treturn await super.requestResourcesForUser(session, userId);\n\t\t} catch (ex) {\n\t\t\tif (!(ex instanceof RequestNotFoundError)) throw ex;\n\n\t\t\tconst azure = await this.authenticationService.apis.azure;\n\t\t\tconst addressed = await azure?.getAddressedCollection(\n\t\t\t\tthis,\n\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\tthis.apiBaseUrlFor(session),\n\t\t\t);\n\t\t\tif (addressed == null) throw ex;\n\n\t\t\treturn [addressed];\n\t\t}\n\t}\n\n\t/**\n\t * The filtered work-item search: one WIQL query per page over ONE collection, then one detail read for the\n\t * page's slice. See {@link resolveWorkItemSearchScope} for why a search never spans several collections.\n\t */\n\tprotected override async searchProviderIssuesPage(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: {\n\t\t\trepos?: ProviderRepoInput[];\n\t\t\torg?: string;\n\t\t\tcriteria?: IssueSearchCriteria;\n\t\t\tcursor?: string;\n\t\t\tpageSize?: number;\n\t\t},\n\t\tcancellation?: AbortSignal,\n\t): Promise<ProviderIssueSearchPage | undefined> {\n\t\tconst azure = await this.authenticationService.apis.azure;\n\t\tif (azure == null) return undefined;\n\n\t\tthis.assertSearchDates(options.criteria);\n\t\t// A first page re-reads the collection's projects, so a project created since discovery is known before its\n\t\t// work items can turn up in a collection-wide query; a continuation keeps the set its first page resolved.\n\t\t// The facade's page-1 marker (`broadenIssues` retries a failed first page with it) is a first page too.\n\t\tconst scope = await this.resolveWorkItemSearchScope(\n\t\t\tsession,\n\t\t\toptions.repos,\n\t\t\toptions.org,\n\t\t\tisAzureWorkItemSearchFirstPage(options.cursor),\n\t\t);\n\t\treturn azure.searchWorkItemsPage(\n\t\t\tthis,\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tscope.collection,\n\t\t\t{\n\t\t\t\tbaseUrl: this.collectionApiBaseUrl(session, scope.collection),\n\t\t\t\tcriteria: options.criteria,\n\t\t\t\tprojectNames: scope.projectNames,\n\t\t\t\tresolveProject: name => scope.projects.find(p => sameAzureName(p.name, name)),\n\t\t\t\tcursor: options.cursor,\n\t\t\t\tpageSize: options.pageSize,\n\t\t\t},\n\t\t\tcancellation,\n\t\t);\n\t}\n\n\t/**\n\t * Counts each scope with the WIQL its search would page through, so a count never previews constraints the read\n\t * doesn't apply. A scope this provider refuses (see {@link resolveWorkItemSearchScope}) is answered as that\n\t * refusal, so the facade drops only it; a request failure still fails the batch, which the facade isolates from\n\t * the other batches.\n\t */\n\tprotected override async countProviderIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tscopes: readonly { repos?: ProviderRepoInput[]; org?: string; criteria?: IssueSearchCriteria }[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<ProviderSearchCount[] | undefined> {\n\t\tconst azure = await this.authenticationService.apis.azure;\n\t\tif (azure == null) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\treturn mapBounded(scopes, azureIssueCountConcurrency, s =>\n\t\t\trefusalAsCount(async () => {\n\t\t\t\tthis.assertSearchDates(s.criteria);\n\t\t\t\t// Re-reads the projects like a first search page does, so a count and the page it previews resolve\n\t\t\t\t// the same project set — a newer project can't be counted by one and refused or missed by the other.\n\t\t\t\tconst scope = await this.resolveWorkItemSearchScope(session, s.repos, s.org, true);\n\t\t\t\treturn azure.countWorkItems(\n\t\t\t\t\tthis,\n\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\tscope.collection,\n\t\t\t\t\t{\n\t\t\t\t\t\tbaseUrl: this.collectionApiBaseUrl(session, scope.collection),\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t\tprojectNames: scope.projectNames,\n\t\t\t\t\t},\n\t\t\t\t\tcancellation,\n\t\t\t\t);\n\t\t\t}),\n\t\t);\n\t}\n\n\t/**\n\t * The ONE collection a work-item search runs in, and the projects that bound it.\n\t *\n\t * A WIQL query is collection-scoped, and ordering a union of several would need every match's details rather\n\t * than one page's, so a search never spans collections. `org` names the collection; without it the repositories'\n\t * collection is used, and without either the only collection the account can see. Anything else — repositories\n\t * in two collections, or several visible collections and nothing to choose one — is refused rather than\n\t * narrowed to whichever came first.\n\t *\n\t * Repositories bound the query by their PROJECT, since Azure work items belong to projects rather than to\n\t * repositories, and every project must be one the collection actually has: WIQL answers an unknown project name\n\t * with an empty result, which would read as \"nothing matched\".\n\t */\n\tprivate async resolveWorkItemSearchScope(\n\t\tsession: ProviderAuthenticationSession,\n\t\trepos: ProviderRepoInput[] | undefined,\n\t\torg: string | undefined,\n\t\trefreshProjects: boolean = false,\n\t): Promise<{ collection: string; projects: AzureProjectDescriptor[]; projectNames?: string[] }> {\n\t\tconst repoCollections = uniqueAzureNames(repos?.map(r => r.namespace) ?? []);\n\t\tif (repoCollections.length > 1) {\n\t\t\tthrow this.searchUnavailable(\n\t\t\t\t'the repositories span several collections; search each collection separately',\n\t\t\t);\n\t\t}\n\t\tif (org && repoCollections.length === 1 && !sameAzureName(org, repoCollections[0])) {\n\t\t\tthrow this.searchUnavailable(`the repositories are not in the '${org}' collection`);\n\t\t}\n\n\t\tconst collections = await this.getSearchCollections(session);\n\t\tconst name = org || repoCollections[0] || (collections.length === 1 ? collections[0].name : undefined);\n\t\tif (name == null) {\n\t\t\tthrow this.searchUnavailable(\n\t\t\t\t`the account can see ${collections.length} collections; pass \\`org\\` to choose the one to search`,\n\t\t\t);\n\t\t}\n\n\t\tconst collection = findAzureCollection(collections, name);\n\t\tif (collection == null) throw this.searchUnavailable(`the '${name}' collection is not visible`);\n\n\t\tconst projects = await this.getSearchProjects(session, collection, refreshProjects);\n\t\tif (!repos?.length) return { collection: collection.name, projects: projects };\n\n\t\tconst projectNames = uniqueAzureNames(\n\t\t\trepos.map(r => {\n\t\t\t\tconst projectName = getAzureRepositoryIdentity({\n\t\t\t\t\towner: r.namespace,\n\t\t\t\t\tname: r.name,\n\t\t\t\t\tproject: r.project,\n\t\t\t\t}).projectName;\n\t\t\t\tif (projectName == null) {\n\t\t\t\t\tthrow this.searchUnavailable(`repository '${r.namespace}/${r.name}' names no project`);\n\t\t\t\t}\n\n\t\t\t\tconst project = projects.find(p => sameAzureName(p.name, projectName));\n\t\t\t\tif (project == null) {\n\t\t\t\t\tthrow this.searchUnavailable(`project '${projectName}' is not visible in '${collection.name}'`);\n\t\t\t\t}\n\n\t\t\t\treturn project.name;\n\t\t\t}),\n\t\t);\n\t\treturn { collection: collection.name, projects: projects, projectNames: projectNames };\n\t}\n\n\t/**\n\t * The filtered pull-request search. Azure's pull request query can only narrow by creator, reviewer, status and\n\t * repository, so every facet is drained (see {@link drainPullRequestSearch}) and the criteria it can't express\n\t * are applied to the complete facets; the union is then ordered and served from a keyset cursor.\n\t */\n\tprotected override async searchProviderPullRequestsPage(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: {\n\t\t\trepos?: ProviderRepoInput[];\n\t\t\torg?: string;\n\t\t\tcriteria?: PullRequestSearchCriteria;\n\t\t\tcursor?: string;\n\t\t\tpageSize?: number;\n\t\t\tsummary?: boolean;\n\t\t},\n\t\tcancellation?: AbortSignal,\n\t): Promise<ProviderPullRequestSearchPage | undefined> {\n\t\tthis.assertSearchDates(options.criteria);\n\t\tconst sort = options.criteria?.sort ?? defaultPullRequestSort;\n\t\tconst query = toPullRequestSearchQuery(options, sort);\n\t\tconst key = toAzurePullRequestSearchCursorKey(query);\n\t\tconst cursor = parseAzurePullRequestSearchCursor(options.cursor, key);\n\t\t// A first page drains fresh, into a drain of its own, and its continuations page through that one: the cache\n\t\t// keeps it alive for as long as the pagination keeps reading it (an access TTL), so rows can't move between\n\t\t// pages, and a later first page of the same query never replaces it. A continuation whose drain is gone is\n\t\t// refused, like an expired work-item snapshot: resuming against a re-drain could skip or repeat a pull request\n\t\t// whose sort date changed. The drain is keyed by credential and installation too, so a cursor handed to\n\t\t// another connection finds nothing and is refused the same way.\n\t\tconst drainId = cursor?.drain ?? uuid();\n\t\tconst drainKey = this.pullRequestSearchCacheKey(session, query, drainId);\n\t\t// A continuation only ever READS its drain: creating one under its id would be the re-drain refused above.\n\t\tconst drainFound =\n\t\t\tcursor != null\n\t\t\t\t? this._pullRequestSearches.get(drainKey)\n\t\t\t\t: this.getPullRequestSearch(this._pullRequestSearches, drainKey, session, options, sort, cancellation);\n\t\tif (drainFound == null) {\n\t\t\tthrow new Error(\n\t\t\t\t'Pull request search results expired or belong to another connection; restart the read without a cursor',\n\t\t\t);\n\t\t}\n\n\t\tconst drain = await drainFound;\n\t\tif (cursor == null) {\n\t\t\t// Also the query's latest drain, which a count of the same query reads rather than draining again. Stored\n\t\t\t// through `getOrCreate`, which applies the cache's capacity and expiry; `set` applies neither.\n\t\t\tconst latestKey = this.pullRequestSearchCacheKey(session, query);\n\t\t\tthis._latestPullRequestSearches.delete(latestKey);\n\t\t\tvoid this._latestPullRequestSearches.getOrCreate(latestKey, () => Promise.resolve(drain));\n\t\t}\n\n\t\tconst pageSize = toAzureSearchPageSize(options.pageSize, 50, 100);\n\t\tconst start =\n\t\t\tcursor == null\n\t\t\t\t? 0\n\t\t\t\t: drain.rows.findIndex(r => compareAzurePullRequestSearchPositions(r.position, cursor.after, sort) > 0);\n\t\tconst slice = start < 0 ? [] : drain.rows.slice(start, start + pageSize);\n\t\tconst page = cursor?.page ?? 1;\n\t\tconst last = slice.at(-1);\n\t\tconst hasMore = last != null && start + slice.length < drain.rows.length;\n\t\treturn {\n\t\t\tvalues: slice.map(r =>\n\t\t\t\tfromProviderPullRequest(r.pr, this, {\n\t\t\t\t\tproject: r.project,\n\t\t\t\t\tcurrentAccount: r.currentAccount,\n\t\t\t\t\tprojection: options.summary ? 'search-summary' : 'search',\n\t\t\t\t}),\n\t\t\t),\n\t\t\tcursor: hasMore\n\t\t\t\t? JSON.stringify({ key: key, drain: drainId, page: page + 1, after: last.position })\n\t\t\t\t: undefined,\n\t\t\thasMore: hasMore,\n\t\t\tpage: page,\n\t\t\ttruncated: drain.truncated,\n\t\t\ttotalCount: drain.truncated ? undefined : drain.rows.length,\n\t\t\tmetadata: drain.failures != null ? { completeness: 'partial', failures: drain.failures } : undefined,\n\t\t};\n\t}\n\n\t/**\n\t * Counts each scope from the latest drain of its query, the one a search's first page just read, or drains it; see\n\t * {@link searchProviderPullRequestsPage}.\n\t */\n\tprotected override async countProviderPullRequests(\n\t\tsession: ProviderAuthenticationSession,\n\t\tscopes: readonly { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria }[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<(ProviderPullRequestCount | Error)[] | undefined> {\n\t\t// One scope at a time: each is a drain that already fans out over its facets, and the facade runs several\n\t\t// batches of scopes at once, so a concurrent batch would multiply the requests reaching the server.\n\t\treturn mapBounded(scopes, 1, s =>\n\t\t\trefusalAsCount(async () => {\n\t\t\t\tthis.assertSearchDates(s.criteria);\n\t\t\t\tconst sort = s.criteria?.sort ?? defaultPullRequestSort;\n\t\t\t\tconst drain = await this.getPullRequestSearch(\n\t\t\t\t\tthis._latestPullRequestSearches,\n\t\t\t\t\tthis.pullRequestSearchCacheKey(session, toPullRequestSearchQuery(s, sort)),\n\t\t\t\t\tsession,\n\t\t\t\t\ts,\n\t\t\t\t\tsort,\n\t\t\t\t\tcancellation,\n\t\t\t\t);\n\t\t\t\t// Past the drain budget the true total is unknown, so it is reported as uncounted rather than understated.\n\t\t\t\treturn { count: drain.truncated ? undefined : drain.rows.length };\n\t\t\t}),\n\t\t);\n\t}\n\n\t/**\n\t * The drain each pagination pages through, one per first page (see {@link searchProviderPullRequestsPage}), so\n\t * the 50 most recently read paginations are kept for as long as they keep reading.\n\t */\n\tprivate readonly _pullRequestSearches = new PromiseCache<string, AzurePullRequestSearchDrain>({\n\t\tcapacity: 50,\n\t\taccessTTL: pullRequestSearchIdleTtl,\n\t\tcreateTTL: pullRequestSearchMaxTtl,\n\t});\n\t/**\n\t * Each query's latest drain, which its count reads for {@link pullRequestCountReuseTtl}. Apart from the\n\t * paginations' drains, so counting and starting searches never evicts a pagination still being read.\n\t */\n\tprivate readonly _latestPullRequestSearches = new PromiseCache<string, AzurePullRequestSearchDrain>({\n\t\tcapacity: 20,\n\t\tcreateTTL: pullRequestCountReuseTtl,\n\t});\n\n\t/**\n\t * Keyed by the query itself, not its cursor fingerprint, so two queries whose fingerprints collide can never share\n\t * a drain; by credential and installation, so two accounts or installations never read each other's results; and\n\t * by `drainId` for a pagination's own drain, left out for the query's latest one.\n\t */\n\tprivate pullRequestSearchCacheKey(\n\t\tsession: ProviderAuthenticationSession,\n\t\tquery: unknown,\n\t\tdrainId?: string,\n\t): string {\n\t\treturn JSON.stringify([session.accessToken, this.apiBaseUrlFor(session), query, drainId ?? null]);\n\t}\n\n\tprivate getPullRequestSearch(\n\t\tcache: PromiseCache<string, AzurePullRequestSearchDrain>,\n\t\tcacheKey: string,\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria },\n\t\tsort: PullRequestSorting,\n\t\tcancellation: AbortSignal | undefined,\n\t): Promise<AzurePullRequestSearchDrain> {\n\t\treturn cache.getOrCreate(\n\t\t\tcacheKey,\n\t\t\t(_cacheable, signal) => this.drainPullRequestSearch(session, options, sort, signal),\n\t\t\t{ cancellation: cancellation },\n\t\t);\n\t}\n\n\t/**\n\t * Every pull request matching the search, filtered, deduplicated and ordered.\n\t *\n\t * One facet per (repository or project) × relationship: `Author` reads by creator, while `Assignee` and\n\t * `ReviewRequested` both read by reviewer, since Azure has no assignee distinct from its reviewers: every open pull\n\t * request, kept when it names the user or one of the user's groups, and closed or merged ones through Azure's filter\n\t * for the user (see `toAzureReviewerReads`). Each facet is drained to {@link azurePullRequestSearchFacetLimit}; a\n\t * facet that still has more marks the whole result truncated, because its unread rows could sort anywhere in the\n\t * union. A facet that fails fails the search: a union missing one facet would be served in an order it doesn't\n\t * have. A collection whose groups can't be read is the exception: its facets still read what names the user, and\n\t * the result is reported incomplete with that collection's failure.\n\t */\n\tprivate async drainPullRequestSearch(\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions: { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria },\n\t\tsort: PullRequestSorting,\n\t\tcancellation: AbortSignal | undefined,\n\t): Promise<AzurePullRequestSearchDrain> {\n\t\tconst criteria = options.criteria;\n\t\t// Built before any request, so criteria it can't apply fail the search before the facets are drained.\n\t\tconst matches = toAzurePullRequestSearchFilter(criteria);\n\t\tconst relationships = new Set(criteria?.relationships ?? []);\n\t\tconst wantAuthored = relationships.has(PullRequestFilter.Author);\n\t\tconst wantReviewed =\n\t\t\trelationships.has(PullRequestFilter.Assignee) || relationships.has(PullRequestFilter.ReviewRequested);\n\n\t\t// A drain runs for a first page (or once its predecessor expired), so it re-reads the projects it fans out\n\t\t// over: an org search trusting the connection-long discovery cache would silently skip a newer project.\n\t\tconst facets = await this.resolvePullRequestSearchFacets(session, options.repos, options.org);\n\n\t\t// The current user's id is resolved PER COLLECTION: Azure DevOps Server gives one person a different\n\t\t// identity id in each collection than at the server level, and a creator/reviewer filter only matches the\n\t\t// collection's. The server-level id matches nothing there and would serve an empty result as if nobody had\n\t\t// authored anything.\n\t\tconst userIds = new Map<string, string>();\n\t\tif (relationships.size > 0) {\n\t\t\tfor (const collection of uniqueAzureNames(facets.map(f => f.collection))) {\n\t\t\t\tconst userId = await this.getFilterUserId(session, collection);\n\t\t\t\tif (userId == null) {\n\t\t\t\t\tthrow this.searchUnavailable(`the current user could not be resolved in '${collection}'`);\n\t\t\t\t}\n\n\t\t\t\tuserIds.set(collection, userId);\n\t\t\t}\n\t\t}\n\n\t\t// Every group the user is in, per collection, which the reviewer facets keep pull requests for too (see\n\t\t// `toAzureReviewerReads`). A collection whose groups can't be read (e.g. a token without the Identity scope)\n\t\t// keeps what names the user and is reported as a failure of that collection, so the search is incomplete\n\t\t// rather than refused, and a credential its other reads accept isn't treated as a dead one.\n\t\tconst failures: CollectionScopeFailure[] = [];\n\t\tconst groupsByCollection = new Map(\n\t\t\twantReviewed\n\t\t\t\t? await Promise.all(\n\t\t\t\t\t\tArray.from(userIds, async ([collection, userId]) => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn [\n\t\t\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\t\t\tawait this.getReviewerGroupIds(session, collection, userId),\n\t\t\t\t\t\t\t\t] as const;\n\t\t\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\t\t\tconst resourceId = facets.find(f => f.collection === collection)!.project.resourceId;\n\t\t\t\t\t\t\t\tfailures.push(\n\t\t\t\t\t\t\t\t\ttoCollectionScopeFailure({ providerId: this.id, resourceId: resourceId }, ex),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn [collection, new Set<string>()] as const;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t: [],\n\t\t);\n\t\tconst groupIds = new Set([...groupsByCollection.values()].flatMap(ids => [...ids]));\n\n\t\tconst states = toProviderPullRequestStates(criteria?.states?.length ? criteria.states : 'open');\n\t\tconst filtersFor = (\n\t\t\tcollection: string,\n\t\t): { filter: AzureReviewerRead['filter'] & { authorLogin?: string }; keep?: AzureReviewerRead['keep'] }[] => {\n\t\t\tif (relationships.size === 0) return [{ filter: {} }];\n\n\t\t\tconst userId = userIds.get(collection)!;\n\t\t\treturn [\n\t\t\t\t...(wantAuthored ? [{ filter: { authorLogin: userId } }] : []),\n\t\t\t\t...(wantReviewed ? toAzureReviewerReads(userId, groupsByCollection.get(collection)!, states) : []),\n\t\t\t];\n\t\t};\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst { tokenWithInfo, options: apiOptions } = this.getApiOptions(session);\n\t\tconst drains = await mapBounded(\n\t\t\tfacets.flatMap(facet => filtersFor(facet.collection).map(f => ({ facet: facet, ...f }))),\n\t\t\tproviderFanOutConcurrency,\n\t\t\tasync ({ facet, filter, keep }) => {\n\t\t\t\tconst baseUrl = this.collectionApiBaseUrl(session, facet.collection);\n\t\t\t\tconst values: ProviderPullRequest[] = [];\n\t\t\t\tlet page = 1;\n\t\t\t\tfor (let read = 1; ; read++) {\n\t\t\t\t\tif (cancellation?.aborted) throw new CancellationError();\n\n\t\t\t\t\tconst input = {\n\t\t\t\t\t\t...apiOptions,\n\t\t\t\t\t\t// A reviewer facet narrows the states itself (see `toAzureReviewerReads`).\n\t\t\t\t\t\tstates: states,\n\t\t\t\t\t\t...filter,\n\t\t\t\t\t\tpage: page,\n\t\t\t\t\t\tpageSize: azurePullRequestSearchPageSize,\n\t\t\t\t\t\tbaseUrl: baseUrl,\n\t\t\t\t\t};\n\t\t\t\t\tconst result =\n\t\t\t\t\t\tfacet.repository != null\n\t\t\t\t\t\t\t? await api\n\t\t\t\t\t\t\t\t\t.getPullRequestsForRepo(\n\t\t\t\t\t\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tnamespace: facet.collection,\n\t\t\t\t\t\t\t\t\t\t\tname: facet.repository,\n\t\t\t\t\t\t\t\t\t\t\tproject: facet.project.name,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tinput,\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t.then(r => ({\n\t\t\t\t\t\t\t\t\t\tvalues: r.values,\n\t\t\t\t\t\t\t\t\t\tmore: r.paging?.more === true,\n\t\t\t\t\t\t\t\t\t\tnextPage: r.paging?.nextPage,\n\t\t\t\t\t\t\t\t\t}))\n\t\t\t\t\t\t\t: await api\n\t\t\t\t\t\t\t\t\t.getPullRequestsForAzureProject(\n\t\t\t\t\t\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\t\t\t\t\t\t{ namespace: facet.collection, project: facet.project.name },\n\t\t\t\t\t\t\t\t\t\tinput,\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t.then(r =>\n\t\t\t\t\t\t\t\t\t\tr != null\n\t\t\t\t\t\t\t\t\t\t\t? { values: r.data, more: r.hasMore, nextPage: r.nextPage ?? undefined }\n\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\tif (result == null) throw new Error('Azure DevOps returned no pull request page');\n\n\t\t\t\t\tvalues.push(...result.values);\n\t\t\t\t\tif (!result.more) return { facet: facet, keep: keep, values: values, truncated: false };\n\t\t\t\t\t// Follows the page the provider says comes next rather than assuming one: a continuation that\n\t\t\t\t\t// doesn't advance would re-read or skip rows, so it fails the facet — and with it the search —\n\t\t\t\t\t// instead of serving a union built from it.\n\t\t\t\t\tif (result.nextPage == null || result.nextPage <= page) {\n\t\t\t\t\t\tthrow new Error('Azure DevOps returned no advancing pull request continuation');\n\t\t\t\t\t}\n\t\t\t\t\t// The budget counts pages READ, not rows kept: several states are one `status=all` query the SDK\n\t\t\t\t\t// narrows client-side, so counting survivors would keep paging through rows it discards.\n\t\t\t\t\tif (read * azurePullRequestSearchPageSize >= azurePullRequestSearchFacetLimit) {\n\t\t\t\t\t\treturn { facet: facet, keep: keep, values: values, truncated: true };\n\t\t\t\t\t}\n\n\t\t\t\t\tpage = result.nextPage;\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\tconst rows = new Map<string, AzurePullRequestSearchRow>();\n\t\tfor (const { facet, keep, values } of drains) {\n\t\t\tfor (const pr of values) {\n\t\t\t\tif (keep != null && !keep(pr)) continue;\n\t\t\t\tif (!matches(pr)) continue;\n\n\t\t\t\tconst identity =\n\t\t\t\t\tgetProviderPullRequestIdentity(pr) ?? `project:${facet.collection}/${facet.project.name}:${pr.id}`;\n\t\t\t\tif (rows.has(identity)) continue;\n\n\t\t\t\tconst userId = userIds.get(facet.collection);\n\t\t\t\trows.set(identity, {\n\t\t\t\t\tpr: markMyGroupReviews(pr, groupIds),\n\t\t\t\t\tproject: facet.project,\n\t\t\t\t\tcurrentAccount: userId != null ? { id: userId } : undefined,\n\t\t\t\t\tposition: toAzurePullRequestSearchPosition(pr, sort, identity),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\trows: [...rows.values()].sort((a, b) =>\n\t\t\t\tcompareAzurePullRequestSearchPositions(a.position, b.position, sort),\n\t\t\t),\n\t\t\ttruncated: failures.length > 0 || drains.some(d => d.truncated),\n\t\t\tfailures: failures.length > 0 ? failures : undefined,\n\t\t};\n\t}\n\n\t/**\n\t * The repositories or projects a pull-request search reads, each with its collection and project descriptor.\n\t *\n\t * Repositories are read one by one; an `org` is its collection's projects; a search bounded only by the current\n\t * user reads every project the account can see, the same breadth as the account-wide pull request read.\n\t */\n\tprivate async resolvePullRequestSearchFacets(\n\t\tsession: ProviderAuthenticationSession,\n\t\trepos: ProviderRepoInput[] | undefined,\n\t\torg: string | undefined,\n\t): Promise<{ collection: string; project: AzureProjectDescriptor; repository?: string }[]> {\n\t\tconst collections = await this.getSearchCollections(session);\n\t\tconst collectionFor = (name: string): AzureOrganizationDescriptor => {\n\t\t\tconst collection = findAzureCollection(collections, name);\n\t\t\tif (collection == null) throw this.searchUnavailable(`the '${name}' collection is not visible`);\n\n\t\t\treturn collection;\n\t\t};\n\n\t\tif (repos?.length) {\n\t\t\tif (org && repos.some(r => !sameAzureName(r.namespace, org))) {\n\t\t\t\tthrow this.searchUnavailable(`the repositories are not all in the '${org}' collection`);\n\t\t\t}\n\n\t\t\t// One discovery per distinct collection, however many of its repositories the search names.\n\t\t\tconst projectsByCollection = new Map<string, Promise<AzureProjectDescriptor[]>>();\n\t\t\tconst projectsOf = (collection: AzureOrganizationDescriptor): Promise<AzureProjectDescriptor[]> => {\n\t\t\t\tlet projects = projectsByCollection.get(collection.id);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tprojects = this.getSearchProjects(session, collection, true);\n\t\t\t\t\tprojectsByCollection.set(collection.id, projects);\n\t\t\t\t}\n\t\t\t\treturn projects;\n\t\t\t};\n\n\t\t\treturn Promise.all(\n\t\t\t\trepos.map(async r => {\n\t\t\t\t\tconst collection = collectionFor(r.namespace);\n\t\t\t\t\tconst identity = getAzureRepositoryIdentity({\n\t\t\t\t\t\towner: r.namespace,\n\t\t\t\t\t\tname: r.name,\n\t\t\t\t\t\tproject: r.project,\n\t\t\t\t\t});\n\t\t\t\t\tif (identity.projectName == null) {\n\t\t\t\t\t\tthrow this.searchUnavailable(`repository '${r.namespace}/${r.name}' names no project`);\n\t\t\t\t\t}\n\t\t\t\t\t// The repository becomes a URL segment the SDK encodes but doesn't validate, and a segment of `..`\n\t\t\t\t\t// survives encoding and resolves to the project-wide route: refused rather than widened.\n\t\t\t\t\tif (!isAzureRouteSegment(identity.repositoryName)) {\n\t\t\t\t\t\tthrow this.searchUnavailable(`repository '${r.namespace}/${r.name}' is not a valid name`);\n\t\t\t\t\t}\n\n\t\t\t\t\tconst project = (await projectsOf(collection)).find(p =>\n\t\t\t\t\t\tsameAzureName(p.name, identity.projectName!),\n\t\t\t\t\t);\n\t\t\t\t\tif (project == null) {\n\t\t\t\t\t\tthrow this.searchUnavailable(\n\t\t\t\t\t\t\t`project '${identity.projectName}' is not visible in '${collection.name}'`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn { collection: collection.name, project: project, repository: identity.repositoryName };\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst scoped = org ? [collectionFor(org)] : collections;\n\t\tconst projects = await Promise.all(scoped.map(c => this.getSearchProjects(session, c, true)));\n\t\treturn projects.flat().map(p => ({ collection: p.resourceName, project: p }));\n\t}\n\n\tprivate readonly _collectionUserIds = new PromiseCache<string, string | undefined>({ capacity: 50 });\n\n\t/**\n\t * The current user's identity id in `collection`, read from the collection's own connection data, which is\n\t * what Azure's creator and reviewer filters compare against. Cached per credential and collection, since it\n\t * can't change for either.\n\t */\n\tprotected override getFilterUserId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tcollection: string,\n\t): Promise<string | undefined> {\n\t\tconst baseUrl = this.collectionApiBaseUrl(session, collection);\n\t\treturn this._collectionUserIds.getOrCreate(\n\t\t\tJSON.stringify([session.accessToken, baseUrl, collection.toLowerCase()]),\n\t\t\tasync cacheable => {\n\t\t\t\tconst azure = await this.authenticationService.apis.azure;\n\t\t\t\tconst user = await azure?.getCurrentUserOnServer(\n\t\t\t\t\tthis,\n\t\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\t\t`${baseUrl}/${encodeURIComponent(collection)}`,\n\t\t\t\t);\n\t\t\t\t// A response without an identity must not refuse later relationship searches from a cached miss.\n\t\t\t\tif (user?.id == null) {\n\t\t\t\t\tcacheable.invalidate();\n\t\t\t\t}\n\n\t\t\t\treturn user?.id;\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Resolved per collection, which a raw row names as its repository owner: a collection's pull requests carry the\n\t * id {@link getFilterUserId} resolves there, never the server-level account's. A row that names no collection or\n\t * has no author to match gets no viewer, nor does one in a collection whose id can't be read: its authorship\n\t * stays unknown.\n\t */\n\toverride async getPullRequestViewers(\n\t\tpullRequests: readonly (ProviderPullRequest | PullRequest)[],\n\t\tconnectionId?: string,\n\t): Promise<({ id: string; username?: string } | undefined)[]> {\n\t\tconst session = await this.resolveReadSessionOrThrow(connectionId, undefined);\n\t\tif (session == null) return pullRequests.map(() => undefined);\n\n\t\tconst viewerFor = this.getOrganizationViewers(session, undefined);\n\t\treturn Promise.all(\n\t\t\tpullRequests.map(pr => {\n\t\t\t\t// Azure DevOps Server's rows are provider-apis' own; only GitHub's are native.\n\t\t\t\tconst collection = isNativePullRequest(pr) ? undefined : pr.repository?.owner?.login;\n\t\t\t\treturn collection && pr.author != null ? viewerFor(collection) : Promise.resolve(undefined);\n\t\t\t}),\n\t\t);\n\t}\n\n\t/**\n\t * Ignores `account`: it is the server-level one, which no collection's pull requests carry. A collection whose id\n\t * can't be read resolves `undefined`, since authorship is optional enrichment and one collection's failed lookup\n\t * must not fail the read or blank the others; it is asked once per read, as a miss isn't cached.\n\t */\n\tprotected override getOrganizationViewers(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_account: { id: string; username?: string } | undefined,\n\t): (collection: string) => Promise<{ id: string } | undefined> {\n\t\tconst viewers = new Map<string, Promise<{ id: string } | undefined>>();\n\t\treturn collection => {\n\t\t\tconst key = collection.toLowerCase();\n\t\t\tlet viewer = viewers.get(key);\n\t\t\tif (viewer == null) {\n\t\t\t\tviewer = this.getFilterUserId(session, collection).then(\n\t\t\t\t\tid => (id != null ? { id: id } : undefined),\n\t\t\t\t\t() => undefined,\n\t\t\t\t);\n\t\t\t\tviewers.set(key, viewer);\n\t\t\t}\n\t\t\treturn viewer;\n\t\t};\n\t}\n\n\t/** Every collection the account can see, or a refusal when discovery itself failed. */\n\tprivate async getSearchCollections(session: ProviderAuthenticationSession): Promise<AzureOrganizationDescriptor[]> {\n\t\tconst collections = await this.getProviderResourcesForUser(session);\n\t\tif (collections == null) throw this.searchUnavailable('the account’s collections could not be read');\n\n\t\treturn collections;\n\t}\n\n\t/**\n\t * A collection's projects, refused when discovery was incomplete: a search bounded by an incomplete project set\n\t * would silently omit the missing projects' matches. `refresh` re-reads them rather than trusting the discovery\n\t * cache, which lives for the whole connection and so doesn't know a project created since.\n\t */\n\tprivate async getSearchProjects(\n\t\tsession: ProviderAuthenticationSession,\n\t\tcollection: AzureOrganizationDescriptor,\n\t\trefresh: boolean = false,\n\t): Promise<AzureProjectDescriptor[]> {\n\t\tconst projects = refresh\n\t\t\t? await this.refreshSearchProjects(session, collection)\n\t\t\t: await this.getProviderProjectsForResources(session, [collection]);\n\t\tif (projects.metadata != null && projects.metadata.completeness !== 'complete') {\n\t\t\tthrow this.searchUnavailable(`the projects of the '${collection.name}' collection could not all be read`);\n\t\t}\n\n\t\treturn projects.values;\n\t}\n\n\tprivate readonly _projectRefreshes = new Map<\n\t\tstring,\n\t\tPromise<ProviderApiCollectionResult<AzureProjectDescriptor>>\n\t>();\n\n\t/**\n\t * Re-reads a collection's projects, sharing one read among the refreshes IN FLIGHT at once — a count batch or a\n\t * multi-repository drain refreshes the same collection once per scope, and each would otherwise re-read every\n\t * project page. Nothing outlives the read, so the next search still sees a project created after it.\n\t */\n\tprivate refreshSearchProjects(\n\t\tsession: ProviderAuthenticationSession,\n\t\tcollection: AzureOrganizationDescriptor,\n\t): Promise<ProviderApiCollectionResult<AzureProjectDescriptor>> {\n\t\tconst key = `${this.discoveryKey(session)}:${collection.id}`;\n\t\tlet pending = this._projectRefreshes.get(key);\n\t\tif (pending == null) {\n\t\t\tpending = this.getProviderProjectsForResources(session, [collection], true).finally(() => {\n\t\t\t\tthis._projectRefreshes.delete(key);\n\t\t\t});\n\t\t\tthis._projectRefreshes.set(key, pending);\n\t\t}\n\n\t\treturn pending;\n\t}\n\n\t/**\n\t * Refuses dates the search can't apply before anything is read. The facade only checks that dates are\n\t * supported, not their shape, and a refusal here stays with its own scope in a count batch.\n\t */\n\tprivate assertSearchDates(criteria: { updatedAfter?: string; createdAfter?: string } | undefined): void {\n\t\tconst invalid = getInvalidAzureSearchDate(criteria);\n\t\tif (invalid != null) throw this.searchUnavailable(invalid);\n\t}\n\n\tprivate searchUnavailable(reason: string): IntegrationReadUnavailableError {\n\t\treturn new IntegrationReadUnavailableError(this.name, `search refused: ${reason}.`);\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tsuper.providerOnDisconnect();\n\t\tthis._pullRequestSearches.clear();\n\t\tthis._latestPullRequestSearches.clear();\n\t\tthis._collectionUserIds.clear();\n\t}\n\n\tprotected override async _requestForCurrentUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<Account | undefined> {\n\t\tconst azure = await this.authenticationService.apis.azure;\n\t\tconst user = azure\n\t\t\t? await azure.getCurrentUserOnServer(this, toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session))\n\t\t\t: undefined;\n\t\treturn user\n\t\t\t? {\n\t\t\t\t\tprovider: this,\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tname: user.name ?? undefined,\n\t\t\t\t\temail: user.email ?? undefined,\n\t\t\t\t\tavatarUrl: user.avatarUrl ?? undefined,\n\t\t\t\t\tusername: user.username ?? undefined,\n\t\t\t\t}\n\t\t\t: undefined;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqEA,SAAS,oBAAoB,OAAwB;CACpD,OAAO,MAAM,SAAS,KAAK,UAAU,OAAO,UAAU;AACvD;;;;;;AAOA,eAAe,eAAkB,OAAuE;CACvG,IAAI;EACH,OAAO,MAAM,MAAM;CACpB,SAAS,IAAI;EACZ,IAAI,cAAc,iCAAiC,OAAO;EAE1D,MAAM;CACP;AACD;AAEA,SAAS,oBACR,aACA,MAC0C;CAC1C,OAAO,YAAY,MAAK,MAAK,cAAc,EAAE,MAAM,IAAI,KAAK,EAAE,OAAO,IAAI;AAC1E;;;;;;AAOA,MAAM,mCAAmC;;;;;AAMzC,MAAM,iCAAiC;;;;;AAMvC,MAAM,6BAA6B;;;;;;AAOnC,MAAM,2BAA2B;AACjC,MAAM,0BAA0B;;;;;;AAOhC,MAAM,2BAA2B;;;;;;AAyBjC,SAAS,yBACR,SACA,MACU;CACV,MAAM,WAAW,QAAQ;CACzB,OAAO;EACN,OAAO,QAAQ,OAAO,KAAI,MAAK;GAAC,EAAE;GAAW,EAAE,WAAW;GAAM,EAAE;EAAI,CAAC;EACvE,KAAK,QAAQ,OAAO;EACpB,eAAe,CAAC,GAAG,IAAI,IAAI,UAAU,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;EAChE,QAAQ,CAAC,GAAG,IAAI,IAAI,UAAU,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;EAClD,MAAM,UAAU,MAAM,KAAK,KAAK;EAChC,OAAO,UAAU,SAAS;EAC1B,cAAc,UAAU,gBAAgB;EACxC,cAAc,UAAU,gBAAgB;EAClC;CACP;AACD;AAEA,MAAM,iBAAiB,kBAAA;AACvB,MAAM,qBAAqB,OAAO,OAAO;CAAE,IAAI,eAAe;CAAI,QAAQ,eAAe;AAAO,CAAC;AAEjG,IAAa,+BAAb,cAAkD,2BAA8E;CAWrH;CAVV,eAAqE;CACrE,KAAS;CACT;CACA,OAAwB;CAExB,YACC,KACA,uBACA,iBACA,qBACA,QACC;EACD,MAAM,KAAK,uBAAuB,iBAAiB,mBAAmB;EAF7D,KAAA,SAAA;EAGT,KAAK,MAAM,GAAG,KAAK,GAAG,GAAG,KAAK;CAC/B;CAEA,cAAiC,SAAgD;EAChF,OAAO,KAAK,yBAAyB,OAAO;CAC7C;;;;;;CAOA,aAAgC,SAAgD;EAC/E,OAAO,GAAG,QAAQ,YAAY,GAAG,KAAK,cAAc,OAAO;CAC5D;CAEA,0BACC,SACA,OACS;EAIT,MAAM,SAAS,IAAI,IAAI,MAAM,KAAI,MAAM,OAAO,MAAM,WAAW,EAAE,YAAY,KAAA,CAAU,CAAC;EACxF,MAAM,CAAC,SAAS;EAChB,MAAM,UAAU,KAAK,cAAc,OAAO;EAC1C,OAAO,OAAO,SAAS,KAAK,SAAS,OAClC,6BAA6B,SAAS;GAAS;GAAO,kBAAkB,KAAA;EAAU,CAAC,IACnF;CACJ;CAEA,cACC,SACA,oBAA6B,OAI5B;EACD,MAAM,EAAE,SAAS,GAAG,SAAS,MAAM,cAAc,SAAS,iBAAiB;EAC3E,OAAO;GACN,GAAG;GACH,SAAS;IAAE,GAAG;IAAS,SAAS,KAAK,cAAc,OAAO;GAAE;EAC7D;CACD;CAEA,wBACC,SACA,YACuC;EACvC,OAAO;GACN,GAAG,MAAM,wBAAwB,SAAS,UAAU;GACpD,SAAS,KAAK,qBAAqB,SAAS,UAAU;EACvD;CACD;;CAGA,qBAAwC,SAAwC,YAA4B;EAC3G,OAAO,6BAA6B,KAAK,cAAc,OAAO,GAAG;GAChE,OAAO;GACP,kBAAkB,KAAA;EACnB,CAAC;CACF;;;;;;CAOA,MAAyB,wBACxB,SACA,QAC+C;EAC/C,IAAI;GACH,OAAO,MAAM,MAAM,wBAAwB,SAAS,MAAM;EAC3D,SAAS,IAAI;GACZ,IAAI,EAAE,cAAc,uBAAuB,MAAM;GAGjD,MAAM,YAAY,OAAM,MADJ,KAAK,sBAAsB,KAAK,MAAA,EACrB,uBAC9B,MACA,gBAAgB,KAAK,IAAI,OAAO,GAChC,KAAK,cAAc,OAAO,CAC3B;GACA,IAAI,aAAa,MAAM,MAAM;GAE7B,OAAO,CAAC,SAAS;EAClB;CACD;;;;;CAMA,MAAyB,yBACxB,SACA,SAOA,cAC+C;EAC/C,MAAM,QAAQ,MAAM,KAAK,sBAAsB,KAAK;EACpD,IAAI,SAAS,MAAM,OAAO,KAAA;EAE1B,KAAK,kBAAkB,QAAQ,QAAQ;EAIvC,MAAM,QAAQ,MAAM,KAAK,2BACxB,SACA,QAAQ,OACR,QAAQ,KACR,+BAA+B,QAAQ,MAAM,CAC9C;EACA,OAAO,MAAM,oBACZ,MACA,gBAAgB,KAAK,IAAI,OAAO,GAChC,MAAM,YACN;GACC,SAAS,KAAK,qBAAqB,SAAS,MAAM,UAAU;GAC5D,UAAU,QAAQ;GAClB,cAAc,MAAM;GACpB,iBAAgB,SAAQ,MAAM,SAAS,MAAK,MAAK,cAAc,EAAE,MAAM,IAAI,CAAC;GAC5E,QAAQ,QAAQ;GAChB,UAAU,QAAQ;EACnB,GACA,YACD;CACD;;;;;;;CAQA,MAAyB,oBACxB,SACA,QACA,cAC6C;EAC7C,MAAM,QAAQ,MAAM,KAAK,sBAAsB,KAAK;EACpD,IAAI,SAAS,MAAM,OAAO,KAAA;EAE1B,MAAM,gBAAgB,gBAAgB,KAAK,IAAI,OAAO;EACtD,OAAO,WAAW,QAAQ,6BAA4B,MACrD,eAAe,YAAY;GAC1B,KAAK,kBAAkB,EAAE,QAAQ;GAGjC,MAAM,QAAQ,MAAM,KAAK,2BAA2B,SAAS,EAAE,OAAO,EAAE,KAAK,IAAI;GACjF,OAAO,MAAM,eACZ,MACA,eACA,MAAM,YACN;IACC,SAAS,KAAK,qBAAqB,SAAS,MAAM,UAAU;IAC5D,UAAU,EAAE;IACZ,cAAc,MAAM;GACrB,GACA,YACD;EACD,CAAC,CACF;CACD;;;;;;;;;;;;;;CAeA,MAAc,2BACb,SACA,OACA,KACA,kBAA2B,OACoE;EAC/F,MAAM,kBAAkB,iBAAiB,OAAO,KAAI,MAAK,EAAE,SAAS,KAAK,CAAC,CAAC;EAC3E,IAAI,gBAAgB,SAAS,GAC5B,MAAM,KAAK,kBACV,8EACD;EAED,IAAI,OAAO,gBAAgB,WAAW,KAAK,CAAC,cAAc,KAAK,gBAAgB,EAAE,GAChF,MAAM,KAAK,kBAAkB,oCAAoC,IAAI,aAAa;EAGnF,MAAM,cAAc,MAAM,KAAK,qBAAqB,OAAO;EAC3D,MAAM,OAAO,OAAO,gBAAgB,OAAO,YAAY,WAAW,IAAI,YAAY,EAAE,CAAC,OAAO,KAAA;EAC5F,IAAI,QAAQ,MACX,MAAM,KAAK,kBACV,uBAAuB,YAAY,OAAO,uDAC3C;EAGD,MAAM,aAAa,oBAAoB,aAAa,IAAI;EACxD,IAAI,cAAc,MAAM,MAAM,KAAK,kBAAkB,QAAQ,KAAK,4BAA4B;EAE9F,MAAM,WAAW,MAAM,KAAK,kBAAkB,SAAS,YAAY,eAAe;EAClF,IAAI,CAAC,OAAO,QAAQ,OAAO;GAAE,YAAY,WAAW;GAAgB;EAAS;EAE7E,MAAM,eAAe,iBACpB,MAAM,KAAI,MAAK;GACd,MAAM,cAAc,2BAA2B;IAC9C,OAAO,EAAE;IACT,MAAM,EAAE;IACR,SAAS,EAAE;GACZ,CAAC,CAAC,CAAC;GACH,IAAI,eAAe,MAClB,MAAM,KAAK,kBAAkB,eAAe,EAAE,UAAU,GAAG,EAAE,KAAK,mBAAmB;GAGtF,MAAM,UAAU,SAAS,MAAK,MAAK,cAAc,EAAE,MAAM,WAAW,CAAC;GACrE,IAAI,WAAW,MACd,MAAM,KAAK,kBAAkB,YAAY,YAAY,uBAAuB,WAAW,KAAK,EAAE;GAG/F,OAAO,QAAQ;EAChB,CAAC,CACF;EACA,OAAO;GAAE,YAAY,WAAW;GAAgB;GAAwB;EAAa;CACtF;;;;;;CAOA,MAAyB,+BACxB,SACA,SAQA,cACqD;EACrD,KAAK,kBAAkB,QAAQ,QAAQ;EACvC,MAAM,OAAO,QAAQ,UAAU,QAAA;EAC/B,MAAM,QAAQ,yBAAyB,SAAS,IAAI;EACpD,MAAM,MAAM,kCAAkC,KAAK;EACnD,MAAM,SAAS,kCAAkC,QAAQ,QAAQ,GAAG;EAOpE,MAAM,UAAU,QAAQ,SAAS,KAAK;EACtC,MAAM,WAAW,KAAK,0BAA0B,SAAS,OAAO,OAAO;EAEvE,MAAM,aACL,UAAU,OACP,KAAK,qBAAqB,IAAI,QAAQ,IACtC,KAAK,qBAAqB,KAAK,sBAAsB,UAAU,SAAS,SAAS,MAAM,YAAY;EACvG,IAAI,cAAc,MACjB,MAAM,IAAI,MACT,wGACD;EAGD,MAAM,QAAQ,MAAM;EACpB,IAAI,UAAU,MAAM;GAGnB,MAAM,YAAY,KAAK,0BAA0B,SAAS,KAAK;GAC/D,KAAK,2BAA2B,OAAO,SAAS;GAChD,KAAU,2BAA2B,YAAY,iBAAiB,QAAQ,QAAQ,KAAK,CAAC;EACzF;EAEA,MAAM,WAAW,sBAAsB,QAAQ,UAAU,IAAI,GAAG;EAChE,MAAM,QACL,UAAU,OACP,IACA,MAAM,KAAK,WAAU,MAAK,uCAAuC,EAAE,UAAU,OAAO,OAAO,IAAI,IAAI,CAAC;EACxG,MAAM,QAAQ,QAAQ,IAAI,CAAC,IAAI,MAAM,KAAK,MAAM,OAAO,QAAQ,QAAQ;EACvE,MAAM,OAAO,QAAQ,QAAQ;EAC7B,MAAM,OAAO,MAAM,GAAG,EAAE;EACxB,MAAM,UAAU,QAAQ,QAAQ,QAAQ,MAAM,SAAS,MAAM,KAAK;EAClE,OAAO;GACN,QAAQ,MAAM,KAAI,MACjB,wBAAwB,EAAE,IAAI,MAAM;IACnC,SAAS,EAAE;IACX,gBAAgB,EAAE;IAClB,YAAY,QAAQ,UAAU,mBAAmB;GAClD,CAAC,CACF;GACA,QAAQ,UACL,KAAK,UAAU;IAAO;IAAK,OAAO;IAAS,MAAM,OAAO;IAAG,OAAO,KAAK;GAAS,CAAC,IACjF,KAAA;GACM;GACH;GACN,WAAW,MAAM;GACjB,YAAY,MAAM,YAAY,KAAA,IAAY,MAAM,KAAK;GACrD,UAAU,MAAM,YAAY,OAAO;IAAE,cAAc;IAAW,UAAU,MAAM;GAAS,IAAI,KAAA;EAC5F;CACD;;;;;CAMA,MAAyB,0BACxB,SACA,QACA,cAC4D;EAG5D,OAAO,WAAW,QAAQ,IAAG,MAC5B,eAAe,YAAY;GAC1B,KAAK,kBAAkB,EAAE,QAAQ;GACjC,MAAM,OAAO,EAAE,UAAU,QAAA;GACzB,MAAM,QAAQ,MAAM,KAAK,qBACxB,KAAK,4BACL,KAAK,0BAA0B,SAAS,yBAAyB,GAAG,IAAI,CAAC,GACzE,SACA,GACA,MACA,YACD;GAEA,OAAO,EAAE,OAAO,MAAM,YAAY,KAAA,IAAY,MAAM,KAAK,OAAO;EACjE,CAAC,CACF;CACD;;;;;CAMA,uBAAwC,IAAI,aAAkD;EAC7F,UAAU;EACV,WAAW;EACX,WAAW;CACZ,CAAC;;;;;CAKD,6BAA8C,IAAI,aAAkD;EACnG,UAAU;EACV,WAAW;CACZ,CAAC;;;;;;CAOD,0BACC,SACA,OACA,SACS;EACT,OAAO,KAAK,UAAU;GAAC,QAAQ;GAAa,KAAK,cAAc,OAAO;GAAG;GAAO,WAAW;EAAI,CAAC;CACjG;CAEA,qBACC,OACA,UACA,SACA,SACA,MACA,cACuC;EACvC,OAAO,MAAM,YACZ,WACC,YAAY,WAAW,KAAK,uBAAuB,SAAS,SAAS,MAAM,MAAM,GAClF,EAAgB,aAAa,CAC9B;CACD;;;;;;;;;;;;;CAcA,MAAc,uBACb,SACA,SACA,MACA,cACuC;EACvC,MAAM,WAAW,QAAQ;EAEzB,MAAM,UAAU,+BAA+B,QAAQ;EACvD,MAAM,gBAAgB,IAAI,IAAI,UAAU,iBAAiB,CAAC,CAAC;EAC3D,MAAM,eAAe,cAAc,IAAA,QAA4B;EAC/D,MAAM,eACL,cAAc,IAAA,UAA8B,KAAK,cAAc,IAAA,kBAAqC;EAIrG,MAAM,SAAS,MAAM,KAAK,+BAA+B,SAAS,QAAQ,OAAO,QAAQ,GAAG;EAM5F,MAAM,0BAAU,IAAI,IAAoB;EACxC,IAAI,cAAc,OAAO,GACxB,KAAK,MAAM,cAAc,iBAAiB,OAAO,KAAI,MAAK,EAAE,UAAU,CAAC,GAAG;GACzE,MAAM,SAAS,MAAM,KAAK,gBAAgB,SAAS,UAAU;GAC7D,IAAI,UAAU,MACb,MAAM,KAAK,kBAAkB,8CAA8C,WAAW,EAAE;GAGzF,QAAQ,IAAI,YAAY,MAAM;EAC/B;EAOD,MAAM,WAAqC,CAAC;EAC5C,MAAM,qBAAqB,IAAI,IAC9B,eACG,MAAM,QAAQ,IACd,MAAM,KAAK,SAAS,OAAO,CAAC,YAAY,YAAY;GACnD,IAAI;IACH,OAAO,CACN,YACA,MAAM,KAAK,oBAAoB,SAAS,YAAY,MAAM,CAC3D;GACD,SAAS,IAAI;IACZ,MAAM,aAAa,OAAO,MAAK,MAAK,EAAE,eAAe,UAAU,CAAC,CAAE,QAAQ;IAC1E,SAAS,KACR,yBAAyB;KAAE,YAAY,KAAK;KAAgB;IAAW,GAAG,EAAE,CAC7E;IACA,OAAO,CAAC,4BAAY,IAAI,IAAY,CAAC;GACtC;EACD,CAAC,CACF,IACC,CAAC,CACL;EACA,MAAM,WAAW,IAAI,IAAI,CAAC,GAAG,mBAAmB,OAAO,CAAC,CAAC,CAAC,SAAQ,QAAO,CAAC,GAAG,GAAG,CAAC,CAAC;EAElF,MAAM,SAAS,4BAA4B,UAAU,QAAQ,SAAS,SAAS,SAAS,MAAM;EAC9F,MAAM,cACL,eAC4G;GAC5G,IAAI,cAAc,SAAS,GAAG,OAAO,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;GAEpD,MAAM,SAAS,QAAQ,IAAI,UAAU;GACrC,OAAO,CACN,GAAI,eAAe,CAAC,EAAE,QAAQ,EAAE,aAAa,OAAO,EAAE,CAAC,IAAI,CAAC,GAC5D,GAAI,eAAe,qBAAqB,QAAQ,mBAAmB,IAAI,UAAU,GAAI,MAAM,IAAI,CAAC,CACjG;EACD;EAEA,MAAM,MAAM,MAAM,KAAK,gBAAgB;EACvC,MAAM,EAAE,eAAe,SAAS,eAAe,KAAK,cAAc,OAAO;EACzE,MAAM,SAAS,MAAM,WACpB,OAAO,SAAQ,UAAS,WAAW,MAAM,UAAU,CAAC,CAAC,KAAI,OAAM;GAAS;GAAO,GAAG;EAAE,EAAE,CAAC,GAAA,GAEvF,OAAO,EAAE,OAAO,QAAQ,WAAW;GAClC,MAAM,UAAU,KAAK,qBAAqB,SAAS,MAAM,UAAU;GACnE,MAAM,SAAgC,CAAC;GACvC,IAAI,OAAO;GACX,KAAK,IAAI,OAAO,IAAK,QAAQ;IAC5B,IAAI,cAAc,SAAS,MAAM,IAAI,kBAAkB;IAEvD,MAAM,QAAQ;KACb,GAAG;KAEK;KACR,GAAG;KACG;KACN,UAAU;KACD;IACV;IACA,MAAM,SACL,MAAM,cAAc,OACjB,MAAM,IACL,uBACA,eACA;KACC,WAAW,MAAM;KACjB,MAAM,MAAM;KACZ,SAAS,MAAM,QAAQ;IACxB,GACA,KACD,CAAC,CACA,MAAK,OAAM;KACX,QAAQ,EAAE;KACV,MAAM,EAAE,QAAQ,SAAS;KACzB,UAAU,EAAE,QAAQ;IACrB,EAAE,IACF,MAAM,IACL,+BACA,eACA;KAAE,WAAW,MAAM;KAAY,SAAS,MAAM,QAAQ;IAAK,GAC3D,KACD,CAAC,CACA,MAAK,MACL,KAAK,OACF;KAAE,QAAQ,EAAE;KAAM,MAAM,EAAE;KAAS,UAAU,EAAE,YAAY,KAAA;IAAU,IACrE,KAAA,CACJ;IACJ,IAAI,UAAU,MAAM,MAAM,IAAI,MAAM,4CAA4C;IAEhF,OAAO,KAAK,GAAG,OAAO,MAAM;IAC5B,IAAI,CAAC,OAAO,MAAM,OAAO;KAAS;KAAa;KAAc;KAAQ,WAAW;IAAM;IAItF,IAAI,OAAO,YAAY,QAAQ,OAAO,YAAY,MACjD,MAAM,IAAI,MAAM,8DAA8D;IAI/E,IAAI,OAAO,kCAAkC,kCAC5C,OAAO;KAAS;KAAa;KAAc;KAAQ,WAAW;IAAK;IAGpE,OAAO,OAAO;GACf;EACD,CACD;EAEA,MAAM,uBAAO,IAAI,IAAuC;EACxD,KAAK,MAAM,EAAE,OAAO,MAAM,YAAY,QACrC,KAAK,MAAM,MAAM,QAAQ;GACxB,IAAI,QAAQ,QAAQ,CAAC,KAAK,EAAE,GAAG;GAC/B,IAAI,CAAC,QAAQ,EAAE,GAAG;GAElB,MAAM,WACL,+BAA+B,EAAE,KAAK,WAAW,MAAM,WAAW,GAAG,MAAM,QAAQ,KAAK,GAAG,GAAG;GAC/F,IAAI,KAAK,IAAI,QAAQ,GAAG;GAExB,MAAM,SAAS,QAAQ,IAAI,MAAM,UAAU;GAC3C,KAAK,IAAI,UAAU;IAClB,IAAI,mBAAmB,IAAI,QAAQ;IACnC,SAAS,MAAM;IACf,gBAAgB,UAAU,OAAO,EAAE,IAAI,OAAO,IAAI,KAAA;IAClD,UAAU,iCAAiC,IAAI,MAAM,QAAQ;GAC9D,CAAC;EACF;EAGD,OAAO;GACN,MAAM,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,MACjC,uCAAuC,EAAE,UAAU,EAAE,UAAU,IAAI,CACpE;GACA,WAAW,SAAS,SAAS,KAAK,OAAO,MAAK,MAAK,EAAE,SAAS;GAC9D,UAAU,SAAS,SAAS,IAAI,WAAW,KAAA;EAC5C;CACD;;;;;;;CAQA,MAAc,+BACb,SACA,OACA,KAC0F;EAC1F,MAAM,cAAc,MAAM,KAAK,qBAAqB,OAAO;EAC3D,MAAM,iBAAiB,SAA8C;GACpE,MAAM,aAAa,oBAAoB,aAAa,IAAI;GACxD,IAAI,cAAc,MAAM,MAAM,KAAK,kBAAkB,QAAQ,KAAK,4BAA4B;GAE9F,OAAO;EACR;EAEA,IAAI,OAAO,QAAQ;GAClB,IAAI,OAAO,MAAM,MAAK,MAAK,CAAC,cAAc,EAAE,WAAW,GAAG,CAAC,GAC1D,MAAM,KAAK,kBAAkB,wCAAwC,IAAI,aAAa;GAIvF,MAAM,uCAAuB,IAAI,IAA+C;GAChF,MAAM,cAAc,eAA+E;IAClG,IAAI,WAAW,qBAAqB,IAAI,WAAW,EAAE;IACrD,IAAI,YAAY,MAAM;KACrB,WAAW,KAAK,kBAAkB,SAAS,YAAY,IAAI;KAC3D,qBAAqB,IAAI,WAAW,IAAI,QAAQ;IACjD;IACA,OAAO;GACR;GAEA,OAAO,QAAQ,IACd,MAAM,IAAI,OAAM,MAAK;IACpB,MAAM,aAAa,cAAc,EAAE,SAAS;IAC5C,MAAM,WAAW,2BAA2B;KAC3C,OAAO,EAAE;KACT,MAAM,EAAE;KACR,SAAS,EAAE;IACZ,CAAC;IACD,IAAI,SAAS,eAAe,MAC3B,MAAM,KAAK,kBAAkB,eAAe,EAAE,UAAU,GAAG,EAAE,KAAK,mBAAmB;IAItF,IAAI,CAAC,oBAAoB,SAAS,cAAc,GAC/C,MAAM,KAAK,kBAAkB,eAAe,EAAE,UAAU,GAAG,EAAE,KAAK,sBAAsB;IAGzF,MAAM,WAAW,MAAM,WAAW,UAAU,EAAA,CAAG,MAAK,MACnD,cAAc,EAAE,MAAM,SAAS,WAAY,CAC5C;IACA,IAAI,WAAW,MACd,MAAM,KAAK,kBACV,YAAY,SAAS,YAAY,uBAAuB,WAAW,KAAK,EACzE;IAGD,OAAO;KAAE,YAAY,WAAW;KAAe;KAAS,YAAY,SAAS;IAAe;GAC7F,CAAC,CACF;EACD;EAEA,MAAM,SAAS,MAAM,CAAC,cAAc,GAAG,CAAC,IAAI;EAE5C,QAAO,MADgB,QAAQ,IAAI,OAAO,KAAI,MAAK,KAAK,kBAAkB,SAAS,GAAG,IAAI,CAAC,CAAC,EAAA,CAC5E,KAAK,CAAC,CAAC,KAAI,OAAM;GAAE,YAAY,EAAE;GAAc,SAAS;EAAE,EAAE;CAC7E;CAEA,qBAAsC,IAAI,aAAyC,EAAE,UAAU,GAAG,CAAC;;;;;;CAOnG,gBACC,SACA,YAC8B;EAC9B,MAAM,UAAU,KAAK,qBAAqB,SAAS,UAAU;EAC7D,OAAO,KAAK,mBAAmB,YAC9B,KAAK,UAAU;GAAC,QAAQ;GAAa;GAAS,WAAW,YAAY;EAAC,CAAC,GACvE,OAAM,cAAa;GAElB,MAAM,OAAO,OAAM,MADC,KAAK,sBAAsB,KAAK,MAAA,EAC1B,uBACzB,MACA,gBAAgB,KAAK,IAAI,OAAO,GAChC,GAAG,QAAQ,GAAG,mBAAmB,UAAU,GAC5C;GAEA,IAAI,MAAM,MAAM,MACf,UAAU,WAAW;GAGtB,OAAO,MAAM;EACd,CACD;CACD;;;;;;;CAQA,MAAe,sBACd,cACA,cAC6D;EAC7D,MAAM,UAAU,MAAM,KAAK,0BAA0B,cAAc,KAAA,CAAS;EAC5E,IAAI,WAAW,MAAM,OAAO,aAAa,UAAU,KAAA,CAAS;EAE5D,MAAM,YAAY,KAAK,uBAAuB,SAAS,KAAA,CAAS;EAChE,OAAO,QAAQ,IACd,aAAa,KAAI,OAAM;GAEtB,MAAM,aAAa,oBAAoB,EAAE,IAAI,KAAA,IAAY,GAAG,YAAY,OAAO;GAC/E,OAAO,cAAc,GAAG,UAAU,OAAO,UAAU,UAAU,IAAI,QAAQ,QAAQ,KAAA,CAAS;EAC3F,CAAC,CACF;CACD;;;;;;CAOA,uBACC,SACA,UAC8D;EAC9D,MAAM,0BAAU,IAAI,IAAiD;EACrE,QAAO,eAAc;GACpB,MAAM,MAAM,WAAW,YAAY;GACnC,IAAI,SAAS,QAAQ,IAAI,GAAG;GAC5B,IAAI,UAAU,MAAM;IACnB,SAAS,KAAK,gBAAgB,SAAS,UAAU,CAAC,CAAC,MAClD,OAAO,MAAM,OAAO,EAAM,GAAG,IAAI,KAAA,SAC3B,KAAA,CACP;IACA,QAAQ,IAAI,KAAK,MAAM;GACxB;GACA,OAAO;EACR;CACD;;CAGA,MAAc,qBAAqB,SAAgF;EAClH,MAAM,cAAc,MAAM,KAAK,4BAA4B,OAAO;EAClE,IAAI,eAAe,MAAM,MAAM,KAAK,kBAAkB,6CAA6C;EAEnG,OAAO;CACR;;;;;;CAOA,MAAc,kBACb,SACA,YACA,UAAmB,OACiB;EACpC,MAAM,WAAW,UACd,MAAM,KAAK,sBAAsB,SAAS,UAAU,IACpD,MAAM,KAAK,gCAAgC,SAAS,CAAC,UAAU,CAAC;EACnE,IAAI,SAAS,YAAY,QAAQ,SAAS,SAAS,iBAAiB,YACnE,MAAM,KAAK,kBAAkB,wBAAwB,WAAW,KAAK,mCAAmC;EAGzG,OAAO,SAAS;CACjB;CAEA,oCAAqC,IAAI,IAGvC;;;;;;CAOF,sBACC,SACA,YAC+D;EAC/D,MAAM,MAAM,GAAG,KAAK,aAAa,OAAO,EAAE,GAAG,WAAW;EACxD,IAAI,UAAU,KAAK,kBAAkB,IAAI,GAAG;EAC5C,IAAI,WAAW,MAAM;GACpB,UAAU,KAAK,gCAAgC,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,cAAc;IACzF,KAAK,kBAAkB,OAAO,GAAG;GAClC,CAAC;GACD,KAAK,kBAAkB,IAAI,KAAK,OAAO;EACxC;EAEA,OAAO;CACR;;;;;CAMA,kBAA0B,UAA8E;EACvG,MAAM,UAAU,0BAA0B,QAAQ;EAClD,IAAI,WAAW,MAAM,MAAM,KAAK,kBAAkB,OAAO;CAC1D;CAEA,kBAA0B,QAAiD;EAC1E,OAAO,IAAI,gCAAgC,KAAK,MAAM,mBAAmB,OAAO,EAAE;CACnF;CAEA,uBAAgD;EAC/C,MAAM,qBAAqB;EAC3B,KAAK,qBAAqB,MAAM;EAChC,KAAK,2BAA2B,MAAM;EACtC,KAAK,mBAAmB,MAAM;CAC/B;CAEA,MAAyB,uBACxB,SAC+B;EAC/B,MAAM,QAAQ,MAAM,KAAK,sBAAsB,KAAK;EACpD,MAAM,OAAO,QACV,MAAM,MAAM,uBAAuB,MAAM,gBAAgB,KAAK,IAAI,OAAO,GAAG,KAAK,cAAc,OAAO,CAAC,IACvG,KAAA;EACH,OAAO,OACJ;GACA,UAAU;GACV,IAAI,KAAK;GACT,MAAM,KAAK,QAAQ,KAAA;GACnB,OAAO,KAAK,SAAS,KAAA;GACrB,WAAW,KAAK,aAAa,KAAA;GAC7B,UAAU,KAAK,YAAY,KAAA;EAC5B,IACC,KAAA;CACJ;AACD"}
@@ -1,5 +1,4 @@
1
1
  import { IssueSorting } from "../../../git/models/issue.js";
2
- import "../providerFilters.js";
3
2
  //#region ../plus/integrations/src/providers/issueSorts.d.ts
4
3
  /** GitHub and GHE: one `search` channel serves all three issue reads, so all three order the same way. */
5
4
  export declare const githubIssueSorts: IssueSorting[];
@@ -1,7 +1,6 @@
1
1
  import { PullRequest, PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
2
2
  import { IntegrationIds } from "../constants.js";
3
3
  import { ProviderWarning } from "../results.js";
4
- import "../providerFilters.js";
5
4
  import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
6
5
  import { GitHostIntegration } from "../models/gitHostIntegration.js";
7
6
  import { IntegrationResult } from "../models/integration.js";
@@ -60,7 +60,9 @@ export interface ProviderWarningScope {
60
60
  repositoryId?: string;
61
61
  }
62
62
  /**
63
- * Why a provider refused a credential it otherwise accepts, when the refusal itself says so:
63
+ * Why a provider request failed, when the failure itself says so:
64
+ * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection
65
+ * still exists; this accompanies `kind: 'other'`, never an authentication warning.
64
66
  * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the
65
67
  * organization policy "Third-party application access via OAuth", off by default for new organizations. An
66
68
  * organization admin enables it, or the user connects with a personal access token instead, which the policy
@@ -72,7 +74,7 @@ export interface ProviderWarningScope {
72
74
  *
73
75
  * None of these is healed by reconnecting, which is the point of naming them.
74
76
  */
75
- export type ProviderWarningCauseReason = 'oauth-app-not-allowed' | 'access-denied' | 'conditional-access';
77
+ export type ProviderWarningCauseReason = 'oauth-app-not-allowed' | 'access-denied' | 'conditional-access' | 'unreachable';
76
78
  /** See {@link ProviderWarning.cause}. */
77
79
  export interface ProviderWarningCause {
78
80
  reason: ProviderWarningCauseReason;
@@ -161,11 +163,12 @@ export interface ProviderWarning {
161
163
  */
162
164
  scope?: ProviderWarningScope;
163
165
  /**
164
- * Why the provider refused, when the refusal itself says so: see {@link ProviderWarningCauseReason}. Switch on
166
+ * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on
165
167
  * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.
166
168
  *
167
- * Set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the
168
- * refusals it names are indistinguishable from a dead credential until then: Azure DevOps answers a
169
+ * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are
170
+ * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the
171
+ * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a
169
172
  * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps
170
173
  * reports one today. Its absence proves nothing about the cause.
171
174
  */
@@ -1,4 +1,5 @@
1
1
  import { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from "../../git/errors.js";
2
+ import { isProviderUnreachableError } from "./errors.js";
2
3
  import { isUnsupportedSortError } from "@gitkraken/provider-apis";
3
4
  //#region ../plus/integrations/src/results.ts
4
5
  const maxProviderWarningMessageLength = 500;
@@ -29,7 +30,8 @@ function toProviderWarning(providerId, domain, connectionId, ex) {
29
30
  connectionId,
30
31
  message: providerWarningMessage(ex),
31
32
  kind,
32
- isAuth: kind === "auth"
33
+ isAuth: kind === "auth",
34
+ ...kind === "other" && isProviderUnreachableError(ex) ? { cause: { reason: "unreachable" } } : {}
33
35
  };
34
36
  }
35
37
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n */\nexport type ProviderWarningKind = 'auth' | 'rate-limit' | 'not-found' | 'no-connection' | 'unsupported' | 'other';\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`.\n\t */\n\tresourceId?: string;\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * Why a provider refused a credential it otherwise accepts, when the refusal itself says so:\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason = 'oauth-app-not-allowed' | 'access-denied' | 'conditional-access';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider refused, when the refusal itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * Set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals it names are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads, while `org` is an optional display label.\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause and omission when present) is absent.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tif (into.some(existing => providerWarningKey(existing) === key)) return;\n\n\tinto.push(warning);\n}\n"],"mappings":";;;AAiWA,MAAM,kCAAkC;;AAGxC,SAAgB,uBAAuB,IAAqB;CAC3D,MAAM,OAAO,cAAc,QAAQ,GAAG,UAAU,OAAO,EAAE,EAAA,CAAG,KAAK;CACjE,MAAM,UAAU;CAChB,MAAM,SACL,OAAO,SAAS,WAAW,WACxB,QAAQ,SACR,OAAO,SAAS,UAAU,WAAW,WACpC,QAAQ,SAAS,SACjB,KAAA;CAIL,IAAI,yCAAyC,KAAK,GAAG,GACpD,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAG5E,IAAI,CAAC,KACJ,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAE5E,OAAO,IAAI,SAAS,kCACjB,GAAG,IAAI,MAAM,GAAG,GAAmC,EAAE,OACrD;AACJ;;;;;;AAoCA,SAAgB,kBACf,YACA,QACA,cACA,IACkB;CAClB,IAAI;CACJ,IAAI,cAAc,qBACjB,OAAO;MACD,IAAI,cAAc,uBACxB,OAAO;MACD,IAAI,cAAc,sBACxB,OAAO;MACD,IAAI,uBAAuB,EAAE,GACnC,OAAO;MAEP,OAAO;CAGR,OAAO;EACM;EACJ;EACM;EACd,SAAS,uBAAuB,EAAE;EAC5B;EACN,QAAQ,SAAS;CAClB;AACD;;;;;;;;;;;;AAaA,SAAgB,mBAAmB,OAAyD;CAC3F,OAAO;EAAC,OAAO,cAAc;EAAI,OAAO,cAAc;EAAI,OAAO,aAAa;EAAI,OAAO,gBAAgB;CAAE,CAAC,CAAC,KAC5G,GACD;AACD;;;;;;;;;AAUA,SAAS,2BAA2B,UAAuD;CAC1F,IAAI,YAAY,MAAM,OAAO;CAE7B,OAAO;EACN,SAAS;EACT,SAAS;EACT,SAAS,SAAS;EAClB,SAAS,cAAc;EACvB,mBAAmB,SAAS,KAAK;CAClC,CAAC,CAAC,KAAK,GAAG;AACX;;;;;;;;;;;;;;AAeA,SAAgB,8BAA8B,UAA6B,aAA4B;CACtG,IAAI,CAAC,eAAe,CAAC,SAAS,MAAK,MAAK,EAAE,YAAY,IAAI,GAAG;CAE7D,MAAM,aAAgC,CAAC;CACvC,KAAK,MAAM,WAAW,UAAU;EAC/B,IAAI,QAAQ,YAAY,MAAM;GAC7B,qBAAqB,YAAY,OAAO;GACxC;EACD;EAEA,MAAM,EAAE,UAAU,WAAW,GAAG,SAAS;EACzC,qBAAqB,YAAY,IAAI;CACtC;CAEA,SAAS,OAAO,GAAG,SAAS,QAAQ,GAAG,UAAU;AAClD;;;;;;;;;;;AAYA,SAAS,mBAAmB,SAAkC;CAC7D,OAAO;EACN,QAAQ;EACR,QAAQ,gBAAgB;EACxB,QAAQ,UAAU;EAClB,QAAQ;EACR,mBAAmB,QAAQ,KAAK;EAChC,QAAQ,OAAO,UAAU;EACzB,2BAA2B,QAAQ,QAAQ;EAC3C,QAAQ;CACT,CAAC,CAAC,KAAK,GAAG;AACX;;;;;AAMA,SAAgB,qBAAqB,MAAyB,SAAgC;CAC7F,MAAM,MAAM,mBAAmB,OAAO;CACtC,IAAI,KAAK,MAAK,aAAY,mBAAmB,QAAQ,MAAM,GAAG,GAAG;CAEjE,KAAK,KAAK,OAAO;AAClB"}
1
+ {"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\nimport { isProviderUnreachableError } from './errors.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n */\nexport type ProviderWarningKind = 'auth' | 'rate-limit' | 'not-found' | 'no-connection' | 'unsupported' | 'other';\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`.\n\t */\n\tresourceId?: string;\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * Why a provider request failed, when the failure itself says so:\n * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection\n * still exists; this accompanies `kind: 'other'`, never an authentication warning.\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason =\n\t| 'oauth-app-not-allowed'\n\t| 'access-denied'\n\t| 'conditional-access'\n\t| 'unreachable';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are\n\t * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads, while `org` is an optional display label.\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t\t...(kind === 'other' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause and omission when present) is absent.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tif (into.some(existing => providerWarningKey(existing) === key)) return;\n\n\tinto.push(warning);\n}\n"],"mappings":";;;;AAyWA,MAAM,kCAAkC;;AAGxC,SAAgB,uBAAuB,IAAqB;CAC3D,MAAM,OAAO,cAAc,QAAQ,GAAG,UAAU,OAAO,EAAE,EAAA,CAAG,KAAK;CACjE,MAAM,UAAU;CAChB,MAAM,SACL,OAAO,SAAS,WAAW,WACxB,QAAQ,SACR,OAAO,SAAS,UAAU,WAAW,WACpC,QAAQ,SAAS,SACjB,KAAA;CAIL,IAAI,yCAAyC,KAAK,GAAG,GACpD,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAG5E,IAAI,CAAC,KACJ,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAE5E,OAAO,IAAI,SAAS,kCACjB,GAAG,IAAI,MAAM,GAAG,GAAmC,EAAE,OACrD;AACJ;;;;;;AAoCA,SAAgB,kBACf,YACA,QACA,cACA,IACkB;CAClB,IAAI;CACJ,IAAI,cAAc,qBACjB,OAAO;MACD,IAAI,cAAc,uBACxB,OAAO;MACD,IAAI,cAAc,sBACxB,OAAO;MACD,IAAI,uBAAuB,EAAE,GACnC,OAAO;MAEP,OAAO;CAGR,OAAO;EACM;EACJ;EACM;EACd,SAAS,uBAAuB,EAAE;EAC5B;EACN,QAAQ,SAAS;EACjB,GAAI,SAAS,WAAW,2BAA2B,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,cAAuB,EAAE,IAAI,CAAC;CAC3G;AACD;;;;;;;;;;;;AAaA,SAAgB,mBAAmB,OAAyD;CAC3F,OAAO;EAAC,OAAO,cAAc;EAAI,OAAO,cAAc;EAAI,OAAO,aAAa;EAAI,OAAO,gBAAgB;CAAE,CAAC,CAAC,KAC5G,GACD;AACD;;;;;;;;;AAUA,SAAS,2BAA2B,UAAuD;CAC1F,IAAI,YAAY,MAAM,OAAO;CAE7B,OAAO;EACN,SAAS;EACT,SAAS;EACT,SAAS,SAAS;EAClB,SAAS,cAAc;EACvB,mBAAmB,SAAS,KAAK;CAClC,CAAC,CAAC,KAAK,GAAG;AACX;;;;;;;;;;;;;;AAeA,SAAgB,8BAA8B,UAA6B,aAA4B;CACtG,IAAI,CAAC,eAAe,CAAC,SAAS,MAAK,MAAK,EAAE,YAAY,IAAI,GAAG;CAE7D,MAAM,aAAgC,CAAC;CACvC,KAAK,MAAM,WAAW,UAAU;EAC/B,IAAI,QAAQ,YAAY,MAAM;GAC7B,qBAAqB,YAAY,OAAO;GACxC;EACD;EAEA,MAAM,EAAE,UAAU,WAAW,GAAG,SAAS;EACzC,qBAAqB,YAAY,IAAI;CACtC;CAEA,SAAS,OAAO,GAAG,SAAS,QAAQ,GAAG,UAAU;AAClD;;;;;;;;;;;AAYA,SAAS,mBAAmB,SAAkC;CAC7D,OAAO;EACN,QAAQ;EACR,QAAQ,gBAAgB;EACxB,QAAQ,UAAU;EAClB,QAAQ;EACR,mBAAmB,QAAQ,KAAK;EAChC,QAAQ,OAAO,UAAU;EACzB,2BAA2B,QAAQ,QAAQ;EAC3C,QAAQ;CACT,CAAC,CAAC,KAAK,GAAG;AACX;;;;;AAMA,SAAgB,qBAAqB,MAAyB,SAAgC;CAC7F,MAAM,MAAM,mBAAmB,OAAO;CACtC,IAAI,KAAK,MAAK,aAAY,mBAAmB,QAAQ,MAAM,GAAG,GAAG;CAEjE,KAAK,KAAK,OAAO;AAClB"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gitkraken/core-gitlens",
3
3
  "description": "GitLens core — shared Git / AI / GitHub primitives for internal GitKraken consumption",
4
- "version": "0.10.2",
4
+ "version": "0.10.3",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": {
7
7
  "name": "GitKraken",